From c15bb5d3de084a10ade0a99e2f6ec1226aa9356a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 5 Jul 2020 22:58:47 +0300 Subject: [PATCH 0001/2846] 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 0002/2846] 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 70764bef4f0915c192213fc8c080a50221679982 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Jul 2020 17:01:27 -0300 Subject: [PATCH 0003/2846] 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 0004/2846] 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 0005/2846] 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 0006/2846] 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 0007/2846] 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 0008/2846] 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 0009/2846] 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 0010/2846] 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 0011/2846] 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 0012/2846] 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 0013/2846] 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 0014/2846] 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 0015/2846] 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 0016/2846] 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 0017/2846] 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 0018/2846] 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 0019/2846] 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 0020/2846] 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 0021/2846] 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 0022/2846] 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 0023/2846] 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 0024/2846] 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 0025/2846] 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 0026/2846] 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 0027/2846] 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 0028/2846] 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 0029/2846] 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 0030/2846] 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 0031/2846] 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 0032/2846] 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 0033/2846] 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 0034/2846] 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 0035/2846] 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 0036/2846] 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 0037/2846] 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 0038/2846] 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 0039/2846] 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 0040/2846] 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 0041/2846] 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 0042/2846] 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 0043/2846] 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 0044/2846] 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 0045/2846] 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 0046/2846] 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 0047/2846] 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 0048/2846] 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 0049/2846] 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 0050/2846] 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 0051/2846] 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 0052/2846] 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 0053/2846] 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 0054/2846] 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 0055/2846] 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 0056/2846] 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 0057/2846] 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 0058/2846] 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 0059/2846] 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 0060/2846] 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 0061/2846] 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 0062/2846] 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 0063/2846] 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 0064/2846] 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 0065/2846] 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 0066/2846] 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 0067/2846] 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 0068/2846] 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 0069/2846] 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 0070/2846] 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 0071/2846] 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 0072/2846] 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 0073/2846] 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 0074/2846] 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 0075/2846] 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 0076/2846] 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 0077/2846] 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 0078/2846] 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 0079/2846] 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 0080/2846] 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 0081/2846] 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 0082/2846] 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 0083/2846] 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 0084/2846] 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 0085/2846] 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 0086/2846] 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 0087/2846] 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 0088/2846] 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 0089/2846] 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 0090/2846] 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 0091/2846] 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 0092/2846] 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 0093/2846] 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 0094/2846] 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 0095/2846] 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 0096/2846] 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 0097/2846] 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 0098/2846] 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 0099/2846] 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 0100/2846] 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 0101/2846] 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 0102/2846] 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 0103/2846] 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 0104/2846] 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 0105/2846] 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 0106/2846] 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 0107/2846] 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 0108/2846] 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 0109/2846] 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 0110/2846] 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 0111/2846] 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 0112/2846] 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 0113/2846] 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 0114/2846] 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 0115/2846] 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 0116/2846] 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 0117/2846] 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 0118/2846] 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 0119/2846] 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 0120/2846] 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 0121/2846] 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 0122/2846] 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 0123/2846] 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 0124/2846] 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 0125/2846] 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 0126/2846] 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 0127/2846] 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 0128/2846] 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 0129/2846] [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 0130/2846] 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 0131/2846] 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 0132/2846] 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 0133/2846] 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 0134/2846] 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 0135/2846] 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 0136/2846] 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 0137/2846] 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 0138/2846] 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 0139/2846] 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 0140/2846] 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 0141/2846] 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 0142/2846] 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 0143/2846] 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 0144/2846] 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 0145/2846] 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 0146/2846] 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 0147/2846] 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 0148/2846] 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 0149/2846] 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 0150/2846] 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 0151/2846] 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 0152/2846] 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 0153/2846] 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 0154/2846] 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 0155/2846] 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 0156/2846] 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 0157/2846] 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 0158/2846] 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 0159/2846] 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 0160/2846] 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 0161/2846] 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 0162/2846] 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 0163/2846] 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 0164/2846] 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 0165/2846] 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 0166/2846] 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 0167/2846] 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 0168/2846] 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 0169/2846] 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 0170/2846] 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 0171/2846] 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 0172/2846] 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 0173/2846] 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 0174/2846] 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 0175/2846] 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 0176/2846] 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 0177/2846] 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 0178/2846] 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 0179/2846] 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 0180/2846] 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 0181/2846] 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 0182/2846] 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 0183/2846] 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 0184/2846] 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 0185/2846] 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 0186/2846] 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 0187/2846] 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 0188/2846] 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 0189/2846] 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 0190/2846] 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 0191/2846] 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 0192/2846] 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 0193/2846] 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 0194/2846] 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 0195/2846] 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 0196/2846] 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 0197/2846] 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 0198/2846] 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 0199/2846] 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 0200/2846] 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 0201/2846] 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 0202/2846] 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 0203/2846] 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 0204/2846] 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 0205/2846] 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 0206/2846] 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 0207/2846] 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 0208/2846] 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 0209/2846] 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 0210/2846] 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 0211/2846] 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 0212/2846] 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 0213/2846] 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 0214/2846] 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 0215/2846] 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 0216/2846] 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 0217/2846] 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 0218/2846] 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 0219/2846] 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 0220/2846] 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 0221/2846] 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 0222/2846] 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 0223/2846] 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 0224/2846] 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 0225/2846] 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 0226/2846] #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 0227/2846] 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 0228/2846] 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 0229/2846] 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 0230/2846] 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 0231/2846] 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 0232/2846] 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 0233/2846] 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 0234/2846] 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 0235/2846] 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 0236/2846] 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 0237/2846] 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 0238/2846] #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 0239/2846] 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 0240/2846] #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 0241/2846] 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 0242/2846] 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 0243/2846] 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 0244/2846] 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 0245/2846] 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 0246/2846] 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 0247/2846] 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 0248/2846] 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 0249/2846] 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 0250/2846] 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 0251/2846] 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 0252/2846] 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 0253/2846] 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 0254/2846] 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 0255/2846] 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 0256/2846] #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 0257/2846] 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 0258/2846] 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 0259/2846] 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 0260/2846] #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 0261/2846] 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 0262/2846] 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 0263/2846] 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 0264/2846] 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 0265/2846] 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 0266/2846] 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 0267/2846] 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 0268/2846] 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 0269/2846] 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 0270/2846] 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 0271/2846] 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 0272/2846] 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 0273/2846] 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 0274/2846] 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 0275/2846] 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 0276/2846] 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 0277/2846] 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 0278/2846] 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 0279/2846] 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 0280/2846] 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 0281/2846] 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 0282/2846] 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 0283/2846] 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 0284/2846] 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 0285/2846] 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 0286/2846] 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 0287/2846] 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 0288/2846] 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 0289/2846] 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 0290/2846] 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 0291/2846] 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 0292/2846] 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 0293/2846] 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 0294/2846] 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 0295/2846] 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 0296/2846] 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 0297/2846] 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 0298/2846] 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 0299/2846] 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 0300/2846] 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 0301/2846] 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 0302/2846] 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 0303/2846] 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 0304/2846] 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 0305/2846] 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 0306/2846] 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 0307/2846] 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 0308/2846] 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 0309/2846] 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 0310/2846] 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 0311/2846] 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 572dfcd160299489e66454de89a608da6f6d468e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 12 Dec 2020 08:49:58 -0300 Subject: [PATCH 0312/2846] Compare also paths on Windows when considering ImportPathMismatchError On Windows, os.path.samefile returns false for paths mounted in UNC paths which point to the same location. I couldn't reproduce the actual case reported, but looking at the code it seems this commit should fix the issue. Fix #7678 Fix #8076 --- 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 ed658d682961a602305112bee72aa1e8843479f2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 11 Dec 2020 13:40:37 +0200 Subject: [PATCH 0313/2846] Some py.path.local -> pathlib.Path - Some conftest related functions - _confcutdir - Allow arbitrary os.PathLike[str] in gethookproxy. --- src/_pytest/config/__init__.py | 70 +++++++++++---------- src/_pytest/doctest.py | 3 +- src/_pytest/main.py | 18 +++--- src/_pytest/nodes.py | 2 +- src/_pytest/python.py | 2 +- testing/python/fixtures.py | 12 ++-- testing/test_collection.py | 4 +- testing/test_config.py | 18 +++--- testing/test_conftest.py | 109 +++++++++++++++++---------------- testing/test_pluginmanager.py | 29 +++++---- 10 files changed, 141 insertions(+), 126 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index bd9e2883f9f..0df4ffa01c1 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -50,9 +50,11 @@ from _pytest.compat import importlib_metadata from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath from _pytest.pathlib import import_path from _pytest.pathlib import ImportMode +from _pytest.pathlib import resolve_package_path from _pytest.store import Store from _pytest.warning_types import PytestConfigWarning @@ -102,9 +104,7 @@ class ExitCode(enum.IntEnum): class ConftestImportFailure(Exception): def __init__( - self, - path: py.path.local, - excinfo: Tuple[Type[Exception], Exception, TracebackType], + self, path: Path, excinfo: Tuple[Type[Exception], Exception, TracebackType], ) -> None: super().__init__(path, excinfo) self.path = path @@ -342,9 +342,9 @@ def __init__(self) -> None: self._conftest_plugins: Set[types.ModuleType] = set() # State related to local conftest plugins. - self._dirpath2confmods: Dict[py.path.local, List[types.ModuleType]] = {} + self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {} self._conftestpath2mod: Dict[Path, types.ModuleType] = {} - self._confcutdir: Optional[py.path.local] = None + self._confcutdir: Optional[Path] = None self._noconftest = False self._duplicatepaths: Set[py.path.local] = set() @@ -479,9 +479,9 @@ def _set_initial_conftests(self, namespace: argparse.Namespace) -> None: All builtin and 3rd party plugins will have been loaded, however, so common options will not confuse our logic here. """ - current = py.path.local() + current = Path.cwd() self._confcutdir = ( - current.join(namespace.confcutdir, abs=True) + absolutepath(current / namespace.confcutdir) if namespace.confcutdir else None ) @@ -495,7 +495,7 @@ def _set_initial_conftests(self, namespace: argparse.Namespace) -> None: i = path.find("::") if i != -1: path = path[:i] - anchor = current.join(path, abs=1) + anchor = absolutepath(current / path) if anchor.exists(): # we found some file object self._try_load_conftest(anchor, namespace.importmode) foundanchor = True @@ -503,24 +503,24 @@ def _set_initial_conftests(self, namespace: argparse.Namespace) -> None: self._try_load_conftest(current, namespace.importmode) def _try_load_conftest( - self, anchor: py.path.local, importmode: Union[str, ImportMode] + self, anchor: Path, importmode: Union[str, ImportMode] ) -> None: 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): + if anchor.is_dir(): + for x in anchor.glob("test*"): + if x.is_dir(): self._getconftestmodules(x, importmode) @lru_cache(maxsize=128) def _getconftestmodules( - self, path: py.path.local, importmode: Union[str, ImportMode], + self, path: Path, importmode: Union[str, ImportMode], ) -> List[types.ModuleType]: if self._noconftest: return [] - if path.isfile(): - directory = path.dirpath() + if path.is_file(): + directory = path.parent else: directory = path @@ -528,18 +528,18 @@ def _getconftestmodules( # and allow users to opt into looking into the rootdir parent # directories instead of requiring to specify confcutdir. clist = [] - for parent in directory.parts(): - if self._confcutdir and self._confcutdir.relto(parent): + for parent in reversed((directory, *directory.parents)): + if self._confcutdir and parent in self._confcutdir.parents: continue - conftestpath = parent.join("conftest.py") - if conftestpath.isfile(): + conftestpath = parent / "conftest.py" + if conftestpath.is_file(): mod = self._importconftest(conftestpath, importmode) clist.append(mod) self._dirpath2confmods[directory] = clist return clist def _rget_with_confmod( - self, name: str, path: py.path.local, importmode: Union[str, ImportMode], + self, name: str, path: Path, importmode: Union[str, ImportMode], ) -> Tuple[types.ModuleType, Any]: modules = self._getconftestmodules(path, importmode) for mod in reversed(modules): @@ -550,21 +550,21 @@ def _rget_with_confmod( raise KeyError(name) def _importconftest( - self, conftestpath: py.path.local, importmode: Union[str, ImportMode], + self, conftestpath: Path, 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. # 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() + key = conftestpath.resolve() with contextlib.suppress(KeyError): return self._conftestpath2mod[key] - pkgpath = conftestpath.pypkgpath() + pkgpath = resolve_package_path(conftestpath) if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.purebasename) + _ensure_removed_sysmodule(conftestpath.stem) try: mod = import_path(conftestpath, mode=importmode) @@ -577,10 +577,10 @@ def _importconftest( self._conftest_plugins.add(mod) self._conftestpath2mod[key] = mod - dirpath = conftestpath.dirpath() + dirpath = conftestpath.parent if dirpath in self._dirpath2confmods: for path, mods in self._dirpath2confmods.items(): - if path and path.relto(dirpath) or path == dirpath: + if path and dirpath in path.parents or path == dirpath: assert mod not in mods mods.append(mod) self.trace(f"loading conftestmodule {mod!r}") @@ -588,7 +588,7 @@ def _importconftest( return mod def _check_non_top_pytest_plugins( - self, mod: types.ModuleType, conftestpath: py.path.local, + self, mod: types.ModuleType, conftestpath: Path, ) -> None: if ( hasattr(mod, "pytest_plugins") @@ -1412,21 +1412,23 @@ def _getini(self, name: str): assert type in [None, "string"] return value - def _getconftest_pathlist( - self, name: str, path: py.path.local - ) -> Optional[List[py.path.local]]: + def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]: try: mod, relroots = self.pluginmanager._rget_with_confmod( name, path, self.getoption("importmode") ) except KeyError: return None - modpath = py.path.local(mod.__file__).dirpath() - values: List[py.path.local] = [] + modpath = Path(mod.__file__).parent + values: List[Path] = [] for relroot in relroots: - if not isinstance(relroot, py.path.local): + if isinstance(relroot, Path): + pass + elif isinstance(relroot, py.path.local): + relroot = Path(relroot) + else: relroot = relroot.replace("/", os.sep) - relroot = modpath.join(relroot, abs=True) + relroot = absolutepath(modpath / relroot) values.append(relroot) return values diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 64e8f0e0eee..d0b6b4c4185 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -7,6 +7,7 @@ import types import warnings from contextlib import contextmanager +from pathlib import Path from typing import Any from typing import Callable from typing import Dict @@ -525,7 +526,7 @@ def _find( if self.fspath.basename == "conftest.py": module = self.config.pluginmanager._importconftest( - self.fspath, self.config.getoption("importmode") + Path(self.fspath), self.config.getoption("importmode") ) else: try: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 41a33d4494c..eab3c9afd27 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -371,22 +371,23 @@ def _in_venv(path: py.path.local) -> bool: def pytest_ignore_collect(path: py.path.local, config: Config) -> Optional[bool]: - ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath()) + path_ = Path(path) + ignore_paths = config._getconftest_pathlist("collect_ignore", path=path_.parent) ignore_paths = ignore_paths or [] excludeopt = config.getoption("ignore") if excludeopt: - ignore_paths.extend([py.path.local(x) for x in excludeopt]) + ignore_paths.extend(absolutepath(x) for x in excludeopt) - if py.path.local(path) in ignore_paths: + if path_ in ignore_paths: return True ignore_globs = config._getconftest_pathlist( - "collect_ignore_glob", path=path.dirpath() + "collect_ignore_glob", path=path_.parent ) ignore_globs = ignore_globs or [] excludeglobopt = config.getoption("ignore_glob") if excludeglobopt: - ignore_globs.extend([py.path.local(x) for x in excludeglobopt]) + ignore_globs.extend(absolutepath(x) for x in excludeglobopt) if any(fnmatch.fnmatch(str(path), str(glob)) for glob in ignore_globs): return True @@ -512,12 +513,12 @@ def pytest_runtest_logreport( def isinitpath(self, path: py.path.local) -> bool: return path in self._initialpaths - def gethookproxy(self, fspath: py.path.local): + def gethookproxy(self, fspath: "os.PathLike[str]"): # 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") + Path(fspath), self.config.getoption("importmode") ) remove_mods = pm._conftest_plugins.difference(my_conftestmodules) if remove_mods: @@ -668,8 +669,9 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # No point in finding packages when collecting doctests. if not self.config.getoption("doctestmodules", False): pm = self.config.pluginmanager + confcutdir = py.path.local(pm._confcutdir) if pm._confcutdir else None for parent in reversed(argpath.parts()): - if pm._confcutdir and pm._confcutdir.relto(parent): + if confcutdir and confcutdir.relto(parent): break if parent.isdir(): diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 27434fb6a67..98bd581b96d 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -520,7 +520,7 @@ 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): + def gethookproxy(self, fspath: "os.PathLike[str]"): warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.gethookproxy(fspath) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e48e7531c19..407f924a5f1 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -653,7 +653,7 @@ def setup(self) -> None: func = partial(_call_with_optional_argument, teardown_module, self.obj) self.addfinalizer(func) - def gethookproxy(self, fspath: py.path.local): + def gethookproxy(self, fspath: "os.PathLike[str]"): warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.gethookproxy(fspath) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 94547dd245c..ac62de608e5 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 Pytester from _pytest.pytester import Testdir @@ -1961,8 +1962,10 @@ def test_result(arg): reprec = testdir.inline_run("-v", "-s") reprec.assertoutcome(passed=4) - def test_class_function_parametrization_finalization(self, testdir): - p = testdir.makeconftest( + def test_class_function_parametrization_finalization( + self, pytester: Pytester + ) -> None: + p = pytester.makeconftest( """ import pytest import pprint @@ -1984,7 +1987,7 @@ def fin(): request.addfinalizer(fin) """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1996,8 +1999,7 @@ def test_2(self): pass """ ) - confcut = f"--confcutdir={testdir.tmpdir}" - reprec = testdir.inline_run("-v", "-s", confcut) + reprec = pytester.inline_run("-v", "-s", "--confcutdir", pytester.path) reprec.assertoutcome(passed=8) config = reprec.getcalls("pytest_unconfigure")[0].config values = config.pluginmanager._getconftestmodules(p, importmode="prepend")[ diff --git a/testing/test_collection.py b/testing/test_collection.py index 1138c2bd6f5..862c1aba8d2 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -364,7 +364,9 @@ def pytest_ignore_collect(path, config): def test_collectignore_exclude_on_option(self, pytester: Pytester) -> None: pytester.makeconftest( """ - collect_ignore = ['hello', 'test_world.py'] + import py + from pathlib import Path + collect_ignore = [py.path.local('hello'), 'test_world.py', Path('bye')] def pytest_addoption(parser): parser.addoption("--XX", action="store_true", default=False) def pytest_configure(config): diff --git a/testing/test_config.py b/testing/test_config.py index b931797d429..eacc9c9ebdd 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -574,16 +574,16 @@ def test_getoption(self, pytester: Pytester) -> None: config.getvalue("x") assert config.getoption("x", 1) == 1 - 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)) + def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None: + somepath = tmp_path.joinpath("x", "y", "z") + p = tmp_path.joinpath("conftest.py") + p.write_text(f"pathlist = ['.', {str(somepath)!r}]") config = pytester.parseconfigure(p) - assert config._getconftest_pathlist("notexist", path=tmpdir) is None - pl = config._getconftest_pathlist("pathlist", path=tmpdir) or [] + assert config._getconftest_pathlist("notexist", path=tmp_path) is None + pl = config._getconftest_pathlist("pathlist", path=tmp_path) or [] print(pl) assert len(pl) == 2 - assert pl[0] == tmpdir + assert pl[0] == tmp_path assert pl[1] == somepath @pytest.mark.parametrize("maybe_type", ["not passed", "None", '"string"']) @@ -1935,10 +1935,10 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives assert msg not in res.stdout.str() -def test_conftest_import_error_repr(tmpdir: py.path.local) -> None: +def test_conftest_import_error_repr(tmp_path: Path) -> None: """`ConftestImportFailure` should use a short error message and readable path to the failed conftest.py file.""" - path = tmpdir.join("foo/conftest.py") + path = tmp_path.joinpath("foo/conftest.py") with pytest.raises( ConftestImportFailure, match=re.escape(f"RuntimeError: some error (from {path})"), diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 638321728d7..36e83191bcd 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import cast from typing import Dict +from typing import Generator from typing import List from typing import Optional @@ -15,7 +16,7 @@ from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import symlink_or_skip from _pytest.pytester import Pytester -from _pytest.pytester import Testdir +from _pytest.tmpdir import TempPathFactory def ConftestWithSetinitial(path) -> PytestPluginManager: @@ -25,12 +26,12 @@ def ConftestWithSetinitial(path) -> PytestPluginManager: def conftest_setinitial( - conftest: PytestPluginManager, args, confcutdir: Optional[py.path.local] = None + conftest: PytestPluginManager, args, confcutdir: Optional["os.PathLike[str]"] = None ) -> None: class Namespace: def __init__(self) -> None: self.file_or_dir = args - self.confcutdir = str(confcutdir) + self.confcutdir = os.fspath(confcutdir) if confcutdir is not None else None self.noconftest = False self.pyargs = False self.importmode = "prepend" @@ -42,54 +43,58 @@ def __init__(self) -> None: @pytest.mark.usefixtures("_sys_snapshot") class TestConftestValueAccessGlobal: @pytest.fixture(scope="module", params=["global", "inpackage"]) - def basedir(self, request, tmpdir_factory): - tmpdir = tmpdir_factory.mktemp("basedir", numbered=True) - tmpdir.ensure("adir/conftest.py").write("a=1 ; Directory = 3") - tmpdir.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5") + def basedir( + self, request, tmp_path_factory: TempPathFactory + ) -> Generator[Path, None, None]: + tmpdir = tmp_path_factory.mktemp("basedir", numbered=True) + tmpdir.joinpath("adir/b").mkdir(parents=True) + tmpdir.joinpath("adir/conftest.py").write_text("a=1 ; Directory = 3") + tmpdir.joinpath("adir/b/conftest.py").write_text("b=2 ; a = 1.5") if request.param == "inpackage": - tmpdir.ensure("adir/__init__.py") - tmpdir.ensure("adir/b/__init__.py") + tmpdir.joinpath("adir/__init__.py").touch() + tmpdir.joinpath("adir/b/__init__.py").touch() yield tmpdir - def test_basic_init(self, basedir): + def test_basic_init(self, basedir: Path) -> None: conftest = PytestPluginManager() - p = basedir.join("adir") + p = basedir / "adir" assert conftest._rget_with_confmod("a", p, importmode="prepend")[1] == 1 - def test_immediate_initialiation_and_incremental_are_the_same(self, basedir): + def test_immediate_initialiation_and_incremental_are_the_same( + self, basedir: Path + ) -> None: conftest = PytestPluginManager() assert not len(conftest._dirpath2confmods) conftest._getconftestmodules(basedir, importmode="prepend") snap1 = len(conftest._dirpath2confmods) assert snap1 == 1 - conftest._getconftestmodules(basedir.join("adir"), importmode="prepend") + conftest._getconftestmodules(basedir / "adir", importmode="prepend") assert len(conftest._dirpath2confmods) == snap1 + 1 - conftest._getconftestmodules(basedir.join("b"), importmode="prepend") + conftest._getconftestmodules(basedir / "b", importmode="prepend") assert len(conftest._dirpath2confmods) == snap1 + 2 - def test_value_access_not_existing(self, basedir): + def test_value_access_not_existing(self, basedir: Path) -> None: conftest = ConftestWithSetinitial(basedir) with pytest.raises(KeyError): conftest._rget_with_confmod("a", basedir, importmode="prepend") - def test_value_access_by_path(self, basedir): + def test_value_access_by_path(self, basedir: Path) -> None: conftest = ConftestWithSetinitial(basedir) - adir = basedir.join("adir") + adir = basedir / "adir" 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 + conftest._rget_with_confmod("a", adir / "b", importmode="prepend")[1] == 1.5 ) - def test_value_access_with_confmod(self, basedir): - startdir = basedir.join("adir", "b") - startdir.ensure("xx", dir=True) + def test_value_access_with_confmod(self, basedir: Path) -> None: + startdir = basedir / "adir" / "b" + startdir.joinpath("xx").mkdir() conftest = ConftestWithSetinitial(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") + assert path.dirpath() == basedir / "adir" / "b" assert path.purebasename.startswith("conftest") @@ -102,12 +107,12 @@ def test_conftest_in_nonpkg_with_init(tmp_path: Path, _sys_snapshot) -> None: ConftestWithSetinitial(tmp_path.joinpath("adir-1.0", "b")) -def test_doubledash_considered(testdir: Testdir) -> None: - conf = testdir.mkdir("--option") - conf.join("conftest.py").ensure() +def test_doubledash_considered(pytester: Pytester) -> None: + conf = pytester.mkdir("--option") + conf.joinpath("conftest.py").touch() conftest = PytestPluginManager() - conftest_setinitial(conftest, [conf.basename, conf.basename]) - values = conftest._getconftestmodules(py.path.local(conf), importmode="prepend") + conftest_setinitial(conftest, [conf.name, conf.name]) + values = conftest._getconftestmodules(conf, importmode="prepend") assert len(values) == 1 @@ -127,15 +132,18 @@ def test_conftest_global_import(pytester: Pytester) -> None: pytester.makeconftest("x=3") p = pytester.makepyfile( """ - import py, pytest + from pathlib import Path + import pytest from _pytest.config import PytestPluginManager conf = PytestPluginManager() - mod = conf._importconftest(py.path.local("conftest.py"), importmode="prepend") + mod = conf._importconftest(Path("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") + sub = Path("sub") + sub.mkdir() + subconf = sub / "conftest.py" + subconf.write_text("y=4") mod2 = conf._importconftest(subconf, importmode="prepend") assert mod != mod2 assert mod2.y == 4 @@ -147,19 +155,19 @@ def test_conftest_global_import(pytester: Pytester) -> None: assert res.ret == 0 -def test_conftestcutdir(testdir: Testdir) -> None: - conf = testdir.makeconftest("") - p = testdir.mkdir("x") +def test_conftestcutdir(pytester: Pytester) -> None: + conf = pytester.makeconftest("") + p = pytester.mkdir("x") conftest = PytestPluginManager() - conftest_setinitial(conftest, [testdir.tmpdir], confcutdir=p) + conftest_setinitial(conftest, [pytester.path], confcutdir=p) values = conftest._getconftestmodules(p, importmode="prepend") assert len(values) == 0 - values = conftest._getconftestmodules(conf.dirpath(), importmode="prepend") + values = conftest._getconftestmodules(conf.parent, importmode="prepend") assert len(values) == 0 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") + values = conftest._getconftestmodules(conf.parent, importmode="prepend") assert values[0].__file__.startswith(str(conf)) # and all sub paths get updated properly values = conftest._getconftestmodules(p, importmode="prepend") @@ -170,10 +178,8 @@ def test_conftestcutdir(testdir: Testdir) -> None: def test_conftestcutdir_inplace_considered(pytester: Pytester) -> None: conf = pytester.makeconftest("") conftest = PytestPluginManager() - conftest_setinitial(conftest, [conf.parent], confcutdir=py.path.local(conf.parent)) - values = conftest._getconftestmodules( - py.path.local(conf.parent), importmode="prepend" - ) + conftest_setinitial(conftest, [conf.parent], confcutdir=conf.parent) + values = conftest._getconftestmodules(conf.parent, importmode="prepend") assert len(values) == 1 assert values[0].__file__.startswith(str(conf)) @@ -184,7 +190,7 @@ def test_setinitial_conftest_subdirs(pytester: Pytester, name: str) -> None: subconftest = sub.joinpath("conftest.py") subconftest.touch() conftest = PytestPluginManager() - conftest_setinitial(conftest, [sub.parent], confcutdir=py.path.local(pytester.path)) + conftest_setinitial(conftest, [sub.parent], confcutdir=pytester.path) key = subconftest.resolve() if name not in ("whatever", ".dotdir"): assert key in conftest._conftestpath2mod @@ -337,22 +343,19 @@ def pytest_addoption(parser): result.stdout.fnmatch_lines(["*--xyz*"]) -def test_conftest_import_order(testdir: Testdir, monkeypatch: MonkeyPatch) -> None: - ct1 = testdir.makeconftest("") - sub = testdir.mkdir("sub") - ct2 = sub.join("conftest.py") - ct2.write("") +def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: + ct1 = pytester.makeconftest("") + sub = pytester.mkdir("sub") + ct2 = sub / "conftest.py" + ct2.write_text("") def impct(p, importmode): return p conftest = PytestPluginManager() - conftest._confcutdir = testdir.tmpdir + conftest._confcutdir = pytester.path monkeypatch.setattr(conftest, "_importconftest", impct) - mods = cast( - List[py.path.local], - conftest._getconftestmodules(py.path.local(sub), importmode="prepend"), - ) + mods = cast(List[Path], conftest._getconftestmodules(sub, importmode="prepend")) expected = [ct1, ct2] assert mods == expected diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 2099f5ae1e3..89f10a7db64 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -8,6 +8,7 @@ from _pytest.config import PytestPluginManager from _pytest.config.exceptions import UsageError from _pytest.main import Session +from _pytest.pytester import Pytester @pytest.fixture @@ -16,14 +17,16 @@ def pytestpm() -> PytestPluginManager: class TestPytestPluginInteractions: - def test_addhooks_conftestplugin(self, testdir, _config_for_test): - testdir.makepyfile( + def test_addhooks_conftestplugin( + self, pytester: Pytester, _config_for_test + ) -> None: + pytester.makepyfile( newhooks=""" def pytest_myhook(xyz): "new hook" """ ) - conf = testdir.makeconftest( + conf = pytester.makeconftest( """ import newhooks def pytest_addhooks(pluginmanager): @@ -54,10 +57,10 @@ def pytest_addhooks(pluginmanager): assert res.ret != 0 res.stderr.fnmatch_lines(["*did not find*sys*"]) - def test_do_option_postinitialize(self, testdir): - config = testdir.parseconfigure() + def test_do_option_postinitialize(self, pytester: Pytester) -> None: + config = pytester.parseconfigure() assert not hasattr(config.option, "test123") - p = testdir.makepyfile( + p = pytester.makepyfile( """ def pytest_addoption(parser): parser.addoption('--test123', action="store_true", @@ -120,20 +123,20 @@ def pytest_plugin_registered(self): finally: undo() - def test_hook_proxy(self, testdir): + def test_hook_proxy(self, pytester: Pytester) -> None: """Test the gethookproxy function(#2016)""" - config = testdir.parseconfig() + config = pytester.parseconfig() session = Session.from_config(config) - testdir.makepyfile(**{"tests/conftest.py": "", "tests/subdir/conftest.py": ""}) + pytester.makepyfile(**{"tests/conftest.py": "", "tests/subdir/conftest.py": ""}) - conftest1 = testdir.tmpdir.join("tests/conftest.py") - conftest2 = testdir.tmpdir.join("tests/subdir/conftest.py") + conftest1 = pytester.path.joinpath("tests/conftest.py") + conftest2 = pytester.path.joinpath("tests/subdir/conftest.py") config.pluginmanager._importconftest(conftest1, importmode="prepend") - ihook_a = session.gethookproxy(testdir.tmpdir.join("tests")) + ihook_a = session.gethookproxy(pytester.path / "tests") assert ihook_a is not None config.pluginmanager._importconftest(conftest2, importmode="prepend") - ihook_b = session.gethookproxy(testdir.tmpdir.join("tests")) + ihook_b = session.gethookproxy(pytester.path / "tests") assert ihook_a is not ihook_b def test_hook_with_addoption(self, testdir): From b16c0912537bee06c83e202112f4b036e4fd66dc Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 27 Aug 2020 17:52:16 +0100 Subject: [PATCH 0314/2846] 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 0315/2846] 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 0316/2846] 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 0317/2846] 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 0318/2846] 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 0319/2846] 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 54a7356a9fb165d7d3e48153ede01cb62f05ecee Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 12 Dec 2020 23:21:28 +0200 Subject: [PATCH 0320/2846] Merge pull request #8130 from pytest-dev/release-6.2.0 Prepare release 6.2.0 (cherry picked from commit c475106f12ed87fe908544ff383c5205638c086d) --- 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 28588bf535b71680df7dbe6bbb936148f06a94ac Mon Sep 17 00:00:00 2001 From: bot2x Date: Sun, 6 Dec 2020 14:50:33 +0530 Subject: [PATCH 0321/2846] migrates test_warnings.py from testdir to pytester --- testing/test_warnings.py | 224 ++++++++++++++++++++------------------- 1 file changed, 114 insertions(+), 110 deletions(-) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 66898041f08..574f3f1ec02 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -6,18 +6,18 @@ import pytest from _pytest.fixtures import FixtureRequest -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester WARNINGS_SUMMARY_HEADER = "warnings summary" @pytest.fixture -def pyfile_with_warnings(testdir: Testdir, request: FixtureRequest) -> str: +def pyfile_with_warnings(pytester: Pytester, request: FixtureRequest) -> str: """Create a test file which calls a function in a module which generates warnings.""" - testdir.syspathinsert() + pytester.syspathinsert() test_name = request.function.__name__ module_name = test_name.lstrip("test_") + "_module" - test_file = testdir.makepyfile( + test_file = pytester.makepyfile( """ import {module_name} def test_func(): @@ -39,9 +39,9 @@ def foo(): @pytest.mark.filterwarnings("default") -def test_normal_flow(testdir, pyfile_with_warnings): +def test_normal_flow(pytester: Pytester, pyfile_with_warnings) -> None: """Check that the warnings section is displayed.""" - result = testdir.runpytest(pyfile_with_warnings) + result = pytester.runpytest(pyfile_with_warnings) result.stdout.fnmatch_lines( [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, @@ -56,8 +56,8 @@ def test_normal_flow(testdir, pyfile_with_warnings): @pytest.mark.filterwarnings("always") -def test_setup_teardown_warnings(testdir): - testdir.makepyfile( +def test_setup_teardown_warnings(pytester: Pytester) -> None: + pytester.makepyfile( """ import warnings import pytest @@ -72,7 +72,7 @@ def test_func(fix): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, @@ -86,10 +86,10 @@ def test_func(fix): @pytest.mark.parametrize("method", ["cmdline", "ini"]) -def test_as_errors(testdir, pyfile_with_warnings, method): +def test_as_errors(pytester: Pytester, pyfile_with_warnings, method) -> None: args = ("-W", "error") if method == "cmdline" else () if method == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] filterwarnings=error @@ -97,7 +97,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, pyfile_with_warnings) + result = pytester.runpytest_subprocess(*args, pyfile_with_warnings) result.stdout.fnmatch_lines( [ "E UserWarning: user warning", @@ -108,24 +108,24 @@ def test_as_errors(testdir, pyfile_with_warnings, method): @pytest.mark.parametrize("method", ["cmdline", "ini"]) -def test_ignore(testdir, pyfile_with_warnings, method): +def test_ignore(pytester: Pytester, pyfile_with_warnings, method) -> None: args = ("-W", "ignore") if method == "cmdline" else () if method == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] filterwarnings= ignore """ ) - result = testdir.runpytest(*args, pyfile_with_warnings) + result = pytester.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): - testdir.makepyfile( +def test_unicode(pytester: Pytester) -> None: + pytester.makepyfile( """ import warnings import pytest @@ -140,7 +140,7 @@ def test_func(fix): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, @@ -150,9 +150,9 @@ def test_func(fix): ) -def test_works_with_filterwarnings(testdir): +def test_works_with_filterwarnings(pytester: Pytester) -> None: """Ensure our warnings capture does not mess with pre-installed filters (#2430).""" - testdir.makepyfile( + pytester.makepyfile( """ import warnings @@ -170,22 +170,22 @@ def test_my_warning(self): assert True """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*== 1 passed in *"]) @pytest.mark.parametrize("default_config", ["ini", "cmdline"]) -def test_filterwarnings_mark(testdir, default_config): +def test_filterwarnings_mark(pytester: Pytester, default_config) -> None: """Test ``filterwarnings`` mark works and takes precedence over command line and ini options.""" if default_config == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] filterwarnings = always """ ) - testdir.makepyfile( + pytester.makepyfile( """ import warnings import pytest @@ -202,13 +202,13 @@ def test_show_warning(): warnings.warn(RuntimeWarning()) """ ) - result = testdir.runpytest("-W always" if default_config == "cmdline" else "") + result = pytester.runpytest("-W always" if default_config == "cmdline" else "") result.stdout.fnmatch_lines(["*= 1 failed, 2 passed, 1 warning in *"]) -def test_non_string_warning_argument(testdir): +def test_non_string_warning_argument(pytester: Pytester) -> None: """Non-str argument passed to warning breaks pytest (#2956)""" - testdir.makepyfile( + pytester.makepyfile( """\ import warnings import pytest @@ -217,13 +217,13 @@ def test(): warnings.warn(UserWarning(1, 'foo')) """ ) - result = testdir.runpytest("-W", "always") + result = pytester.runpytest("-W", "always") result.stdout.fnmatch_lines(["*= 1 passed, 1 warning in *"]) -def test_filterwarnings_mark_registration(testdir): +def test_filterwarnings_mark_registration(pytester: Pytester) -> None: """Ensure filterwarnings mark is registered""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -232,19 +232,19 @@ def test_func(): pass """ ) - result = testdir.runpytest("--strict-markers") + result = pytester.runpytest("--strict-markers") assert result.ret == 0 @pytest.mark.filterwarnings("always") -def test_warning_captured_hook(testdir): - testdir.makeconftest( +def test_warning_captured_hook(pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_configure(config): config.issue_config_time_warning(UserWarning("config warning"), stacklevel=2) """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest, warnings @@ -268,7 +268,7 @@ class WarningCollector: 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 = pytester.runpytest(plugins=[WarningCollector()]) result.stdout.fnmatch_lines(["*1 passed*"]) expected = [ @@ -298,9 +298,9 @@ def pytest_warning_recorded(self, warning_message, when, nodeid, location): @pytest.mark.filterwarnings("always") -def test_collection_warnings(testdir): +def test_collection_warnings(pytester: Pytester) -> None: """Check that we also capture warnings issued during test collection (#3251).""" - testdir.makepyfile( + pytester.makepyfile( """ import warnings @@ -310,7 +310,7 @@ def test_foo(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, @@ -322,9 +322,9 @@ def test_foo(): @pytest.mark.filterwarnings("always") -def test_mark_regex_escape(testdir): +def test_mark_regex_escape(pytester: Pytester) -> None: """@pytest.mark.filterwarnings should not try to escape regex characters (#3936)""" - testdir.makepyfile( + pytester.makepyfile( r""" import pytest, warnings @@ -333,15 +333,17 @@ def test_foo(): warnings.warn(UserWarning("some (warning)")) """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() @pytest.mark.filterwarnings("default") @pytest.mark.parametrize("ignore_pytest_warnings", ["no", "ini", "cmdline"]) -def test_hide_pytest_internal_warnings(testdir, ignore_pytest_warnings): +def test_hide_pytest_internal_warnings( + pytester: Pytester, ignore_pytest_warnings +) -> None: """Make sure we can ignore internal pytest warnings using a warnings filter.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest import warnings @@ -353,7 +355,7 @@ def test_bar(): """ ) if ignore_pytest_warnings == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] filterwarnings = ignore::pytest.PytestWarning @@ -364,7 +366,7 @@ def test_bar(): if ignore_pytest_warnings == "cmdline" else [] ) - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) if ignore_pytest_warnings != "no": assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() else: @@ -378,15 +380,17 @@ def test_bar(): @pytest.mark.parametrize("ignore_on_cmdline", [True, False]) -def test_option_precedence_cmdline_over_ini(testdir, ignore_on_cmdline): +def test_option_precedence_cmdline_over_ini( + pytester: Pytester, ignore_on_cmdline +) -> None: """Filters defined in the command-line should take precedence over filters in ini files (#3946).""" - testdir.makeini( + pytester.makeini( """ [pytest] filterwarnings = error """ ) - testdir.makepyfile( + pytester.makepyfile( """ import warnings def test(): @@ -394,22 +398,22 @@ def test(): """ ) args = ["-W", "ignore"] if ignore_on_cmdline else [] - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) if ignore_on_cmdline: result.stdout.fnmatch_lines(["* 1 passed in*"]) else: result.stdout.fnmatch_lines(["* 1 failed in*"]) -def test_option_precedence_mark(testdir): +def test_option_precedence_mark(pytester: Pytester) -> None: """Filters defined by marks should always take precedence (#3946).""" - testdir.makeini( + pytester.makeini( """ [pytest] filterwarnings = ignore """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest, warnings @pytest.mark.filterwarnings('error') @@ -417,7 +421,7 @@ def test(): warnings.warn(UserWarning('hello')) """ ) - result = testdir.runpytest("-W", "ignore") + result = pytester.runpytest("-W", "ignore") result.stdout.fnmatch_lines(["* 1 failed in*"]) @@ -427,8 +431,8 @@ class TestDeprecationWarningsByDefault: from pytest's own test suite """ - def create_file(self, testdir, mark=""): - testdir.makepyfile( + def create_file(self, pytester: Pytester, mark="") -> None: + pytester.makepyfile( """ import pytest, warnings @@ -443,18 +447,18 @@ def test_foo(): ) @pytest.mark.parametrize("customize_filters", [True, False]) - def test_shown_by_default(self, testdir, customize_filters): + def test_shown_by_default(self, pytester: Pytester, customize_filters) -> None: """Show deprecation warnings by default, even if user has customized the warnings filters (#4013).""" - self.create_file(testdir) + self.create_file(pytester) if customize_filters: - testdir.makeini( + pytester.makeini( """ [pytest] filterwarnings = once::UserWarning """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines( [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, @@ -464,9 +468,9 @@ def test_shown_by_default(self, testdir, customize_filters): ] ) - def test_hidden_by_ini(self, testdir): - self.create_file(testdir) - testdir.makeini( + def test_hidden_by_ini(self, pytester: Pytester) -> None: + self.create_file(pytester) + pytester.makeini( """ [pytest] filterwarnings = @@ -474,18 +478,18 @@ def test_hidden_by_ini(self, testdir): ignore::PendingDeprecationWarning """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() - def test_hidden_by_mark(self, testdir): + def test_hidden_by_mark(self, pytester: Pytester) -> None: """Should hide the deprecation warning from the function, but the warning during collection should be displayed normally. """ self.create_file( - testdir, + pytester, mark='@pytest.mark.filterwarnings("ignore::PendingDeprecationWarning")', ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines( [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, @@ -494,9 +498,9 @@ def test_hidden_by_mark(self, testdir): ] ) - def test_hidden_by_cmdline(self, testdir): - self.create_file(testdir) - result = testdir.runpytest_subprocess( + def test_hidden_by_cmdline(self, pytester: Pytester) -> None: + self.create_file(pytester) + result = pytester.runpytest_subprocess( "-W", "ignore::DeprecationWarning", "-W", @@ -504,10 +508,10 @@ def test_hidden_by_cmdline(self, testdir): ) assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() - def test_hidden_by_system(self, testdir, monkeypatch): - self.create_file(testdir) + def test_hidden_by_system(self, pytester: Pytester, monkeypatch) -> None: + self.create_file(pytester) monkeypatch.setenv("PYTHONWARNINGS", "once::UserWarning") - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() @@ -515,13 +519,13 @@ def test_hidden_by_system(self, testdir, monkeypatch): @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): +def test_deprecation_warning_as_error(pytester: Pytester, change_default) -> None: """This ensures that PytestDeprecationWarnings raised by pytest are turned into errors. This test should be enabled as part of each major release, and skipped again afterwards to ensure our deprecations are turning into warnings as expected. """ - testdir.makepyfile( + pytester.makepyfile( """ import warnings, pytest def test(): @@ -529,7 +533,7 @@ def test(): """ ) if change_default == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] filterwarnings = @@ -542,7 +546,7 @@ def test(): if change_default == "cmdline" else () ) - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) if change_default is None: result.stdout.fnmatch_lines(["* 1 failed in *"]) else: @@ -552,23 +556,23 @@ def test(): class TestAssertionWarnings: @staticmethod - def assert_result_warns(result, msg): + def assert_result_warns(result, msg) -> None: result.stdout.fnmatch_lines(["*PytestAssertRewriteWarning: %s*" % msg]) - def test_tuple_warning(self, testdir): - testdir.makepyfile( + def test_tuple_warning(self, pytester: Pytester) -> None: + pytester.makepyfile( """\ def test_foo(): assert (1,2) """ ) - result = testdir.runpytest() + result = pytester.runpytest() self.assert_result_warns( result, "assertion is always true, perhaps remove parentheses?" ) -def test_warnings_checker_twice(): +def test_warnings_checker_twice() -> None: """Issue #4617""" expectation = pytest.warns(UserWarning) with expectation: @@ -579,9 +583,9 @@ def test_warnings_checker_twice(): @pytest.mark.filterwarnings("ignore::pytest.PytestExperimentalApiWarning") @pytest.mark.filterwarnings("always") -def test_group_warnings_by_message(testdir): - testdir.copy_example("warnings/test_group_warnings_by_message.py") - result = testdir.runpytest() +def test_group_warnings_by_message(pytester: Pytester) -> None: + pytester.copy_example("warnings/test_group_warnings_by_message.py") + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, @@ -611,10 +615,10 @@ def test_group_warnings_by_message(testdir): @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") - testdir.syspathinsert() - result = testdir.runpytest() +def test_group_warnings_by_message_summary(pytester: Pytester) -> None: + pytester.copy_example("warnings/test_group_warnings_by_message_summary") + pytester.syspathinsert() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, @@ -634,9 +638,9 @@ def test_group_warnings_by_message_summary(testdir): ) -def test_pytest_configure_warning(testdir, recwarn): +def test_pytest_configure_warning(pytester: Pytester, recwarn) -> None: """Issue 5115.""" - testdir.makeconftest( + pytester.makeconftest( """ def pytest_configure(): import warnings @@ -645,7 +649,7 @@ def pytest_configure(): """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 5 assert "INTERNALERROR" not in result.stderr.str() warning = recwarn.pop() @@ -654,7 +658,7 @@ def pytest_configure(): class TestStackLevel: @pytest.fixture - def capwarn(self, testdir): + def capwarn(self, pytester: Pytester): class CapturedWarnings: captured: List[ Tuple[warnings.WarningMessage, Optional[Tuple[str, int, str]]] @@ -664,16 +668,16 @@ class CapturedWarnings: def pytest_warning_recorded(cls, warning_message, when, nodeid, location): cls.captured.append((warning_message, location)) - testdir.plugins = [CapturedWarnings()] + pytester.plugins = [CapturedWarnings()] return CapturedWarnings - def test_issue4445_rewrite(self, testdir, capwarn): + def test_issue4445_rewrite(self, pytester: Pytester, capwarn) -> None: """#4445: Make sure the warning points to a reasonable location See origin of _issue_warning_captured at: _pytest.assertion.rewrite.py:241 """ - testdir.makepyfile(some_mod="") - conftest = testdir.makeconftest( + pytester.makepyfile(some_mod="") + conftest = pytester.makeconftest( """ import some_mod import pytest @@ -681,7 +685,7 @@ def test_issue4445_rewrite(self, testdir, capwarn): pytest.register_assert_rewrite("some_mod") """ ) - testdir.parseconfig() + pytester.parseconfig() # with stacklevel=5 the warning originates from register_assert_rewrite # function in the created conftest.py @@ -694,16 +698,16 @@ def test_issue4445_rewrite(self, testdir, capwarn): assert func == "" # the above conftest.py assert lineno == 4 - def test_issue4445_preparse(self, testdir, capwarn): + def test_issue4445_preparse(self, pytester: Pytester, capwarn) -> None: """#4445: Make sure the warning points to a reasonable location See origin of _issue_warning_captured at: _pytest.config.__init__.py:910 """ - testdir.makeconftest( + pytester.makeconftest( """ import nothing """ ) - testdir.parseconfig("--help") + pytester.parseconfig("--help") # with stacklevel=2 the warning should originate from config._preparse and is # thrown by an errorneous conftest.py @@ -716,29 +720,29 @@ def test_issue4445_preparse(self, testdir, capwarn): assert func == "_preparse" @pytest.mark.filterwarnings("default") - def test_conftest_warning_captured(self, testdir: Testdir) -> None: + def test_conftest_warning_captured(self, pytester: Pytester) -> None: """Warnings raised during importing of conftest.py files is captured (#2891).""" - testdir.makeconftest( + pytester.makeconftest( """ import warnings warnings.warn(UserWarning("my custom warning")) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( ["conftest.py:2", "*UserWarning: my custom warning*"] ) - def test_issue4445_import_plugin(self, testdir, capwarn): + def test_issue4445_import_plugin(self, pytester: Pytester, capwarn) -> None: """#4445: Make sure the warning points to a reasonable location""" - testdir.makepyfile( + pytester.makepyfile( some_plugin=""" import pytest pytest.skip("thing", allow_module_level=True) """ ) - testdir.syspathinsert() - testdir.parseconfig("-p", "some_plugin") + pytester.syspathinsert() + pytester.parseconfig("-p", "some_plugin") # with stacklevel=2 the warning should originate from # config.PytestPluginManager.import_plugin is thrown by a skipped plugin @@ -751,11 +755,11 @@ def test_issue4445_import_plugin(self, testdir, capwarn): assert f"config{os.sep}__init__.py" in file assert func == "_warn_about_skipped_plugins" - def test_issue4445_issue5928_mark_generator(self, testdir): + def test_issue4445_issue5928_mark_generator(self, pytester: Pytester) -> None: """#4445 and #5928: Make sure the warning from an unknown mark points to the test file where this mark is used. """ - testfile = testdir.makepyfile( + testfile = pytester.makepyfile( """ import pytest @@ -764,7 +768,7 @@ def test_it(): pass """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() # with stacklevel=2 the warning should originate from the above created test file result.stdout.fnmatch_lines_random( [ From 4ed9a385192c252cc4d64a0775fa744b8fb95063 Mon Sep 17 00:00:00 2001 From: antonblr Date: Wed, 9 Dec 2020 21:47:58 -0800 Subject: [PATCH 0322/2846] tests: Migrate testing/python to pytester fixture --- src/_pytest/pytester.py | 16 +- testing/python/collect.py | 584 +++++----- testing/python/fixtures.py | 1254 ++++++++++++---------- testing/python/integration.py | 103 +- testing/python/metafunc.py | 362 ++++--- testing/python/raises.py | 19 +- testing/python/show_fixtures_per_test.py | 45 +- 7 files changed, 1251 insertions(+), 1132 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 6833eb02149..0d1f8f278f9 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1190,7 +1190,9 @@ def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config: config._do_configure() return config - def getitem(self, source: str, funcname: str = "test_func") -> Item: + def getitem( + self, source: Union[str, "os.PathLike[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 @@ -1210,7 +1212,7 @@ def getitem(self, source: str, funcname: str = "test_func") -> Item: funcname, source, items ) - def getitems(self, source: str) -> List[Item]: + def getitems(self, source: Union[str, "os.PathLike[str]"]) -> List[Item]: """Return all test items collected from the module. Writes the source to a Python file and runs pytest's collection on @@ -1220,7 +1222,11 @@ def getitems(self, source: str) -> List[Item]: return self.genitems([modcol]) def getmodulecol( - self, source: Union[str, Path], configargs=(), *, withinit: bool = False + self, + source: Union[str, "os.PathLike[str]"], + configargs=(), + *, + withinit: bool = False, ): """Return the module collection node for ``source``. @@ -1238,7 +1244,9 @@ def getmodulecol( Whether to also write an ``__init__.py`` file to the same directory to ensure it is a package. """ - if isinstance(source, Path): + # TODO: Remove type ignore in next mypy release (> 0.790). + # https://github.com/python/typeshed/pull/4582 + if isinstance(source, os.PathLike): # type: ignore[misc] path = self.path.joinpath(source) assert not withinit, "not supported for paths" else: diff --git a/testing/python/collect.py b/testing/python/collect.py index 4d5f4c6895f..c52fb017d0c 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,3 +1,4 @@ +import os import sys import textwrap from typing import Any @@ -6,42 +7,50 @@ import _pytest._code import pytest from _pytest.config import ExitCode +from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Collector -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester +from _pytest.python import Class +from _pytest.python import Instance class TestModule: - def test_failing_import(self, testdir): - modcol = testdir.getmodulecol("import alksdjalskdjalkjals") + def test_failing_import(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol("import alksdjalskdjalkjals") pytest.raises(Collector.CollectError, modcol.collect) - def test_import_duplicate(self, testdir): - a = testdir.mkdir("a") - b = testdir.mkdir("b") - p = a.ensure("test_whatever.py") - p.pyimport() - del sys.modules["test_whatever"] - b.ensure("test_whatever.py") - result = testdir.runpytest() + def test_import_duplicate(self, pytester: Pytester) -> None: + a = pytester.mkdir("a") + b = pytester.mkdir("b") + p1 = a.joinpath("test_whatever.py") + p1.touch() + p2 = b.joinpath("test_whatever.py") + p2.touch() + # ensure we don't have it imported already + sys.modules.pop(p1.stem, None) + + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*import*mismatch*", "*imported*test_whatever*", - "*%s*" % a.join("test_whatever.py"), + "*%s*" % p1, "*not the same*", - "*%s*" % b.join("test_whatever.py"), + "*%s*" % p2, "*HINT*", ] ) - def test_import_prepend_append(self, testdir, monkeypatch): - root1 = testdir.mkdir("root1") - root2 = testdir.mkdir("root2") - root1.ensure("x456.py") - root2.ensure("x456.py") - p = root2.join("test_x456.py") + def test_import_prepend_append( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: + root1 = pytester.mkdir("root1") + root2 = pytester.mkdir("root2") + root1.joinpath("x456.py").touch() + root2.joinpath("x456.py").touch() + p = root2.joinpath("test_x456.py") monkeypatch.syspath_prepend(str(root1)) - p.write( + p.write_text( textwrap.dedent( """\ import x456 @@ -52,25 +61,26 @@ def test(): ) ) ) - with root2.as_cwd(): - reprec = testdir.inline_run("--import-mode=append") + with monkeypatch.context() as mp: + mp.chdir(root2) + reprec = pytester.inline_run("--import-mode=append") reprec.assertoutcome(passed=0, failed=1) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_syntax_error_in_module(self, testdir): - modcol = testdir.getmodulecol("this is a syntax error") + def test_syntax_error_in_module(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol("this is a syntax error") pytest.raises(modcol.CollectError, modcol.collect) pytest.raises(modcol.CollectError, modcol.collect) - def test_module_considers_pluginmanager_at_import(self, testdir): - modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") + def test_module_considers_pluginmanager_at_import(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol("pytest_plugins='xasdlkj',") pytest.raises(ImportError, lambda: modcol.obj) - def test_invalid_test_module_name(self, testdir): - a = testdir.mkdir("a") - a.ensure("test_one.part1.py") - result = testdir.runpytest() + def test_invalid_test_module_name(self, pytester: Pytester) -> None: + a = pytester.mkdir("a") + a.joinpath("test_one.part1.py").touch() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "ImportError while importing test module*test_one.part1*", @@ -79,24 +89,26 @@ def test_invalid_test_module_name(self, testdir): ) @pytest.mark.parametrize("verbose", [0, 1, 2]) - def test_show_traceback_import_error(self, testdir, verbose): + def test_show_traceback_import_error( + self, pytester: Pytester, verbose: int + ) -> None: """Import errors when collecting modules should display the traceback (#1976). With low verbosity we omit pytest and internal modules, otherwise show all traceback entries. """ - testdir.makepyfile( + pytester.makepyfile( foo_traceback_import_error=""" from bar_traceback_import_error import NOT_AVAILABLE """, bar_traceback_import_error="", ) - testdir.makepyfile( + pytester.makepyfile( """ import foo_traceback_import_error """ ) args = ("-v",) * verbose - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) result.stdout.fnmatch_lines( [ "ImportError while importing test module*", @@ -113,12 +125,12 @@ def test_show_traceback_import_error(self, testdir, verbose): else: assert "_pytest" not in stdout - def test_show_traceback_import_error_unicode(self, testdir): + def test_show_traceback_import_error_unicode(self, pytester: Pytester) -> None: """Check test modules collected which raise ImportError with unicode messages are handled properly (#2336). """ - testdir.makepyfile("raise ImportError('Something bad happened ☺')") - result = testdir.runpytest() + pytester.makepyfile("raise ImportError('Something bad happened ☺')") + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "ImportError while importing test module*", @@ -130,15 +142,15 @@ def test_show_traceback_import_error_unicode(self, testdir): class TestClass: - def test_class_with_init_warning(self, testdir): - testdir.makepyfile( + def test_class_with_init_warning(self, pytester: Pytester) -> None: + pytester.makepyfile( """ class TestClass1(object): def __init__(self): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*cannot collect test class 'TestClass1' because it has " @@ -146,15 +158,15 @@ def __init__(self): ] ) - def test_class_with_new_warning(self, testdir): - testdir.makepyfile( + def test_class_with_new_warning(self, pytester: Pytester) -> None: + pytester.makepyfile( """ class TestClass1(object): def __new__(self): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*cannot collect test class 'TestClass1' because it has " @@ -162,19 +174,19 @@ def __new__(self): ] ) - def test_class_subclassobject(self, testdir): - testdir.getmodulecol( + def test_class_subclassobject(self, pytester: Pytester) -> None: + pytester.getmodulecol( """ class test(object): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*collected 0*"]) - def test_static_method(self, testdir): + def test_static_method(self, pytester: Pytester) -> None: """Support for collecting staticmethod tests (#2528, #2699)""" - testdir.getmodulecol( + pytester.getmodulecol( """ import pytest class Test(object): @@ -191,11 +203,11 @@ def test_fix(fix): assert fix == 1 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*collected 2 items*", "*2 passed in*"]) - def test_setup_teardown_class_as_classmethod(self, testdir): - testdir.makepyfile( + def test_setup_teardown_class_as_classmethod(self, pytester: Pytester) -> None: + pytester.makepyfile( test_mod1=""" class TestClassMethod(object): @classmethod @@ -208,11 +220,11 @@ def teardown_class(cls): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - def test_issue1035_obj_has_getattr(self, testdir): - modcol = testdir.getmodulecol( + def test_issue1035_obj_has_getattr(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol( """ class Chameleon(object): def __getattr__(self, name): @@ -223,22 +235,22 @@ def __getattr__(self, name): colitems = modcol.collect() assert len(colitems) == 0 - def test_issue1579_namedtuple(self, testdir): - testdir.makepyfile( + def test_issue1579_namedtuple(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import collections TestCase = collections.namedtuple('TestCase', ['a']) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( "*cannot collect test class 'TestCase' " "because it has a __new__ constructor*" ) - def test_issue2234_property(self, testdir): - testdir.makepyfile( + def test_issue2234_property(self, pytester: Pytester) -> None: + pytester.makepyfile( """ class TestCase(object): @property @@ -246,20 +258,20 @@ def prop(self): raise NotImplementedError() """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED class TestFunction: - def test_getmodulecollector(self, testdir): - item = testdir.getitem("def test_func(): pass") + def test_getmodulecollector(self, pytester: Pytester) -> None: + item = pytester.getitem("def test_func(): pass") modcol = item.getparent(pytest.Module) assert isinstance(modcol, pytest.Module) assert hasattr(modcol.obj, "test_func") @pytest.mark.filterwarnings("default") - def test_function_as_object_instance_ignored(self, testdir): - testdir.makepyfile( + def test_function_as_object_instance_ignored(self, pytester: Pytester) -> None: + pytester.makepyfile( """ class A(object): def __call__(self, tmpdir): @@ -268,7 +280,7 @@ def __call__(self, tmpdir): test_a = A() """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "collected 0 items", @@ -278,37 +290,37 @@ def __call__(self, tmpdir): ) @staticmethod - def make_function(testdir, **kwargs): + def make_function(pytester: Pytester, **kwargs: Any) -> Any: from _pytest.fixtures import FixtureManager - config = testdir.parseconfigure() - session = testdir.Session.from_config(config) + config = pytester.parseconfigure() + session = pytester.Session.from_config(config) session._fixturemanager = FixtureManager(session) return pytest.Function.from_parent(parent=session, **kwargs) - def test_function_equality(self, testdir): + def test_function_equality(self, pytester: Pytester) -> None: def func1(): pass def func2(): pass - f1 = self.make_function(testdir, name="name", callobj=func1) + f1 = self.make_function(pytester, name="name", callobj=func1) assert f1 == f1 f2 = self.make_function( - testdir, name="name", callobj=func2, originalname="foobar" + pytester, name="name", callobj=func2, originalname="foobar" ) assert f1 != f2 - def test_repr_produces_actual_test_id(self, testdir): + def test_repr_produces_actual_test_id(self, pytester: Pytester) -> None: f = self.make_function( - testdir, name=r"test[\xe5]", callobj=self.test_repr_produces_actual_test_id + pytester, name=r"test[\xe5]", callobj=self.test_repr_produces_actual_test_id ) assert repr(f) == r"" - def test_issue197_parametrize_emptyset(self, testdir): - testdir.makepyfile( + def test_issue197_parametrize_emptyset(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.parametrize('arg', []) @@ -316,11 +328,11 @@ def test_function(arg): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(skipped=1) - def test_single_tuple_unwraps_values(self, testdir): - testdir.makepyfile( + def test_single_tuple_unwraps_values(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.parametrize(('arg',), [(1,)]) @@ -328,11 +340,11 @@ def test_function(arg): assert arg == 1 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_issue213_parametrize_value_no_equal(self, testdir): - testdir.makepyfile( + def test_issue213_parametrize_value_no_equal(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest class A(object): @@ -343,12 +355,12 @@ def test_function(arg): assert arg.__class__.__name__ == "A" """ ) - reprec = testdir.inline_run("--fulltrace") + reprec = pytester.inline_run("--fulltrace") reprec.assertoutcome(passed=1) - def test_parametrize_with_non_hashable_values(self, testdir): + def test_parametrize_with_non_hashable_values(self, pytester: Pytester) -> None: """Test parametrization with non-hashable values.""" - testdir.makepyfile( + pytester.makepyfile( """ archival_mapping = { '1.0': {'tag': '1.0'}, @@ -363,12 +375,14 @@ def test_archival_to_version(key, value): assert value == archival_mapping[key] """ ) - rec = testdir.inline_run() + rec = pytester.inline_run() rec.assertoutcome(passed=2) - def test_parametrize_with_non_hashable_values_indirect(self, testdir): + def test_parametrize_with_non_hashable_values_indirect( + self, pytester: Pytester + ) -> None: """Test parametrization with non-hashable values with indirect parametrization.""" - testdir.makepyfile( + pytester.makepyfile( """ archival_mapping = { '1.0': {'tag': '1.0'}, @@ -392,12 +406,12 @@ def test_archival_to_version(key, value): assert value == archival_mapping[key] """ ) - rec = testdir.inline_run() + rec = pytester.inline_run() rec.assertoutcome(passed=2) - def test_parametrize_overrides_fixture(self, testdir): + def test_parametrize_overrides_fixture(self, pytester: Pytester) -> None: """Test parametrization when parameter overrides existing fixture with same name.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -421,12 +435,14 @@ def test_overridden_via_multiparam(other, value): assert value == 'overridden' """ ) - rec = testdir.inline_run() + rec = pytester.inline_run() rec.assertoutcome(passed=3) - def test_parametrize_overrides_parametrized_fixture(self, testdir): + def test_parametrize_overrides_parametrized_fixture( + self, pytester: Pytester + ) -> None: """Test parametrization when parameter overrides existing parametrized fixture with same name.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -440,12 +456,14 @@ def test_overridden_via_param(value): assert value == 'overridden' """ ) - rec = testdir.inline_run() + rec = pytester.inline_run() rec.assertoutcome(passed=1) - def test_parametrize_overrides_indirect_dependency_fixture(self, testdir): + def test_parametrize_overrides_indirect_dependency_fixture( + self, pytester: Pytester + ) -> None: """Test parametrization when parameter overrides a fixture that a test indirectly depends on""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -471,11 +489,11 @@ def test_it(fix1): assert not fix3_instantiated """ ) - rec = testdir.inline_run() + rec = pytester.inline_run() rec.assertoutcome(passed=1) - def test_parametrize_with_mark(self, testdir): - items = testdir.getitems( + def test_parametrize_with_mark(self, pytester: Pytester) -> None: + items = pytester.getitems( """ import pytest @pytest.mark.foo @@ -495,8 +513,8 @@ def test_function(arg): ) assert "foo" in keywords[1] and "bar" in keywords[1] and "baz" in keywords[1] - def test_parametrize_with_empty_string_arguments(self, testdir): - items = testdir.getitems( + def test_parametrize_with_empty_string_arguments(self, pytester: Pytester) -> None: + items = pytester.getitems( """\ import pytest @@ -508,8 +526,8 @@ def test(v, w): ... names = {item.name for item in items} assert names == {"test[-]", "test[ -]", "test[- ]", "test[ - ]"} - def test_function_equality_with_callspec(self, testdir): - items = testdir.getitems( + def test_function_equality_with_callspec(self, pytester: Pytester) -> None: + items = pytester.getitems( """ import pytest @pytest.mark.parametrize('arg', [1,2]) @@ -520,8 +538,8 @@ def test_function(arg): assert items[0] != items[1] assert not (items[0] == items[1]) - def test_pyfunc_call(self, testdir): - item = testdir.getitem("def test_func(): raise ValueError") + def test_pyfunc_call(self, pytester: Pytester) -> None: + item = pytester.getitem("def test_func(): raise ValueError") config = item.config class MyPlugin1: @@ -537,8 +555,8 @@ def pytest_pyfunc_call(self): config.hook.pytest_runtest_setup(item=item) config.hook.pytest_pyfunc_call(pyfuncitem=item) - def test_multiple_parametrize(self, testdir): - modcol = testdir.getmodulecol( + def test_multiple_parametrize(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol( """ import pytest @pytest.mark.parametrize('x', [0, 1]) @@ -553,8 +571,8 @@ def test1(x, y): assert colitems[2].name == "test1[3-0]" assert colitems[3].name == "test1[3-1]" - def test_issue751_multiple_parametrize_with_ids(self, testdir): - modcol = testdir.getmodulecol( + def test_issue751_multiple_parametrize_with_ids(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol( """ import pytest @pytest.mark.parametrize('x', [0], ids=['c']) @@ -572,8 +590,8 @@ def test2(self, x, y): assert colitems[2].name == "test2[a-c]" assert colitems[3].name == "test2[b-c]" - def test_parametrize_skipif(self, testdir): - testdir.makepyfile( + def test_parametrize_skipif(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -584,11 +602,11 @@ def test_skip_if(x): assert x < 2 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 2 passed, 1 skipped in *"]) - def test_parametrize_skip(self, testdir): - testdir.makepyfile( + def test_parametrize_skip(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -599,11 +617,11 @@ def test_skip(x): assert x < 2 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 2 passed, 1 skipped in *"]) - def test_parametrize_skipif_no_skip(self, testdir): - testdir.makepyfile( + def test_parametrize_skipif_no_skip(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -614,11 +632,11 @@ def test_skipif_no_skip(x): assert x < 2 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 1 failed, 2 passed in *"]) - def test_parametrize_xfail(self, testdir): - testdir.makepyfile( + def test_parametrize_xfail(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -629,11 +647,11 @@ def test_xfail(x): assert x < 2 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 2 passed, 1 xfailed in *"]) - def test_parametrize_passed(self, testdir): - testdir.makepyfile( + def test_parametrize_passed(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -644,11 +662,11 @@ def test_xfail(x): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 2 passed, 1 xpassed in *"]) - def test_parametrize_xfail_passed(self, testdir): - testdir.makepyfile( + def test_parametrize_xfail_passed(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -659,11 +677,11 @@ def test_passed(x): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 3 passed in *"]) - def test_function_originalname(self, testdir: Testdir) -> None: - items = testdir.getitems( + def test_function_originalname(self, pytester: Pytester) -> None: + items = pytester.getitems( """ import pytest @@ -685,14 +703,14 @@ def test_no_param(): "test_no_param", ] - def test_function_with_square_brackets(self, testdir: Testdir) -> None: + def test_function_with_square_brackets(self, pytester: Pytester) -> None: """Check that functions with square brackets don't cause trouble.""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ locals()["test_foo[name]"] = lambda: None """ ) - result = testdir.runpytest("-v", str(p1)) + result = pytester.runpytest("-v", str(p1)) result.stdout.fnmatch_lines( [ "test_function_with_square_brackets.py::test_foo[[]name[]] PASSED *", @@ -702,23 +720,23 @@ def test_function_with_square_brackets(self, testdir: Testdir) -> None: class TestSorting: - def test_check_equality(self, 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 @@ -730,8 +748,8 @@ def test_fail(): assert 0 assert [1, 2, 3] != fn # type: ignore[comparison-overlap] assert modcol != fn - def test_allow_sane_sorting_for_decorators(self, testdir): - modcol = testdir.getmodulecol( + def test_allow_sane_sorting_for_decorators(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol( """ def dec(f): g = lambda: f(2) @@ -754,8 +772,8 @@ def test_a(y): class TestConftestCustomization: - def test_pytest_pycollect_module(self, testdir): - testdir.makeconftest( + def test_pytest_pycollect_module(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest class MyModule(pytest.Module): @@ -765,14 +783,15 @@ def pytest_pycollect_makemodule(path, parent): return MyModule.from_parent(fspath=path, parent=parent) """ ) - testdir.makepyfile("def test_some(): pass") - testdir.makepyfile(test_xyz="def test_func(): pass") - result = testdir.runpytest("--collect-only") + pytester.makepyfile("def test_some(): pass") + pytester.makepyfile(test_xyz="def test_func(): pass") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["* None: + b = pytester.path.joinpath("a", "b") + b.mkdir(parents=True) + b.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -784,7 +803,7 @@ def pytest_pycollect_makemodule(): """ ) ) - b.join("test_module.py").write( + b.joinpath("test_module.py").write_text( textwrap.dedent( """\ def test_hello(): @@ -792,12 +811,13 @@ def test_hello(): """ ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_customized_pymakeitem(self, testdir): - b = testdir.mkdir("a").mkdir("b") - b.join("conftest.py").write( + def test_customized_pymakeitem(self, pytester: Pytester) -> None: + b = pytester.path.joinpath("a", "b") + b.mkdir(parents=True) + b.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -812,7 +832,7 @@ def pytest_pycollect_makeitem(): """ ) ) - b.join("test_module.py").write( + b.joinpath("test_module.py").write_text( textwrap.dedent( """\ import pytest @@ -825,11 +845,11 @@ def test_hello(obj): """ ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_pytest_pycollect_makeitem(self, testdir): - testdir.makeconftest( + def test_pytest_pycollect_makeitem(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest class MyFunction(pytest.Function): @@ -839,16 +859,16 @@ def pytest_pycollect_makeitem(collector, name, obj): return MyFunction.from_parent(name=name, parent=collector) """ ) - testdir.makepyfile("def some(): pass") - result = testdir.runpytest("--collect-only") + pytester.makepyfile("def some(): pass") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["*MyFunction*some*"]) - def test_issue2369_collect_module_fileext(self, testdir): + def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None: """Ensure we can collect files with weird file extensions as Python modules (#2369)""" # We'll implement a little finder and loader to import files containing # Python source code whose file extension is ".narf". - testdir.makeconftest( + pytester.makeconftest( """ import sys, os, imp from _pytest.python import Module @@ -866,17 +886,17 @@ def pytest_collect_file(path, parent): if path.ext == ".narf": return Module.from_parent(fspath=path, parent=parent)""" ) - testdir.makefile( + pytester.makefile( ".narf", """\ def test_something(): assert 1 + 1 == 2""", ) # Use runpytest_subprocess, since we're futzing with sys.meta_path. - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 passed*"]) - def test_early_ignored_attributes(self, testdir: Testdir) -> None: + def test_early_ignored_attributes(self, pytester: Pytester) -> None: """Builtin attributes should be ignored early on, even if configuration would otherwise allow them. @@ -884,14 +904,14 @@ def test_early_ignored_attributes(self, testdir: Testdir) -> None: although it tests PytestCollectionWarning is not raised, while it would have been raised otherwise. """ - testdir.makeini( + pytester.makeini( """ [pytest] python_classes=* python_functions=* """ ) - testdir.makepyfile( + pytester.makepyfile( """ class TestEmpty: pass @@ -900,15 +920,15 @@ def test_real(): pass """ ) - items, rec = testdir.inline_genitems() + items, rec = pytester.inline_genitems() assert rec.ret == 0 assert len(items) == 1 -def test_setup_only_available_in_subdir(testdir): - sub1 = testdir.mkpydir("sub1") - sub2 = testdir.mkpydir("sub2") - sub1.join("conftest.py").write( +def test_setup_only_available_in_subdir(pytester: Pytester) -> None: + sub1 = pytester.mkpydir("sub1") + sub2 = pytester.mkpydir("sub2") + sub1.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -921,7 +941,7 @@ def pytest_runtest_teardown(item): """ ) ) - sub2.join("conftest.py").write( + sub2.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -934,14 +954,14 @@ def pytest_runtest_teardown(item): """ ) ) - sub1.join("test_in_sub1.py").write("def test_1(): pass") - sub2.join("test_in_sub2.py").write("def test_2(): pass") - result = testdir.runpytest("-v", "-s") + sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass") + sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass") + result = pytester.runpytest("-v", "-s") result.assert_outcomes(passed=2) -def test_modulecol_roundtrip(testdir): - modcol = testdir.getmodulecol("pass", withinit=False) +def test_modulecol_roundtrip(pytester: Pytester) -> None: + modcol = pytester.getmodulecol("pass", withinit=False) trail = modcol.nodeid newcol = modcol.session.perform_collect([trail], genitems=0)[0] assert modcol.name == newcol.name @@ -956,8 +976,8 @@ def test_skip_simple(self): assert excinfo.traceback[-2].frame.code.name == "test_skip_simple" assert not excinfo.traceback[-2].ishidden() - def test_traceback_argsetup(self, testdir): - testdir.makeconftest( + def test_traceback_argsetup(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @@ -966,8 +986,8 @@ def hello(request): raise ValueError("xyz") """ ) - 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 out = result.stdout.str() assert "xyz" in out @@ -975,14 +995,14 @@ def hello(request): numentries = out.count("_ _ _") # separator for traceback entries assert numentries == 0 - result = testdir.runpytest("--fulltrace", p) + result = pytester.runpytest("--fulltrace", p) out = result.stdout.str() assert "conftest.py:5: ValueError" in out numentries = out.count("_ _ _ _") # separator for traceback entries assert numentries > 3 - def test_traceback_error_during_import(self, testdir): - testdir.makepyfile( + def test_traceback_error_during_import(self, pytester: Pytester) -> None: + pytester.makepyfile( """ x = 1 x = 2 @@ -990,21 +1010,23 @@ def test_traceback_error_during_import(self, testdir): asd """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret != 0 out = result.stdout.str() assert "x = 1" not in out assert "x = 2" not in out result.stdout.fnmatch_lines([" *asd*", "E*NameError*"]) - result = testdir.runpytest("--fulltrace") + result = pytester.runpytest("--fulltrace") out = result.stdout.str() assert "x = 1" in out assert "x = 2" in out result.stdout.fnmatch_lines([">*asd*", "E*NameError*"]) - def test_traceback_filter_error_during_fixture_collection(self, testdir): + def test_traceback_filter_error_during_fixture_collection( + self, pytester: Pytester + ) -> None: """Integration test for issue #995.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1022,7 +1044,7 @@ def test_failing_fixture(fail_fixture): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret != 0 out = result.stdout.str() assert "INTERNALERROR>" not in out @@ -1039,6 +1061,7 @@ def test_filter_traceback_generated_code(self) -> None: """ from _pytest._code import filter_traceback + tb = None try: ns: Dict[str, Any] = {} exec("def foo(): raise ValueError", ns) @@ -1051,7 +1074,7 @@ def test_filter_traceback_generated_code(self) -> None: assert isinstance(traceback[-1].path, str) assert not filter_traceback(traceback[-1]) - def test_filter_traceback_path_no_longer_valid(self, testdir) -> None: + def test_filter_traceback_path_no_longer_valid(self, pytester: Pytester) -> None: """Test that filter_traceback() works with the fact that _pytest._code.code.Code.path attribute might return an str object. @@ -1060,13 +1083,14 @@ def test_filter_traceback_path_no_longer_valid(self, testdir) -> None: """ from _pytest._code import filter_traceback - testdir.syspathinsert() - testdir.makepyfile( + pytester.syspathinsert() + pytester.makepyfile( filter_traceback_entry_as_str=""" def foo(): raise ValueError """ ) + tb = None try: import filter_traceback_entry_as_str @@ -1075,15 +1099,15 @@ def foo(): _, _, tb = sys.exc_info() assert tb is not None - testdir.tmpdir.join("filter_traceback_entry_as_str.py").remove() + pytester.path.joinpath("filter_traceback_entry_as_str.py").unlink() traceback = _pytest._code.Traceback(tb) assert isinstance(traceback[-1].path, str) assert filter_traceback(traceback[-1]) class TestReportInfo: - def test_itemreport_reportinfo(self, testdir): - testdir.makeconftest( + def test_itemreport_reportinfo(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest class MyFunction(pytest.Function): @@ -1094,26 +1118,27 @@ def pytest_pycollect_makeitem(collector, name, obj): return MyFunction.from_parent(name=name, parent=collector) """ ) - item = testdir.getitem("def test_func(): pass") + item = pytester.getitem("def test_func(): pass") item.config.pluginmanager.getplugin("runner") assert item.location == ("ABCDE", 42, "custom") - def test_func_reportinfo(self, testdir): - item = testdir.getitem("def test_func(): pass") + def test_func_reportinfo(self, pytester: Pytester) -> None: + item = pytester.getitem("def test_func(): pass") fspath, lineno, modpath = item.reportinfo() assert fspath == item.fspath assert lineno == 0 assert modpath == "test_func" - def test_class_reportinfo(self, testdir): - modcol = testdir.getmodulecol( + def test_class_reportinfo(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol( """ # lineno 0 class TestClass(object): def test_hello(self): pass """ ) - classcol = testdir.collect_by_name(modcol, "TestClass") + classcol = pytester.collect_by_name(modcol, "TestClass") + assert isinstance(classcol, Class) fspath, lineno, msg = classcol.reportinfo() assert fspath == modcol.fspath assert lineno == 1 @@ -1122,26 +1147,28 @@ def test_hello(self): pass @pytest.mark.filterwarnings( "ignore:usage of Generator.Function is deprecated, please use pytest.Function instead" ) - def test_reportinfo_with_nasty_getattr(self, testdir): + def test_reportinfo_with_nasty_getattr(self, pytester: Pytester) -> None: # https://github.com/pytest-dev/pytest/issues/1204 - modcol = testdir.getmodulecol( + modcol = pytester.getmodulecol( """ # lineno 0 class TestClass(object): def __getattr__(self, name): return "this is not an int" - def test_foo(self): + def intest_foo(self): pass """ ) - classcol = testdir.collect_by_name(modcol, "TestClass") - instance = classcol.collect()[0] + classcol = pytester.collect_by_name(modcol, "TestClass") + assert isinstance(classcol, Class) + instance = list(classcol.collect())[0] + assert isinstance(instance, Instance) fspath, lineno, msg = instance.reportinfo() -def test_customized_python_discovery(testdir): - testdir.makeini( +def test_customized_python_discovery(pytester: Pytester) -> None: + pytester.makeini( """ [pytest] python_files=check_*.py @@ -1149,7 +1176,7 @@ def test_customized_python_discovery(testdir): python_functions=check """ ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ def check_simple(): pass @@ -1158,41 +1185,41 @@ def check_meth(self): pass """ ) - p2 = p.new(basename=p.basename.replace("test", "check")) - p.move(p2) - result = testdir.runpytest("--collect-only", "-s") + p2 = p.with_name(p.name.replace("test", "check")) + p.rename(p2) + result = pytester.runpytest("--collect-only", "-s") result.stdout.fnmatch_lines( ["*check_customized*", "*check_simple*", "*CheckMyApp*", "*check_meth*"] ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) -def test_customized_python_discovery_functions(testdir): - testdir.makeini( +def test_customized_python_discovery_functions(pytester: Pytester) -> None: + pytester.makeini( """ [pytest] python_functions=_test """ ) - testdir.makepyfile( + pytester.makepyfile( """ def _test_underscore(): pass """ ) - result = testdir.runpytest("--collect-only", "-s") + result = pytester.runpytest("--collect-only", "-s") result.stdout.fnmatch_lines(["*_test_underscore*"]) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) -def test_unorderable_types(testdir): - testdir.makepyfile( +def test_unorderable_types(pytester: Pytester) -> None: + pytester.makepyfile( """ class TestJoinEmpty(object): pass @@ -1205,19 +1232,19 @@ class Test(object): TestFoo = make_test() """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.no_fnmatch_line("*TypeError*") assert result.ret == ExitCode.NO_TESTS_COLLECTED @pytest.mark.filterwarnings("default") -def test_dont_collect_non_function_callable(testdir): +def test_dont_collect_non_function_callable(pytester: Pytester) -> None: """Test for issue https://github.com/pytest-dev/pytest/issues/331 In this case an INTERNALERROR occurred trying to report the failure of a test like this one because pytest failed to get the source lines. """ - testdir.makepyfile( + pytester.makepyfile( """ class Oh(object): def __call__(self): @@ -1229,7 +1256,7 @@ def test_real(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*collected 1 item*", @@ -1239,21 +1266,21 @@ def test_real(): ) -def test_class_injection_does_not_break_collection(testdir): +def test_class_injection_does_not_break_collection(pytester: Pytester) -> None: """Tests whether injection during collection time will terminate testing. In this case the error should not occur if the TestClass itself is modified during collection time, and the original method list is still used for collection. """ - testdir.makeconftest( + pytester.makeconftest( """ from test_inject import TestClass def pytest_generate_tests(metafunc): TestClass.changed_var = {} """ ) - testdir.makepyfile( + pytester.makepyfile( test_inject=''' class TestClass(object): def test_injection(self): @@ -1261,7 +1288,7 @@ def test_injection(self): pass ''' ) - result = testdir.runpytest() + result = pytester.runpytest() assert ( "RuntimeError: dictionary changed size during iteration" not in result.stdout.str() @@ -1269,16 +1296,16 @@ def test_injection(self): result.stdout.fnmatch_lines(["*1 passed*"]) -def test_syntax_error_with_non_ascii_chars(testdir): +def test_syntax_error_with_non_ascii_chars(pytester: Pytester) -> None: """Fix decoding issue while formatting SyntaxErrors during collection (#578).""" - testdir.makepyfile("☃") - result = testdir.runpytest() + pytester.makepyfile("☃") + result = pytester.runpytest() result.stdout.fnmatch_lines(["*ERROR collecting*", "*SyntaxError*", "*1 error in*"]) -def test_collect_error_with_fulltrace(testdir): - testdir.makepyfile("assert 0") - result = testdir.runpytest("--fulltrace") +def test_collect_error_with_fulltrace(pytester: Pytester) -> None: + pytester.makepyfile("assert 0") + result = pytester.runpytest("--fulltrace") result.stdout.fnmatch_lines( [ "collected 0 items / 1 error", @@ -1295,14 +1322,14 @@ def test_collect_error_with_fulltrace(testdir): ) -def test_skip_duplicates_by_default(testdir): +def test_skip_duplicates_by_default(pytester: Pytester) -> None: """Test for issue https://github.com/pytest-dev/pytest/issues/1609 (#1609) Ignore duplicate directories. """ - a = testdir.mkdir("a") - fh = a.join("test_a.py") - fh.write( + a = pytester.mkdir("a") + fh = a.joinpath("test_a.py") + fh.write_text( textwrap.dedent( """\ import pytest @@ -1311,18 +1338,18 @@ def test_real(): """ ) ) - result = testdir.runpytest(a.strpath, a.strpath) + result = pytester.runpytest(str(a), str(a)) result.stdout.fnmatch_lines(["*collected 1 item*"]) -def test_keep_duplicates(testdir): +def test_keep_duplicates(pytester: Pytester) -> None: """Test for issue https://github.com/pytest-dev/pytest/issues/1609 (#1609) Use --keep-duplicates to collect tests from duplicate directories. """ - a = testdir.mkdir("a") - fh = a.join("test_a.py") - fh.write( + a = pytester.mkdir("a") + fh = a.joinpath("test_a.py") + fh.write_text( textwrap.dedent( """\ import pytest @@ -1331,24 +1358,24 @@ def test_real(): """ ) ) - result = testdir.runpytest("--keep-duplicates", a.strpath, a.strpath) + result = pytester.runpytest("--keep-duplicates", str(a), str(a)) result.stdout.fnmatch_lines(["*collected 2 item*"]) -def test_package_collection_infinite_recursion(testdir): - testdir.copy_example("collect/package_infinite_recursion") - result = testdir.runpytest() +def test_package_collection_infinite_recursion(pytester: Pytester) -> None: + pytester.copy_example("collect/package_infinite_recursion") + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) -def test_package_collection_init_given_as_argument(testdir): +def test_package_collection_init_given_as_argument(pytester: Pytester) -> None: """Regression test for #3749""" - p = testdir.copy_example("collect/package_init_given_as_arg") - result = testdir.runpytest(p / "pkg" / "__init__.py") + p = pytester.copy_example("collect/package_init_given_as_arg") + result = pytester.runpytest(p / "pkg" / "__init__.py") result.stdout.fnmatch_lines(["*1 passed*"]) -def test_package_with_modules(testdir): +def test_package_with_modules(pytester: Pytester) -> None: """ . └── root @@ -1363,32 +1390,35 @@ def test_package_with_modules(testdir): └── test_in_sub2.py """ - root = testdir.mkpydir("root") - sub1 = root.mkdir("sub1") - sub1.ensure("__init__.py") - sub1_test = sub1.mkdir("sub1_1") - sub1_test.ensure("__init__.py") - sub2 = root.mkdir("sub2") - sub2_test = sub2.mkdir("sub2") + root = pytester.mkpydir("root") + sub1 = root.joinpath("sub1") + sub1_test = sub1.joinpath("sub1_1") + sub1_test.mkdir(parents=True) + for d in (sub1, sub1_test): + d.joinpath("__init__.py").touch() - sub1_test.join("test_in_sub1.py").write("def test_1(): pass") - sub2_test.join("test_in_sub2.py").write("def test_2(): pass") + sub2 = root.joinpath("sub2") + sub2_test = sub2.joinpath("test") + sub2_test.mkdir(parents=True) + + sub1_test.joinpath("test_in_sub1.py").write_text("def test_1(): pass") + sub2_test.joinpath("test_in_sub2.py").write_text("def test_2(): pass") # Execute from . - result = testdir.runpytest("-v", "-s") + result = pytester.runpytest("-v", "-s") result.assert_outcomes(passed=2) # Execute from . with one argument "root" - result = testdir.runpytest("-v", "-s", "root") + result = pytester.runpytest("-v", "-s", "root") result.assert_outcomes(passed=2) # Chdir into package's root and execute with no args - root.chdir() - result = testdir.runpytest("-v", "-s") + os.chdir(root) + result = pytester.runpytest("-v", "-s") result.assert_outcomes(passed=2) -def test_package_ordering(testdir): +def test_package_ordering(pytester: Pytester) -> None: """ . └── root @@ -1402,22 +1432,24 @@ def test_package_ordering(testdir): └── test_sub2.py """ - testdir.makeini( + pytester.makeini( """ [pytest] python_files=*.py """ ) - root = testdir.mkpydir("root") - sub1 = root.mkdir("sub1") - sub1.ensure("__init__.py") - sub2 = root.mkdir("sub2") - sub2_test = sub2.mkdir("sub2") - - root.join("Test_root.py").write("def test_1(): pass") - sub1.join("Test_sub1.py").write("def test_2(): pass") - sub2_test.join("test_sub2.py").write("def test_3(): pass") + root = pytester.mkpydir("root") + sub1 = root.joinpath("sub1") + sub1.mkdir() + sub1.joinpath("__init__.py").touch() + sub2 = root.joinpath("sub2") + sub2_test = sub2.joinpath("test") + sub2_test.mkdir(parents=True) + + root.joinpath("Test_root.py").write_text("def test_1(): pass") + sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass") + sub2_test.joinpath("test_sub2.py").write_text("def test_3(): pass") # Execute from . - result = testdir.runpytest("-v", "-s") + result = pytester.runpytest("-v", "-s") result.assert_outcomes(passed=3) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index ac62de608e5..862a65abe10 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1,3 +1,4 @@ +import os import sys import textwrap from pathlib import Path @@ -7,9 +8,10 @@ from _pytest.compat import getfuncargnames from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest +from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import get_public_names from _pytest.pytester import Pytester -from _pytest.pytester import Testdir +from _pytest.python import Function def test_getfuncargnames_functions(): @@ -91,9 +93,9 @@ def test_fillfuncargs_exposed(self): # used by oejskit, kept for compatibility assert pytest._fillfuncargs == fixtures._fillfuncargs - def test_funcarg_lookupfails(self, testdir): - testdir.copy_example() - result = testdir.runpytest() # "--collect-only") + def test_funcarg_lookupfails(self, pytester: Pytester) -> None: + pytester.copy_example() + result = pytester.runpytest() # "--collect-only") assert result.ret != 0 result.stdout.fnmatch_lines( """ @@ -103,60 +105,63 @@ def test_funcarg_lookupfails(self, testdir): """ ) - def test_detect_recursive_dependency_error(self, testdir): - testdir.copy_example() - result = testdir.runpytest() + def test_detect_recursive_dependency_error(self, pytester: Pytester) -> None: + pytester.copy_example() + result = pytester.runpytest() result.stdout.fnmatch_lines( ["*recursive dependency involving fixture 'fix1' detected*"] ) - def test_funcarg_basic(self, testdir): - testdir.copy_example() - item = testdir.getitem(Path("test_funcarg_basic.py")) + def test_funcarg_basic(self, pytester: Pytester) -> None: + pytester.copy_example() + item = pytester.getitem(Path("test_funcarg_basic.py")) + assert isinstance(item, Function) item._request._fillfixtures() del item.funcargs["request"] assert len(get_public_names(item.funcargs)) == 2 assert item.funcargs["some"] == "test_func" assert item.funcargs["other"] == 42 - def test_funcarg_lookup_modulelevel(self, testdir): - testdir.copy_example() - reprec = testdir.inline_run() + def test_funcarg_lookup_modulelevel(self, pytester: Pytester) -> None: + pytester.copy_example() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_funcarg_lookup_classlevel(self, testdir): - p = testdir.copy_example() - result = testdir.runpytest(p) + def test_funcarg_lookup_classlevel(self, pytester: Pytester) -> None: + p = pytester.copy_example() + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*1 passed*"]) - def test_conftest_funcargs_only_available_in_subdir(self, testdir): - testdir.copy_example() - result = testdir.runpytest("-v") + def test_conftest_funcargs_only_available_in_subdir( + self, pytester: Pytester + ) -> None: + pytester.copy_example() + result = pytester.runpytest("-v") result.assert_outcomes(passed=2) - def test_extend_fixture_module_class(self, testdir): - testfile = testdir.copy_example() - result = testdir.runpytest() + def test_extend_fixture_module_class(self, pytester: Pytester) -> None: + testfile = pytester.copy_example() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - result = testdir.runpytest(testfile) + result = pytester.runpytest(testfile) result.stdout.fnmatch_lines(["*1 passed*"]) - def test_extend_fixture_conftest_module(self, testdir): - p = testdir.copy_example() - result = testdir.runpytest() + def test_extend_fixture_conftest_module(self, pytester: Pytester) -> None: + p = pytester.copy_example() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - result = testdir.runpytest(str(next(Path(str(p)).rglob("test_*.py")))) + result = pytester.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() + def test_extend_fixture_conftest_conftest(self, pytester: Pytester) -> None: + p = pytester.copy_example() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - result = testdir.runpytest(str(next(Path(str(p)).rglob("test_*.py")))) + result = pytester.runpytest(str(next(Path(str(p)).rglob("test_*.py")))) result.stdout.fnmatch_lines(["*1 passed*"]) - def test_extend_fixture_conftest_plugin(self, testdir): - testdir.makepyfile( + def test_extend_fixture_conftest_plugin(self, pytester: Pytester) -> None: + pytester.makepyfile( testplugin=""" import pytest @@ -165,8 +170,8 @@ def foo(): return 7 """ ) - testdir.syspathinsert() - testdir.makeconftest( + pytester.syspathinsert() + pytester.makeconftest( """ import pytest @@ -177,18 +182,18 @@ def foo(foo): return foo + 7 """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_foo(foo): assert foo == 14 """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") assert result.ret == 0 - def test_extend_fixture_plugin_plugin(self, testdir): + def test_extend_fixture_plugin_plugin(self, pytester: Pytester) -> None: # Two plugins should extend each order in loading order - testdir.makepyfile( + pytester.makepyfile( testplugin0=""" import pytest @@ -197,7 +202,7 @@ def foo(): return 7 """ ) - testdir.makepyfile( + pytester.makepyfile( testplugin1=""" import pytest @@ -206,8 +211,8 @@ def foo(foo): return foo + 7 """ ) - testdir.syspathinsert() - testdir.makepyfile( + pytester.syspathinsert() + pytester.makepyfile( """ pytest_plugins = ['testplugin0', 'testplugin1'] @@ -215,12 +220,14 @@ def test_foo(foo): assert foo == 14 """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 - def test_override_parametrized_fixture_conftest_module(self, testdir): + def test_override_parametrized_fixture_conftest_module( + self, pytester: Pytester + ) -> None: """Test override of the parametrized fixture with non-parametrized one on the test module level.""" - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -229,7 +236,7 @@ def spam(request): return request.param """ ) - testfile = testdir.makepyfile( + testfile = pytester.makepyfile( """ import pytest @@ -241,14 +248,16 @@ def test_spam(spam): assert spam == 'spam' """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - result = testdir.runpytest(testfile) + result = pytester.runpytest(testfile) result.stdout.fnmatch_lines(["*1 passed*"]) - def test_override_parametrized_fixture_conftest_conftest(self, testdir): + def test_override_parametrized_fixture_conftest_conftest( + self, pytester: Pytester + ) -> None: """Test override of the parametrized fixture with non-parametrized one on the conftest level.""" - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -257,8 +266,8 @@ def spam(request): return request.param """ ) - subdir = testdir.mkpydir("subdir") - subdir.join("conftest.py").write( + subdir = pytester.mkpydir("subdir") + subdir.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -269,8 +278,8 @@ def spam(): """ ) ) - testfile = subdir.join("test_spam.py") - testfile.write( + testfile = subdir.joinpath("test_spam.py") + testfile.write_text( textwrap.dedent( """\ def test_spam(spam): @@ -278,14 +287,16 @@ def test_spam(spam): """ ) ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - result = testdir.runpytest(testfile) + result = pytester.runpytest(testfile) result.stdout.fnmatch_lines(["*1 passed*"]) - def test_override_non_parametrized_fixture_conftest_module(self, testdir): + def test_override_non_parametrized_fixture_conftest_module( + self, pytester: Pytester + ) -> None: """Test override of the non-parametrized fixture with parametrized one on the test module level.""" - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -294,7 +305,7 @@ def spam(): return 'spam' """ ) - testfile = testdir.makepyfile( + testfile = pytester.makepyfile( """ import pytest @@ -309,14 +320,16 @@ def test_spam(spam): params['spam'] += 1 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) - result = testdir.runpytest(testfile) + result = pytester.runpytest(testfile) result.stdout.fnmatch_lines(["*3 passed*"]) - def test_override_non_parametrized_fixture_conftest_conftest(self, testdir): + def test_override_non_parametrized_fixture_conftest_conftest( + self, pytester: Pytester + ) -> None: """Test override of the non-parametrized fixture with parametrized one on the conftest level.""" - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -325,8 +338,8 @@ def spam(): return 'spam' """ ) - subdir = testdir.mkpydir("subdir") - subdir.join("conftest.py").write( + subdir = pytester.mkpydir("subdir") + subdir.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -337,8 +350,8 @@ def spam(request): """ ) ) - testfile = subdir.join("test_spam.py") - testfile.write( + testfile = subdir.joinpath("test_spam.py") + testfile.write_text( textwrap.dedent( """\ params = {'spam': 1} @@ -349,18 +362,18 @@ def test_spam(spam): """ ) ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) - result = testdir.runpytest(testfile) + result = pytester.runpytest(testfile) result.stdout.fnmatch_lines(["*3 passed*"]) def test_override_autouse_fixture_with_parametrized_fixture_conftest_conftest( - self, testdir - ): + self, pytester: Pytester + ) -> None: """Test override of the autouse fixture with parametrized one on the conftest level. This test covers the issue explained in issue 1601 """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -369,8 +382,8 @@ def spam(): return 'spam' """ ) - subdir = testdir.mkpydir("subdir") - subdir.join("conftest.py").write( + subdir = pytester.mkpydir("subdir") + subdir.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -381,8 +394,8 @@ def spam(request): """ ) ) - testfile = subdir.join("test_spam.py") - testfile.write( + testfile = subdir.joinpath("test_spam.py") + testfile.write_text( textwrap.dedent( """\ params = {'spam': 1} @@ -393,16 +406,18 @@ def test_spam(spam): """ ) ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) - result = testdir.runpytest(testfile) + result = pytester.runpytest(testfile) result.stdout.fnmatch_lines(["*3 passed*"]) - def test_override_fixture_reusing_super_fixture_parametrization(self, testdir): + def test_override_fixture_reusing_super_fixture_parametrization( + self, pytester: Pytester + ) -> None: """Override a fixture at a lower level, reusing the higher-level fixture that is parametrized (#1953). """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -411,7 +426,7 @@ def foo(request): return request.param """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -423,14 +438,16 @@ def test_spam(foo): assert foo in (2, 4) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 passed*"]) - def test_override_parametrize_fixture_and_indirect(self, testdir): + def test_override_parametrize_fixture_and_indirect( + self, pytester: Pytester + ) -> None: """Override a fixture at a lower level, reusing the higher-level fixture that is parametrized, while also using indirect parametrization. """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -439,7 +456,7 @@ def foo(request): return request.param """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -457,14 +474,14 @@ def test_spam(bar, foo): assert foo in (2, 4) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 passed*"]) def test_override_top_level_fixture_reusing_super_fixture_parametrization( - self, testdir - ): + self, pytester: Pytester + ) -> None: """Same as the above test, but with another level of overwriting.""" - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -473,7 +490,7 @@ def foo(request): return request.param """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -491,15 +508,17 @@ def test_spam(self, foo): assert foo in (2, 4) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 passed*"]) - def test_override_parametrized_fixture_with_new_parametrized_fixture(self, testdir): + def test_override_parametrized_fixture_with_new_parametrized_fixture( + self, pytester: Pytester + ) -> None: """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( + pytester.makeconftest( """ import pytest @@ -508,7 +527,7 @@ def foo(request): return request.param """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -521,13 +540,13 @@ def test_spam(foo): assert foo in (20, 40) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 passed*"]) - def test_autouse_fixture_plugin(self, testdir): + def test_autouse_fixture_plugin(self, pytester: Pytester) -> None: # A fixture from a plugin has no baseid set, which screwed up # the autouse fixture handling. - testdir.makepyfile( + pytester.makepyfile( testplugin=""" import pytest @@ -536,8 +555,8 @@ def foo(request): request.function.foo = 7 """ ) - testdir.syspathinsert() - testdir.makepyfile( + pytester.syspathinsert() + pytester.makepyfile( """ pytest_plugins = 'testplugin' @@ -545,11 +564,11 @@ def test_foo(request): assert request.function.foo == 7 """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 - def test_funcarg_lookup_error(self, testdir): - testdir.makeconftest( + def test_funcarg_lookup_error(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @@ -566,13 +585,13 @@ def c_fixture(): pass def d_fixture(): pass """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_lookup_error(unknown): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*ERROR at setup of test_lookup_error*", @@ -586,9 +605,9 @@ def test_lookup_error(unknown): ) result.stdout.no_fnmatch_line("*INTERNAL*") - def test_fixture_excinfo_leak(self, testdir): + def test_fixture_excinfo_leak(self, pytester: Pytester) -> None: # on python2 sys.excinfo would leak into fixture executions - testdir.makepyfile( + pytester.makepyfile( """ import sys import traceback @@ -607,13 +626,13 @@ def test_leak(leak): assert sys.exc_info() == (None, None, None) """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 class TestRequestBasic: - def test_request_attributes(self, testdir): - item = testdir.getitem( + def test_request_attributes(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @@ -622,6 +641,7 @@ def something(request): pass def test_func(something): pass """ ) + assert isinstance(item, Function) req = fixtures.FixtureRequest(item, _ispytest=True) assert req.function == item.obj assert req.keywords == item.keywords @@ -631,8 +651,8 @@ def test_func(something): pass assert req.config == item.config assert repr(req).find(req.function.__name__) != -1 - def test_request_attributes_method(self, testdir): - (item,) = testdir.getitems( + def test_request_attributes_method(self, pytester: Pytester) -> None: + (item,) = pytester.getitems( """ import pytest class TestB(object): @@ -644,12 +664,13 @@ def test_func(self, something): pass """ ) + assert isinstance(item, Function) req = item._request assert req.cls.__name__ == "TestB" assert req.instance.__class__ == req.cls - def test_request_contains_funcarg_arg2fixturedefs(self, testdir): - modcol = testdir.getmodulecol( + def test_request_contains_funcarg_arg2fixturedefs(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol( """ import pytest @pytest.fixture @@ -660,7 +681,7 @@ def test_method(self, something): pass """ ) - (item1,) = testdir.genitems([modcol]) + (item1,) = pytester.genitems([modcol]) assert item1.name == "test_method" arg2fixturedefs = fixtures.FixtureRequest( item1, _ispytest=True @@ -672,14 +693,14 @@ def test_method(self, something): hasattr(sys, "pypy_version_info"), reason="this method of test doesn't work on pypy", ) - def test_request_garbage(self, testdir): + def test_request_garbage(self, pytester: Pytester) -> None: try: import xdist # noqa except ImportError: pass else: pytest.xfail("this test is flaky when executed with xdist") - testdir.makepyfile( + pytester.makepyfile( """ import sys import pytest @@ -705,11 +726,11 @@ def test_func(): pass """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["* 1 passed in *"]) - def test_getfixturevalue_recursive(self, testdir): - testdir.makeconftest( + def test_getfixturevalue_recursive(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @@ -718,7 +739,7 @@ def something(request): return 1 """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -729,10 +750,10 @@ def test_func(something): assert something == 2 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_getfixturevalue_teardown(self, testdir): + def test_getfixturevalue_teardown(self, pytester: Pytester) -> None: """ Issue #1895 @@ -743,7 +764,7 @@ def test_getfixturevalue_teardown(self, testdir): `inner` dependent on `resource` when it is used via `getfixturevalue`: `test_func` will then cause the `resource`'s finalizer to be called first because of this. """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -767,11 +788,11 @@ def test_func(resource): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 2 passed in *"]) - def test_getfixturevalue(self, testdir): - item = testdir.getitem( + def test_getfixturevalue(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest values = [2] @@ -783,6 +804,7 @@ def other(request): def test_func(something): pass """ ) + assert isinstance(item, Function) req = item._request with pytest.raises(pytest.FixtureLookupError): @@ -800,8 +822,8 @@ def test_func(something): pass assert len(get_public_names(item.funcargs)) == 2 assert "request" in item.funcargs - def test_request_addfinalizer(self, testdir): - item = testdir.getitem( + def test_request_addfinalizer(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest teardownlist = [] @@ -811,18 +833,21 @@ def something(request): def test_func(something): pass """ ) + assert isinstance(item, Function) item.session._setupstate.prepare(item) item._request._fillfixtures() # successively check finalization calls - teardownlist = item.getparent(pytest.Module).obj.teardownlist + parent = item.getparent(pytest.Module) + assert parent is not None + teardownlist = parent.obj.teardownlist ss = item.session._setupstate assert not teardownlist ss.teardown_exact(item, None) print(ss.stack) assert teardownlist == [1] - def test_request_addfinalizer_failing_setup(self, testdir): - testdir.makepyfile( + def test_request_addfinalizer_failing_setup(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [1] @@ -836,11 +861,13 @@ def test_finalizer_ran(): assert not values """ ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") reprec.assertoutcome(failed=1, passed=1) - def test_request_addfinalizer_failing_setup_module(self, testdir): - testdir.makepyfile( + def test_request_addfinalizer_failing_setup_module( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( """ import pytest values = [1, 2] @@ -853,12 +880,14 @@ def test_fix(myfix): pass """ ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") mod = reprec.getcalls("pytest_runtest_setup")[0].item.module assert not mod.values - def test_request_addfinalizer_partial_setup_failure(self, testdir): - p = testdir.makepyfile( + def test_request_addfinalizer_partial_setup_failure( + self, pytester: Pytester + ) -> None: + p = pytester.makepyfile( """ import pytest values = [] @@ -871,17 +900,19 @@ def test_second(): assert len(values) == 1 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines( ["*1 error*"] # XXX the whole module collection fails ) - def test_request_subrequest_addfinalizer_exceptions(self, testdir): + def test_request_subrequest_addfinalizer_exceptions( + self, pytester: Pytester + ) -> None: """ Ensure exceptions raised during teardown by a finalizer are suppressed until all finalizers are called, re-raising the first exception (#2440) """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest values = [] @@ -905,19 +936,19 @@ def test_second(): assert values == [3, 2, 1] """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( ["*Exception: Error in excepts fixture", "* 2 passed, 1 error in *"] ) - def test_request_getmodulepath(self, testdir): - modcol = testdir.getmodulecol("def test_somefunc(): pass") - (item,) = testdir.genitems([modcol]) + def test_request_getmodulepath(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol("def test_somefunc(): pass") + (item,) = pytester.genitems([modcol]) req = fixtures.FixtureRequest(item, _ispytest=True) assert req.fspath == modcol.fspath - def test_request_fixturenames(self, testdir): - testdir.makepyfile( + def test_request_fixturenames(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest from _pytest.pytester import get_public_names @@ -936,17 +967,17 @@ def test_function(request, farg): "tmp_path", "tmp_path_factory"]) """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_request_fixturenames_dynamic_fixture(self, testdir): + def test_request_fixturenames_dynamic_fixture(self, pytester: Pytester) -> None: """Regression test for #3057""" - testdir.copy_example("fixtures/test_getfixturevalue_dynamic.py") - result = testdir.runpytest() + pytester.copy_example("fixtures/test_getfixturevalue_dynamic.py") + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - def test_setupdecorator_and_xunit(self, testdir): - testdir.makepyfile( + def test_setupdecorator_and_xunit(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -974,13 +1005,14 @@ def test_all(): "function", "method", "function"] """ ) - reprec = testdir.inline_run("-v") + reprec = pytester.inline_run("-v") reprec.assertoutcome(passed=3) - def test_fixtures_sub_subdir_normalize_sep(self, testdir): + def test_fixtures_sub_subdir_normalize_sep(self, pytester: Pytester) -> None: # this tests that normalization of nodeids takes place - b = testdir.mkdir("tests").mkdir("unit") - b.join("conftest.py").write( + b = pytester.path.joinpath("tests", "unit") + b.mkdir(parents=True) + b.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -990,9 +1022,9 @@ def arg1(): """ ) ) - p = b.join("test_module.py") - p.write("def test_func(arg1): pass") - result = testdir.runpytest(p, "--fixtures") + p = b.joinpath("test_module.py") + p.write_text("def test_func(arg1): pass") + result = pytester.runpytest(p, "--fixtures") assert result.ret == 0 result.stdout.fnmatch_lines( """ @@ -1001,13 +1033,13 @@ def arg1(): """ ) - def test_show_fixtures_color_yes(self, testdir): - testdir.makepyfile("def test_this(): assert 1") - result = testdir.runpytest("--color=yes", "--fixtures") + def test_show_fixtures_color_yes(self, pytester: Pytester) -> None: + pytester.makepyfile("def test_this(): assert 1") + result = pytester.runpytest("--color=yes", "--fixtures") assert "\x1b[32mtmpdir" in result.stdout.str() - def test_newstyle_with_request(self, testdir): - testdir.makepyfile( + def test_newstyle_with_request(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture() @@ -1017,11 +1049,11 @@ def test_1(arg): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_setupcontext_no_param(self, testdir): - testdir.makepyfile( + def test_setupcontext_no_param(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(params=[1,2]) @@ -1035,13 +1067,13 @@ def test_1(arg): assert arg in (1,2) """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) class TestRequestMarking: - def test_applymarker(self, testdir): - item1, item2 = testdir.getitems( + def test_applymarker(self, pytester: Pytester) -> None: + item1, item2 = pytester.getitems( """ import pytest @@ -1065,8 +1097,8 @@ def test_func2(self, something): with pytest.raises(ValueError): req1.applymarker(42) # type: ignore[arg-type] - def test_accesskeywords(self, testdir): - testdir.makepyfile( + def test_accesskeywords(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture() @@ -1078,11 +1110,11 @@ def test_function(keywords): assert "abc" not in keywords """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_accessmarker_dynamic(self, testdir): - testdir.makeconftest( + def test_accessmarker_dynamic(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @pytest.fixture() @@ -1094,7 +1126,7 @@ def marking(request): request.applymarker(pytest.mark.XYZ("hello")) """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest def test_fun1(keywords): @@ -1105,13 +1137,13 @@ def test_fun2(keywords): assert "abc" not in keywords """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) class TestFixtureUsages: - def test_noargfixturedec(self, testdir): - testdir.makepyfile( + def test_noargfixturedec(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture @@ -1122,11 +1154,11 @@ def test_func(arg1): assert arg1 == 1 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_receives_funcargs(self, testdir): - testdir.makepyfile( + def test_receives_funcargs(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture() @@ -1144,11 +1176,11 @@ def test_all(arg1, arg2): assert arg2 == 2 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_receives_funcargs_scope_mismatch(self, testdir): - testdir.makepyfile( + def test_receives_funcargs_scope_mismatch(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(scope="function") @@ -1163,7 +1195,7 @@ def test_add(arg2): assert arg2 == 2 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*ScopeMismatch*involved factories*", @@ -1173,8 +1205,10 @@ def test_add(arg2): ] ) - def test_receives_funcargs_scope_mismatch_issue660(self, testdir): - testdir.makepyfile( + def test_receives_funcargs_scope_mismatch_issue660( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(scope="function") @@ -1189,13 +1223,13 @@ def test_add(arg1, arg2): assert arg2 == 2 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( ["*ScopeMismatch*involved factories*", "* def arg2*", "*1 error*"] ) - def test_invalid_scope(self, testdir): - testdir.makepyfile( + def test_invalid_scope(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(scope="functions") @@ -1206,14 +1240,14 @@ def test_nothing(badscope): pass """ ) - result = testdir.runpytest_inprocess() + result = pytester.runpytest_inprocess() result.stdout.fnmatch_lines( "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'" ) @pytest.mark.parametrize("scope", ["function", "session"]) - def test_parameters_without_eq_semantics(self, scope, testdir): - testdir.makepyfile( + def test_parameters_without_eq_semantics(self, scope, pytester: Pytester) -> None: + pytester.makepyfile( """ class NoEq1: # fails on `a == b` statement def __eq__(self, _): @@ -1240,11 +1274,11 @@ def test2(no_eq): scope=scope ) ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*4 passed*"]) - def test_funcarg_parametrized_and_used_twice(self, testdir): - testdir.makepyfile( + def test_funcarg_parametrized_and_used_twice(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -1262,11 +1296,13 @@ def test_add(arg1, arg2): assert len(values) == arg1 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 passed*"]) - def test_factory_uses_unknown_funcarg_as_dependency_error(self, testdir): - testdir.makepyfile( + def test_factory_uses_unknown_funcarg_as_dependency_error( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( """ import pytest @@ -1282,7 +1318,7 @@ def test_missing(call_fail): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( """ *pytest.fixture()* @@ -1293,8 +1329,8 @@ def test_missing(call_fail): """ ) - def test_factory_setup_as_classes_fails(self, testdir): - testdir.makepyfile( + def test_factory_setup_as_classes_fails(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest class arg1(object): @@ -1304,12 +1340,12 @@ def __init__(self, request): """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() values = reprec.getfailedcollections() assert len(values) == 1 - def test_usefixtures_marker(self, testdir): - testdir.makepyfile( + def test_usefixtures_marker(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1330,17 +1366,17 @@ def test_two(self): pytest.mark.usefixtures("myfix")(TestClass) """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_usefixtures_ini(self, testdir): - testdir.makeini( + def test_usefixtures_ini(self, pytester: Pytester) -> None: + pytester.makeini( """ [pytest] usefixtures = myfix """ ) - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -1350,7 +1386,7 @@ def myfix(request): """ ) - testdir.makepyfile( + pytester.makepyfile( """ class TestClass(object): def test_one(self): @@ -1359,19 +1395,19 @@ def test_two(self): assert self.hello == "world" """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_usefixtures_seen_in_showmarkers(self, testdir): - result = testdir.runpytest("--markers") + def test_usefixtures_seen_in_showmarkers(self, pytester: Pytester) -> None: + result = pytester.runpytest("--markers") result.stdout.fnmatch_lines( """ *usefixtures(fixturename1*mark tests*fixtures* """ ) - def test_request_instance_issue203(self, testdir): - testdir.makepyfile( + def test_request_instance_issue203(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1384,11 +1420,11 @@ def test_hello(self, setup1): assert self.arg1 == 1 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_fixture_parametrized_with_iterator(self, testdir): - testdir.makepyfile( + def test_fixture_parametrized_with_iterator(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1411,14 +1447,14 @@ def test_2(arg2): values.append(arg2*10) """ ) - reprec = testdir.inline_run("-v") + reprec = pytester.inline_run("-v") reprec.assertoutcome(passed=4) values = reprec.getcalls("pytest_runtest_call")[0].item.module.values assert values == [1, 2, 10, 20] - def test_setup_functions_as_fixtures(self, testdir): + def test_setup_functions_as_fixtures(self, pytester: Pytester) -> None: """Ensure setup_* methods obey fixture scope rules (#517, #3094).""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1452,15 +1488,14 @@ def test_printer_2(self): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 2 passed in *"]) class TestFixtureManagerParseFactories: @pytest.fixture - def testdir(self, request): - testdir = request.getfixturevalue("testdir") - testdir.makeconftest( + def pytester(self, pytester: Pytester) -> Pytester: + pytester.makeconftest( """ import pytest @@ -1477,10 +1512,10 @@ def item(request): return request._pyfuncitem """ ) - return testdir + return pytester - def test_parsefactories_evil_objects_issue214(self, testdir): - testdir.makepyfile( + def test_parsefactories_evil_objects_issue214(self, pytester: Pytester) -> None: + pytester.makepyfile( """ class A(object): def __call__(self): @@ -1492,11 +1527,11 @@ def test_hello(): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1, failed=0) - def test_parsefactories_conftest(self, testdir): - testdir.makepyfile( + def test_parsefactories_conftest(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_hello(item, fm): for name in ("fm", "hello", "item"): @@ -1506,11 +1541,13 @@ def test_hello(item, fm): assert fac.func.__name__ == name """ ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=1) - def test_parsefactories_conftest_and_module_and_class(self, testdir): - testdir.makepyfile( + def test_parsefactories_conftest_and_module_and_class( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( """\ import pytest @@ -1531,15 +1568,17 @@ def test_hello(self, item, fm): assert faclist[2].func(item._request) == "class" """ ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=1) - def test_parsefactories_relative_node_ids(self, testdir): + def test_parsefactories_relative_node_ids( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: # example mostly taken from: # https://mail.python.org/pipermail/pytest-dev/2014-September/002617.html - runner = testdir.mkdir("runner") - package = testdir.mkdir("package") - package.join("conftest.py").write( + runner = pytester.mkdir("runner") + package = pytester.mkdir("package") + package.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -1549,7 +1588,7 @@ def one(): """ ) ) - package.join("test_x.py").write( + package.joinpath("test_x.py").write_text( textwrap.dedent( """\ def test_x(one): @@ -1557,9 +1596,10 @@ def test_x(one): """ ) ) - sub = package.mkdir("sub") - sub.join("__init__.py").ensure() - sub.join("conftest.py").write( + sub = package.joinpath("sub") + sub.mkdir() + sub.joinpath("__init__.py").touch() + sub.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -1569,7 +1609,7 @@ def one(): """ ) ) - sub.join("test_y.py").write( + sub.joinpath("test_y.py").write_text( textwrap.dedent( """\ def test_x(one): @@ -1577,20 +1617,21 @@ def test_x(one): """ ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - with runner.as_cwd(): - reprec = testdir.inline_run("..") + with monkeypatch.context() as mp: + mp.chdir(runner) + reprec = pytester.inline_run("..") reprec.assertoutcome(passed=2) - def test_package_xunit_fixture(self, testdir): - testdir.makepyfile( + def test_package_xunit_fixture(self, pytester: Pytester) -> None: + pytester.makepyfile( __init__="""\ values = [] """ ) - package = testdir.mkdir("package") - package.join("__init__.py").write( + package = pytester.mkdir("package") + package.joinpath("__init__.py").write_text( textwrap.dedent( """\ from .. import values @@ -1601,7 +1642,7 @@ def teardown_module(): """ ) ) - package.join("test_x.py").write( + package.joinpath("test_x.py").write_text( textwrap.dedent( """\ from .. import values @@ -1610,8 +1651,8 @@ def test_x(): """ ) ) - package = testdir.mkdir("package2") - package.join("__init__.py").write( + package = pytester.mkdir("package2") + package.joinpath("__init__.py").write_text( textwrap.dedent( """\ from .. import values @@ -1622,7 +1663,7 @@ def teardown_module(): """ ) ) - package.join("test_x.py").write( + package.joinpath("test_x.py").write_text( textwrap.dedent( """\ from .. import values @@ -1631,19 +1672,19 @@ def test_x(): """ ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_package_fixture_complex(self, testdir): - testdir.makepyfile( + def test_package_fixture_complex(self, pytester: Pytester) -> None: + pytester.makepyfile( __init__="""\ values = [] """ ) - testdir.syspathinsert(testdir.tmpdir.dirname) - package = testdir.mkdir("package") - package.join("__init__.py").write("") - package.join("conftest.py").write( + pytester.syspathinsert(pytester.path.name) + package = pytester.mkdir("package") + package.joinpath("__init__.py").write_text("") + package.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -1661,7 +1702,7 @@ def two(): """ ) ) - package.join("test_x.py").write( + package.joinpath("test_x.py").write_text( textwrap.dedent( """\ from .. import values @@ -1672,19 +1713,19 @@ def test_package(one): """ ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_collect_custom_items(self, testdir): - testdir.copy_example("fixtures/custom_item") - result = testdir.runpytest("foo") + def test_collect_custom_items(self, pytester: Pytester) -> None: + pytester.copy_example("fixtures/custom_item") + result = pytester.runpytest("foo") result.stdout.fnmatch_lines(["*passed*"]) class TestAutouseDiscovery: @pytest.fixture - def testdir(self, testdir): - testdir.makeconftest( + def pytester(self, pytester: Pytester) -> Pytester: + pytester.makeconftest( """ import pytest @pytest.fixture(autouse=True) @@ -1707,10 +1748,10 @@ def item(request): return request._pyfuncitem """ ) - return testdir + return pytester - def test_parsefactories_conftest(self, testdir): - testdir.makepyfile( + def test_parsefactories_conftest(self, pytester: Pytester) -> None: + pytester.makepyfile( """ from _pytest.pytester import get_public_names def test_check_setup(item, fm): @@ -1720,11 +1761,11 @@ def test_check_setup(item, fm): assert "perfunction" in autousenames """ ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=1) - def test_two_classes_separated_autouse(self, testdir): - testdir.makepyfile( + def test_two_classes_separated_autouse(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest class TestA(object): @@ -1743,11 +1784,11 @@ def test_setup2(self): assert self.values == [1] """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_setup_at_classlevel(self, testdir): - testdir.makepyfile( + def test_setup_at_classlevel(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest class TestClass(object): @@ -1760,12 +1801,12 @@ def test_method2(self): assert self.funcname == "test_method2" """ ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=2) @pytest.mark.xfail(reason="'enabled' feature not implemented") - def test_setup_enabled_functionnode(self, testdir): - testdir.makepyfile( + def test_setup_enabled_functionnode(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1788,13 +1829,13 @@ def test_func2(request): assert "db" in request.fixturenames """ ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=2) - def test_callables_nocode(self, testdir): + def test_callables_nocode(self, pytester: Pytester) -> None: """An imported mock.call would break setup/factory discovery due to it being callable and __code__ not being a code object.""" - testdir.makepyfile( + pytester.makepyfile( """ class _call(tuple): def __call__(self, *k, **kw): @@ -1805,13 +1846,13 @@ def __getattr__(self, k): call = _call() """ ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") reprec.assertoutcome(failed=0, passed=0) - def test_autouse_in_conftests(self, testdir): - a = testdir.mkdir("a") - b = testdir.mkdir("a1") - conftest = testdir.makeconftest( + def test_autouse_in_conftests(self, pytester: Pytester) -> None: + a = pytester.mkdir("a") + b = pytester.mkdir("a1") + conftest = pytester.makeconftest( """ import pytest @pytest.fixture(autouse=True) @@ -1819,18 +1860,18 @@ def hello(): xxx """ ) - conftest.move(a.join(conftest.basename)) - a.join("test_something.py").write("def test_func(): pass") - b.join("test_otherthing.py").write("def test_func(): pass") - result = testdir.runpytest() + conftest.rename(a.joinpath(conftest.name)) + a.joinpath("test_something.py").write_text("def test_func(): pass") + b.joinpath("test_otherthing.py").write_text("def test_func(): pass") + result = pytester.runpytest() result.stdout.fnmatch_lines( """ *1 passed*1 error* """ ) - def test_autouse_in_module_and_two_classes(self, testdir): - testdir.makepyfile( + def test_autouse_in_module_and_two_classes(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -1851,14 +1892,14 @@ def test_world(self): assert values == ["module", "module", "A", "module"], values """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=3) class TestAutouseManagement: - def test_autouse_conftest_mid_directory(self, testdir): - pkgdir = testdir.mkpydir("xyz123") - pkgdir.join("conftest.py").write( + def test_autouse_conftest_mid_directory(self, pytester: Pytester) -> None: + pkgdir = pytester.mkpydir("xyz123") + pkgdir.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -1869,8 +1910,11 @@ def app(): """ ) ) - t = pkgdir.ensure("tests", "test_app.py") - t.write( + sub = pkgdir.joinpath("tests") + sub.mkdir() + t = sub.joinpath("test_app.py") + t.touch() + t.write_text( textwrap.dedent( """\ import sys @@ -1879,11 +1923,11 @@ def test_app(): """ ) ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=1) - def test_funcarg_and_setup(self, testdir): - testdir.makepyfile( + def test_funcarg_and_setup(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -1906,11 +1950,11 @@ def test_hello2(arg): assert arg == 0 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_uses_parametrized_resource(self, testdir): - testdir.makepyfile( + def test_uses_parametrized_resource(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -1932,11 +1976,11 @@ def test_hello(): """ ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=2) - def test_session_parametrized_function(self, testdir): - testdir.makepyfile( + def test_session_parametrized_function(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1959,7 +2003,7 @@ def test_result(arg): assert values[:arg] == [1,2][:arg] """ ) - reprec = testdir.inline_run("-v", "-s") + reprec = pytester.inline_run("-v", "-s") reprec.assertoutcome(passed=4) def test_class_function_parametrization_finalization( @@ -2007,8 +2051,8 @@ def test_2(self): ].values assert values == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2 - def test_scope_ordering(self, testdir): - testdir.makepyfile( + def test_scope_ordering(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -2027,11 +2071,11 @@ def test_method(self): assert values == [1,3,2] """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_parametrization_setup_teardown_ordering(self, testdir): - testdir.makepyfile( + def test_parametrization_setup_teardown_ordering(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -2056,11 +2100,11 @@ def test_finish(): "setup-2", "step1-2", "step2-2", "teardown-2",] """ ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=5) - def test_ordering_autouse_before_explicit(self, testdir): - testdir.makepyfile( + def test_ordering_autouse_before_explicit(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -2075,14 +2119,16 @@ def test_hello(arg1): assert values == [1,2] """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @pytest.mark.parametrize("param1", ["", "params=[1]"], ids=["p00", "p01"]) @pytest.mark.parametrize("param2", ["", "params=[1]"], ids=["p10", "p11"]) - def test_ordering_dependencies_torndown_first(self, testdir, param1, param2): + def test_ordering_dependencies_torndown_first( + self, pytester: Pytester, param1, param2 + ) -> None: """#226""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest values = [] @@ -2102,13 +2148,13 @@ def test_check(): """ % locals() ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=2) class TestFixtureMarker: - def test_parametrize(self, testdir): - testdir.makepyfile( + def test_parametrize(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(params=["a", "b", "c"]) @@ -2121,11 +2167,11 @@ def test_result(): assert values == list("abc") """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=4) - def test_multiple_parametrization_issue_736(self, testdir): - testdir.makepyfile( + def test_multiple_parametrization_issue_736(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -2139,19 +2185,21 @@ def test_issue(foo, foobar): assert foobar in [4,5,6] """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=9) @pytest.mark.parametrize( "param_args", ["'fixt, val'", "'fixt,val'", "['fixt', 'val']", "('fixt', 'val')"], ) - def test_override_parametrized_fixture_issue_979(self, testdir, param_args): + def test_override_parametrized_fixture_issue_979( + self, pytester: Pytester, param_args + ) -> None: """Make sure a parametrized argument can override a parametrized fixture. This was a regression introduced in the fix for #736. """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -2165,11 +2213,11 @@ def test_foo(fixt, val): """ % param_args ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_scope_session(self, testdir): - testdir.makepyfile( + def test_scope_session(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -2189,11 +2237,11 @@ def test3(self, arg): assert len(values) == 1 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=3) - def test_scope_session_exc(self, testdir): - testdir.makepyfile( + def test_scope_session_exc(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -2210,11 +2258,11 @@ def test_last(): assert values == [1] """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(skipped=2, passed=1) - def test_scope_session_exc_two_fix(self, testdir): - testdir.makepyfile( + def test_scope_session_exc_two_fix(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -2236,11 +2284,11 @@ def test_last(): assert m == [] """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(skipped=2, passed=1) - def test_scope_exc(self, testdir): - testdir.makepyfile( + def test_scope_exc(self, pytester: Pytester) -> None: + pytester.makepyfile( test_foo=""" def test_foo(fix): pass @@ -2265,11 +2313,11 @@ def test_last(req_list): assert req_list == [1] """, ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(skipped=2, passed=1) - def test_scope_module_uses_session(self, testdir): - testdir.makepyfile( + def test_scope_module_uses_session(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -2289,11 +2337,11 @@ def test3(self, arg): assert len(values) == 1 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=3) - def test_scope_module_and_finalizer(self, testdir): - testdir.makeconftest( + def test_scope_module_and_finalizer(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest finalized_list = [] @@ -2311,7 +2359,7 @@ def finalized(request): return len(finalized_list) """ ) - testdir.makepyfile( + pytester.makepyfile( test_mod1=""" def test_1(arg, created, finalized): assert created == 1 @@ -2329,11 +2377,11 @@ def test_4(arg, created, finalized): assert finalized == 2 """, ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=4) - def test_scope_mismatch_various(self, testdir): - testdir.makeconftest( + def test_scope_mismatch_various(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest finalized = [] @@ -2343,7 +2391,7 @@ def arg(request): pass """ ) - testdir.makepyfile( + pytester.makepyfile( test_mod1=""" import pytest @pytest.fixture(scope="session") @@ -2353,14 +2401,14 @@ def test_1(arg): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines( ["*ScopeMismatch*You tried*function*session*request*"] ) - def test_dynamic_scope(self, testdir): - testdir.makeconftest( + def test_dynamic_scope(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @@ -2383,7 +2431,7 @@ def dynamic_fixture(calls=[]): """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_first(dynamic_fixture): assert dynamic_fixture == 1 @@ -2395,14 +2443,14 @@ def test_second(dynamic_fixture): """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - reprec = testdir.inline_run("--extend-scope") + reprec = pytester.inline_run("--extend-scope") reprec.assertoutcome(passed=1, failed=1) - def test_dynamic_scope_bad_return(self, testdir): - testdir.makepyfile( + def test_dynamic_scope_bad_return(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -2415,14 +2463,14 @@ def fixture(): """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( "Fixture 'fixture' from test_dynamic_scope_bad_return.py " "got an unexpected scope value 'wrong-scope'" ) - def test_register_only_with_mark(self, testdir): - testdir.makeconftest( + def test_register_only_with_mark(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @pytest.fixture() @@ -2430,7 +2478,7 @@ def arg(): return 1 """ ) - testdir.makepyfile( + pytester.makepyfile( test_mod1=""" import pytest @pytest.fixture() @@ -2440,11 +2488,11 @@ def test_1(arg): assert arg == 2 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_parametrize_and_scope(self, testdir): - testdir.makepyfile( + def test_parametrize_and_scope(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(scope="module", params=["a", "b", "c"]) @@ -2455,7 +2503,7 @@ def test_param(arg): values.append(arg) """ ) - reprec = testdir.inline_run("-v") + reprec = pytester.inline_run("-v") reprec.assertoutcome(passed=3) values = reprec.getcalls("pytest_runtest_call")[0].item.module.values assert len(values) == 3 @@ -2463,8 +2511,8 @@ def test_param(arg): assert "b" in values assert "c" in values - def test_scope_mismatch(self, testdir): - testdir.makeconftest( + def test_scope_mismatch(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @pytest.fixture(scope="function") @@ -2472,7 +2520,7 @@ def arg(request): pass """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.fixture(scope="session") @@ -2482,11 +2530,11 @@ def test_mismatch(arg): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*ScopeMismatch*", "*1 error*"]) - def test_parametrize_separated_order(self, testdir): - testdir.makepyfile( + def test_parametrize_separated_order(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -2501,19 +2549,19 @@ def test_2(arg): values.append(arg) """ ) - reprec = testdir.inline_run("-v") + reprec = pytester.inline_run("-v") reprec.assertoutcome(passed=4) values = reprec.getcalls("pytest_runtest_call")[0].item.module.values assert values == [1, 1, 2, 2] - def test_module_parametrized_ordering(self, testdir): - testdir.makeini( + def test_module_parametrized_ordering(self, pytester: Pytester) -> None: + pytester.makeini( """ [pytest] console_output_style=classic """ ) - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -2525,7 +2573,7 @@ def marg(): pass """ ) - testdir.makepyfile( + pytester.makepyfile( test_mod1=""" def test_func(sarg): pass @@ -2543,7 +2591,7 @@ def test_func4(marg): pass """, ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( """ test_mod1.py::test_func[s1] PASSED @@ -2565,14 +2613,14 @@ def test_func4(marg): """ ) - def test_dynamic_parametrized_ordering(self, testdir): - testdir.makeini( + def test_dynamic_parametrized_ordering(self, pytester: Pytester) -> None: + pytester.makeini( """ [pytest] console_output_style=classic """ ) - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -2592,7 +2640,7 @@ def reprovision(request, flavor, encap): pass """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test(reprovision): pass @@ -2600,7 +2648,7 @@ def test2(reprovision): pass """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( """ test_dynamic_parametrized_ordering.py::test[flavor1-vxlan] PASSED @@ -2614,14 +2662,14 @@ def test2(reprovision): """ ) - def test_class_ordering(self, testdir): - testdir.makeini( + def test_class_ordering(self, pytester: Pytester) -> None: + pytester.makeini( """ [pytest] console_output_style=classic """ ) - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -2642,7 +2690,7 @@ def fin(): request.addfinalizer(fin) """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -2656,7 +2704,7 @@ def test_3(self): pass """ ) - result = testdir.runpytest("-vs") + result = pytester.runpytest("-vs") result.stdout.re_match_lines( r""" test_class_ordering.py::TestClass2::test_1\[a-1\] PASSED @@ -2674,8 +2722,10 @@ def test_3(self): """ ) - def test_parametrize_separated_order_higher_scope_first(self, testdir): - testdir.makepyfile( + def test_parametrize_separated_order_higher_scope_first( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( """ import pytest @@ -2704,7 +2754,7 @@ def test_4(modarg, arg): values.append("test4") """ ) - reprec = testdir.inline_run("-v") + reprec = pytester.inline_run("-v") reprec.assertoutcome(passed=12) values = reprec.getcalls("pytest_runtest_call")[0].item.module.values expected = [ @@ -2750,8 +2800,8 @@ def test_4(modarg, arg): pprint.pprint(list(zip(values, expected))) assert values == expected - def test_parametrized_fixture_teardown_order(self, testdir): - testdir.makepyfile( + def test_parametrized_fixture_teardown_order(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(params=[1,2], scope="class") @@ -2783,7 +2833,7 @@ def test_finish(): assert not values """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( """ *3 passed* @@ -2791,8 +2841,8 @@ def test_finish(): ) result.stdout.no_fnmatch_line("*error*") - def test_fixture_finalizer(self, testdir): - testdir.makeconftest( + def test_fixture_finalizer(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest import sys @@ -2801,13 +2851,13 @@ def test_fixture_finalizer(self, testdir): def browser(request): def finalize(): - sys.stdout.write('Finalized') + sys.stdout.write_text('Finalized') request.addfinalizer(finalize) return {} """ ) - b = testdir.mkdir("subdir") - b.join("test_overridden_fixture_finalizer.py").write( + b = pytester.mkdir("subdir") + b.joinpath("test_overridden_fixture_finalizer.py").write_text( textwrap.dedent( """\ import pytest @@ -2821,12 +2871,12 @@ def test_browser(browser): """ ) ) - reprec = testdir.runpytest("-s") + reprec = pytester.runpytest("-s") for test in ["test_browser"]: reprec.stdout.fnmatch_lines(["*Finalized*"]) - def test_class_scope_with_normal_tests(self, testdir): - testpath = testdir.makepyfile( + def test_class_scope_with_normal_tests(self, pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import pytest @@ -2849,12 +2899,12 @@ class Test2(object): def test_c(self, a): assert a == 3""" ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) for test in ["test_a", "test_b", "test_c"]: assert reprec.matchreport(test).passed - def test_request_is_clean(self, testdir): - testdir.makepyfile( + def test_request_is_clean(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest values = [] @@ -2865,12 +2915,12 @@ def test_fix(fix): pass """ ) - reprec = testdir.inline_run("-s") + reprec = pytester.inline_run("-s") values = reprec.getcalls("pytest_runtest_call")[0].item.module.values assert values == [1, 2] - def test_parametrize_separated_lifecycle(self, testdir): - testdir.makepyfile( + def test_parametrize_separated_lifecycle(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -2886,7 +2936,7 @@ def test_2(arg): values.append(arg) """ ) - reprec = testdir.inline_run("-vs") + reprec = pytester.inline_run("-vs") reprec.assertoutcome(passed=4) values = reprec.getcalls("pytest_runtest_call")[0].item.module.values import pprint @@ -2898,8 +2948,10 @@ def test_2(arg): assert values[3] == values[4] == 2 assert values[5] == "fin2" - def test_parametrize_function_scoped_finalizers_called(self, testdir): - testdir.makepyfile( + def test_parametrize_function_scoped_finalizers_called( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( """ import pytest @@ -2919,13 +2971,15 @@ def test_3(): assert values == [1, "fin1", 2, "fin2", 1, "fin1", 2, "fin2"] """ ) - reprec = testdir.inline_run("-v") + reprec = pytester.inline_run("-v") reprec.assertoutcome(passed=5) @pytest.mark.parametrize("scope", ["session", "function", "module"]) - def test_finalizer_order_on_parametrization(self, scope, testdir): + def test_finalizer_order_on_parametrization( + self, scope, pytester: Pytester + ) -> None: """#246""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest values = [] @@ -2956,12 +3010,12 @@ def test_other(): """ % {"scope": scope} ) - reprec = testdir.inline_run("-lvs") + reprec = pytester.inline_run("-lvs") reprec.assertoutcome(passed=3) - def test_class_scope_parametrization_ordering(self, testdir): + def test_class_scope_parametrization_ordering(self, pytester: Pytester) -> None: """#396""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest values = [] @@ -2982,7 +3036,7 @@ def test_population(self, human): values.append("test_population") """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=6) values = reprec.getcalls("pytest_runtest_call")[0].item.module.values assert values == [ @@ -2998,8 +3052,8 @@ def test_population(self, human): "fin Doe", ] - def test_parametrize_setup_function(self, testdir): - testdir.makepyfile( + def test_parametrize_setup_function(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -3028,11 +3082,13 @@ def test_3(): """ ) - reprec = testdir.inline_run("-v") + reprec = pytester.inline_run("-v") reprec.assertoutcome(passed=6) - def test_fixture_marked_function_not_collected_as_test(self, testdir): - testdir.makepyfile( + def test_fixture_marked_function_not_collected_as_test( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture @@ -3043,11 +3099,11 @@ def test_something(test_app): assert test_app == 1 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_params_and_ids(self, testdir): - testdir.makepyfile( + def test_params_and_ids(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -3060,11 +3116,11 @@ def test_foo(fix): assert 1 """ ) - res = testdir.runpytest("-v") + res = pytester.runpytest("-v") res.stdout.fnmatch_lines(["*test_foo*alpha*", "*test_foo*beta*"]) - def test_params_and_ids_yieldfixture(self, testdir): - testdir.makepyfile( + def test_params_and_ids_yieldfixture(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -3076,12 +3132,14 @@ def test_foo(fix): assert 1 """ ) - res = testdir.runpytest("-v") + res = pytester.runpytest("-v") res.stdout.fnmatch_lines(["*test_foo*alpha*", "*test_foo*beta*"]) - def test_deterministic_fixture_collection(self, testdir, monkeypatch): + def test_deterministic_fixture_collection( + self, pytester: Pytester, monkeypatch + ) -> None: """#920""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -3106,21 +3164,21 @@ def test_foo(B): """ ) monkeypatch.setenv("PYTHONHASHSEED", "1") - out1 = testdir.runpytest_subprocess("-v") + out1 = pytester.runpytest_subprocess("-v") monkeypatch.setenv("PYTHONHASHSEED", "2") - out2 = testdir.runpytest_subprocess("-v") - out1 = [ + out2 = pytester.runpytest_subprocess("-v") + output1 = [ line for line in out1.outlines if line.startswith("test_deterministic_fixture_collection.py::test_foo") ] - out2 = [ + output2 = [ line for line in out2.outlines if line.startswith("test_deterministic_fixture_collection.py::test_foo") ] - assert len(out1) == 12 - assert out1 == out2 + assert len(output1) == 12 + assert output1 == output2 class TestRequestScopeAccess: @@ -3134,8 +3192,8 @@ class TestRequestScopeAccess: ], ) - def test_setup(self, testdir, scope, ok, error): - testdir.makepyfile( + def test_setup(self, pytester: Pytester, scope, ok, error) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(scope=%r, autouse=True) @@ -3152,11 +3210,11 @@ def test_func(): """ % (scope, ok.split(), error.split()) ) - reprec = testdir.inline_run("-l") + reprec = pytester.inline_run("-l") reprec.assertoutcome(passed=1) - def test_funcarg(self, testdir, scope, ok, error): - testdir.makepyfile( + def test_funcarg(self, pytester: Pytester, scope, ok, error) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(scope=%r) @@ -3173,13 +3231,13 @@ def test_func(arg): """ % (scope, ok.split(), error.split()) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) class TestErrors: - def test_subfactory_missing_funcarg(self, testdir): - testdir.makepyfile( + def test_subfactory_missing_funcarg(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture() @@ -3189,14 +3247,14 @@ def test_something(gen): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines( ["*def gen(qwe123):*", "*fixture*qwe123*not found*", "*1 error*"] ) - def test_issue498_fixture_finalizer_failing(self, testdir): - testdir.makepyfile( + def test_issue498_fixture_finalizer_failing(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture @@ -3215,7 +3273,7 @@ def test_3(): assert values[0] != values[1] """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( """ *ERROR*teardown*test_1* @@ -3226,8 +3284,8 @@ def test_3(): """ ) - def test_setupfunc_missing_funcarg(self, testdir): - testdir.makepyfile( + def test_setupfunc_missing_funcarg(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(autouse=True) @@ -3237,7 +3295,7 @@ def test_something(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines( ["*def gen(qwe123):*", "*fixture*qwe123*not found*", "*1 error*"] @@ -3245,12 +3303,12 @@ def test_something(): class TestShowFixtures: - def test_funcarg_compat(self, testdir): - config = testdir.parseconfigure("--funcargs") + def test_funcarg_compat(self, pytester: Pytester) -> None: + config = pytester.parseconfigure("--funcargs") assert config.option.showfixtures - def test_show_fixtures(self, testdir): - result = testdir.runpytest("--fixtures") + def test_show_fixtures(self, pytester: Pytester) -> None: + result = pytester.runpytest("--fixtures") result.stdout.fnmatch_lines( [ "tmpdir_factory [[]session scope[]]", @@ -3260,8 +3318,8 @@ def test_show_fixtures(self, testdir): ] ) - def test_show_fixtures_verbose(self, testdir): - result = testdir.runpytest("--fixtures", "-v") + def test_show_fixtures_verbose(self, pytester: Pytester) -> None: + result = pytester.runpytest("--fixtures", "-v") result.stdout.fnmatch_lines( [ "tmpdir_factory [[]session scope[]] -- *tmpdir.py*", @@ -3271,8 +3329,8 @@ def test_show_fixtures_verbose(self, testdir): ] ) - def test_show_fixtures_testmodule(self, testdir): - p = testdir.makepyfile( + def test_show_fixtures_testmodule(self, pytester: Pytester) -> None: + p = pytester.makepyfile( ''' import pytest @pytest.fixture @@ -3283,7 +3341,7 @@ def arg1(): """ hello world """ ''' ) - result = testdir.runpytest("--fixtures", p) + result = pytester.runpytest("--fixtures", p) result.stdout.fnmatch_lines( """ *tmpdir @@ -3295,8 +3353,8 @@ def arg1(): result.stdout.no_fnmatch_line("*arg0*") @pytest.mark.parametrize("testmod", [True, False]) - def test_show_fixtures_conftest(self, testdir, testmod): - testdir.makeconftest( + def test_show_fixtures_conftest(self, pytester: Pytester, testmod) -> None: + pytester.makeconftest( ''' import pytest @pytest.fixture @@ -3305,13 +3363,13 @@ def arg1(): ''' ) if testmod: - testdir.makepyfile( + pytester.makepyfile( """ def test_hello(): pass """ ) - result = testdir.runpytest("--fixtures") + result = pytester.runpytest("--fixtures") result.stdout.fnmatch_lines( """ *tmpdir* @@ -3321,8 +3379,8 @@ def test_hello(): """ ) - def test_show_fixtures_trimmed_doc(self, testdir): - p = testdir.makepyfile( + def test_show_fixtures_trimmed_doc(self, pytester: Pytester) -> None: + p = pytester.makepyfile( textwrap.dedent( '''\ import pytest @@ -3343,7 +3401,7 @@ def arg2(): ''' ) ) - result = testdir.runpytest("--fixtures", p) + result = pytester.runpytest("--fixtures", p) result.stdout.fnmatch_lines( textwrap.dedent( """\ @@ -3358,8 +3416,8 @@ def arg2(): ) ) - def test_show_fixtures_indented_doc(self, testdir): - p = testdir.makepyfile( + def test_show_fixtures_indented_doc(self, pytester: Pytester) -> None: + p = pytester.makepyfile( textwrap.dedent( '''\ import pytest @@ -3372,7 +3430,7 @@ def fixture1(): ''' ) ) - result = testdir.runpytest("--fixtures", p) + result = pytester.runpytest("--fixtures", p) result.stdout.fnmatch_lines( textwrap.dedent( """\ @@ -3384,8 +3442,10 @@ def fixture1(): ) ) - def test_show_fixtures_indented_doc_first_line_unindented(self, testdir): - p = testdir.makepyfile( + def test_show_fixtures_indented_doc_first_line_unindented( + self, pytester: Pytester + ) -> None: + p = pytester.makepyfile( textwrap.dedent( '''\ import pytest @@ -3398,7 +3458,7 @@ def fixture1(): ''' ) ) - result = testdir.runpytest("--fixtures", p) + result = pytester.runpytest("--fixtures", p) result.stdout.fnmatch_lines( textwrap.dedent( """\ @@ -3411,8 +3471,8 @@ def fixture1(): ) ) - def test_show_fixtures_indented_in_class(self, testdir): - p = testdir.makepyfile( + def test_show_fixtures_indented_in_class(self, pytester: Pytester) -> None: + p = pytester.makepyfile( textwrap.dedent( '''\ import pytest @@ -3426,7 +3486,7 @@ def fixture1(self): ''' ) ) - result = testdir.runpytest("--fixtures", p) + result = pytester.runpytest("--fixtures", p) result.stdout.fnmatch_lines( textwrap.dedent( """\ @@ -3439,9 +3499,9 @@ def fixture1(self): ) ) - def test_show_fixtures_different_files(self, testdir): + def test_show_fixtures_different_files(self, pytester: Pytester) -> None: """`--fixtures` only shows fixtures from first file (#833).""" - testdir.makepyfile( + pytester.makepyfile( test_a=''' import pytest @@ -3454,7 +3514,7 @@ def test_a(fix_a): pass ''' ) - testdir.makepyfile( + pytester.makepyfile( test_b=''' import pytest @@ -3467,7 +3527,7 @@ def test_b(fix_b): pass ''' ) - result = testdir.runpytest("--fixtures") + result = pytester.runpytest("--fixtures") result.stdout.fnmatch_lines( """ * fixtures defined from test_a * @@ -3480,8 +3540,8 @@ def test_b(fix_b): """ ) - def test_show_fixtures_with_same_name(self, testdir): - testdir.makeconftest( + def test_show_fixtures_with_same_name(self, pytester: Pytester) -> None: + pytester.makeconftest( ''' import pytest @pytest.fixture @@ -3490,13 +3550,13 @@ def arg1(): return "Hello World" ''' ) - testdir.makepyfile( + pytester.makepyfile( """ def test_foo(arg1): assert arg1 == "Hello World" """ ) - testdir.makepyfile( + pytester.makepyfile( ''' import pytest @pytest.fixture @@ -3507,7 +3567,7 @@ def test_bar(arg1): assert arg1 == "Hi" ''' ) - result = testdir.runpytest("--fixtures") + result = pytester.runpytest("--fixtures") result.stdout.fnmatch_lines( """ * fixtures defined from conftest * @@ -3531,8 +3591,8 @@ def foo(): class TestContextManagerFixtureFuncs: - def test_simple(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_simple(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture @@ -3547,7 +3607,7 @@ def test_2(arg1): assert 0 """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") result.stdout.fnmatch_lines( """ *setup* @@ -3559,8 +3619,8 @@ def test_2(arg1): """ ) - def test_scoped(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_scoped(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(scope="module") @@ -3574,7 +3634,7 @@ def test_2(arg1): print("test2", arg1) """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") result.stdout.fnmatch_lines( """ *setup* @@ -3584,8 +3644,8 @@ def test_2(arg1): """ ) - def test_setup_exception(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_setup_exception(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(scope="module") @@ -3596,7 +3656,7 @@ def test_1(arg1): pass """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") result.stdout.fnmatch_lines( """ *pytest.fail*setup* @@ -3604,8 +3664,8 @@ def test_1(arg1): """ ) - def test_teardown_exception(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_teardown_exception(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(scope="module") @@ -3616,7 +3676,7 @@ def test_1(arg1): pass """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") result.stdout.fnmatch_lines( """ *pytest.fail*teardown* @@ -3624,8 +3684,8 @@ def test_1(arg1): """ ) - def test_yields_more_than_one(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_yields_more_than_one(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(scope="module") @@ -3636,7 +3696,7 @@ def test_1(arg1): pass """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") result.stdout.fnmatch_lines( """ *fixture function* @@ -3644,8 +3704,8 @@ def test_1(arg1): """ ) - def test_custom_name(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_custom_name(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(name='meow') @@ -3655,13 +3715,13 @@ def test_1(meow): print(meow) """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") result.stdout.fnmatch_lines(["*mew*"]) class TestParameterizedSubRequest: - def test_call_from_fixture(self, testdir): - testdir.makepyfile( + def test_call_from_fixture(self, pytester: Pytester) -> None: + pytester.makepyfile( test_call_from_fixture=""" import pytest @@ -3677,7 +3737,7 @@ def test_foo(request, get_named_fixture): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "The requested fixture has no parameter defined for test:", @@ -3690,8 +3750,8 @@ def test_foo(request, get_named_fixture): ] ) - def test_call_from_test(self, testdir): - testdir.makepyfile( + def test_call_from_test(self, pytester: Pytester) -> None: + pytester.makepyfile( test_call_from_test=""" import pytest @@ -3703,7 +3763,7 @@ def test_foo(request): request.getfixturevalue('fix_with_param') """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "The requested fixture has no parameter defined for test:", @@ -3716,8 +3776,8 @@ def test_foo(request): ] ) - def test_external_fixture(self, testdir): - testdir.makeconftest( + def test_external_fixture(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @@ -3727,13 +3787,13 @@ def fix_with_param(request): """ ) - testdir.makepyfile( + pytester.makepyfile( test_external_fixture=""" def test_foo(request): request.getfixturevalue('fix_with_param') """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "The requested fixture has no parameter defined for test:", @@ -3747,11 +3807,11 @@ def test_foo(request): ] ) - def test_non_relative_path(self, testdir): - tests_dir = testdir.mkdir("tests") - fixdir = testdir.mkdir("fixtures") - fixfile = fixdir.join("fix.py") - fixfile.write( + def test_non_relative_path(self, pytester: Pytester) -> None: + tests_dir = pytester.mkdir("tests") + fixdir = pytester.mkdir("fixtures") + fixfile = fixdir.joinpath("fix.py") + fixfile.write_text( textwrap.dedent( """\ import pytest @@ -3763,8 +3823,8 @@ def fix_with_param(request): ) ) - testfile = tests_dir.join("test_foos.py") - testfile.write( + testfile = tests_dir.joinpath("test_foos.py") + testfile.write_text( textwrap.dedent( """\ from fix import fix_with_param @@ -3775,9 +3835,9 @@ def test_foo(request): ) ) - tests_dir.chdir() - testdir.syspathinsert(fixdir) - result = testdir.runpytest() + os.chdir(tests_dir) + pytester.syspathinsert(fixdir) + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "The requested fixture has no parameter defined for test:", @@ -3792,9 +3852,9 @@ def test_foo(request): ) # With non-overlapping rootdir, passing tests_dir. - rootdir = testdir.mkdir("rootdir") - rootdir.chdir() - result = testdir.runpytest("--rootdir", rootdir, tests_dir) + rootdir = pytester.mkdir("rootdir") + os.chdir(rootdir) + result = pytester.runpytest("--rootdir", rootdir, tests_dir) result.stdout.fnmatch_lines( [ "The requested fixture has no parameter defined for test:", @@ -3809,8 +3869,8 @@ def test_foo(request): ) -def test_pytest_fixture_setup_and_post_finalizer_hook(testdir): - testdir.makeconftest( +def test_pytest_fixture_setup_and_post_finalizer_hook(pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_fixture_setup(fixturedef, request): print('ROOT setup hook called for {0} from {1}'.format(fixturedef.argname, request.node.name)) @@ -3818,7 +3878,7 @@ def pytest_fixture_post_finalizer(fixturedef, request): print('ROOT finalizer hook called for {0} from {1}'.format(fixturedef.argname, request.node.name)) """ ) - testdir.makepyfile( + pytester.makepyfile( **{ "tests/conftest.py": """ def pytest_fixture_setup(fixturedef, request): @@ -3839,7 +3899,7 @@ def test_func(my_fixture): """, } ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") assert result.ret == 0 result.stdout.fnmatch_lines( [ @@ -3856,10 +3916,12 @@ class TestScopeOrdering: """Class of tests that ensure fixtures are ordered based on their scopes (#2405)""" @pytest.mark.parametrize("variant", ["mark", "autouse"]) - def test_func_closure_module_auto(self, testdir, variant, monkeypatch): + def test_func_closure_module_auto( + self, pytester: Pytester, variant, monkeypatch + ) -> None: """Semantically identical to the example posted in #2405 when ``use_mark=True``""" monkeypatch.setenv("FIXTURE_ACTIVATION_VARIANT", variant) - testdir.makepyfile( + pytester.makepyfile( """ import warnings import os @@ -3885,16 +3947,18 @@ def test_func(m1): pass """ ) - items, _ = testdir.inline_genitems() + items, _ = pytester.inline_genitems() request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "m1 f1".split() - def test_func_closure_with_native_fixtures(self, testdir, monkeypatch) -> None: + def test_func_closure_with_native_fixtures( + self, pytester: Pytester, monkeypatch: 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. """ monkeypatch.setattr(pytest, "FIXTURE_ORDER", [], raising=False) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -3931,19 +3995,19 @@ def f2(): def test_foo(f1, p1, m1, f2, s1): pass """ ) - items, _ = testdir.inline_genitems() + items, _ = pytester.inline_genitems() 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() ) - testdir.runpytest() + pytester.runpytest() # actual fixture execution differs: dependent fixtures must be created first ("my_tmpdir") 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): - testdir.makepyfile( + def test_func_closure_module(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -3957,15 +4021,15 @@ def test_func(f1, m1): pass """ ) - items, _ = testdir.inline_genitems() + items, _ = pytester.inline_genitems() request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "m1 f1".split() - def test_func_closure_scopes_reordered(self, testdir): + def test_func_closure_scopes_reordered(self, pytester: Pytester) -> None: """Test ensures that fixtures are ordered by scope regardless of the order of the parameters, although fixtures of same scope keep the declared order """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -3990,13 +4054,15 @@ def test_func(self, f2, f1, c1, m1, s1): pass """ ) - items, _ = testdir.inline_genitems() + items, _ = pytester.inline_genitems() 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): + def test_func_closure_same_scope_closer_root_first( + self, pytester: Pytester + ) -> None: """Auto-use fixtures of same scope are ordered by closer-to-root first""" - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -4004,7 +4070,7 @@ def test_func_closure_same_scope_closer_root_first(self, testdir): def m_conf(): pass """ ) - testdir.makepyfile( + pytester.makepyfile( **{ "sub/conftest.py": """ import pytest @@ -4030,13 +4096,13 @@ def test_func(m_test, f1): """, } ) - items, _ = testdir.inline_genitems() + items, _ = pytester.inline_genitems() 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): + def test_func_closure_all_scopes_complex(self, pytester: Pytester) -> None: """Complex test involving all scopes and mixing autouse with normal fixtures""" - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -4047,8 +4113,8 @@ def s1(): pass def p1(): pass """ ) - testdir.makepyfile(**{"__init__.py": ""}) - testdir.makepyfile( + pytester.makepyfile(**{"__init__.py": ""}) + pytester.makepyfile( """ import pytest @@ -4074,11 +4140,11 @@ def test_func(self, f2, f1, m2): pass """ ) - items, _ = testdir.inline_genitems() + items, _ = pytester.inline_genitems() request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split() - def test_multiple_packages(self, testdir): + def test_multiple_packages(self, pytester: Pytester) -> None: """Complex test involving multiple package fixtures. Make sure teardowns are executed in order. . @@ -4093,11 +4159,12 @@ def test_multiple_packages(self, testdir): ├── conftest.py └── test_2.py """ - root = testdir.mkdir("root") - root.join("__init__.py").write("values = []") - sub1 = root.mkdir("sub1") - sub1.ensure("__init__.py") - sub1.join("conftest.py").write( + root = pytester.mkdir("root") + root.joinpath("__init__.py").write_text("values = []") + sub1 = root.joinpath("sub1") + sub1.mkdir() + sub1.joinpath("__init__.py").touch() + sub1.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -4110,7 +4177,7 @@ def fix(): """ ) ) - sub1.join("test_1.py").write( + sub1.joinpath("test_1.py").write_text( textwrap.dedent( """\ from .. import values @@ -4119,9 +4186,10 @@ def test_1(fix): """ ) ) - sub2 = root.mkdir("sub2") - sub2.ensure("__init__.py") - sub2.join("conftest.py").write( + sub2 = root.joinpath("sub2") + sub2.mkdir() + sub2.joinpath("__init__.py").touch() + sub2.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -4134,7 +4202,7 @@ def fix(): """ ) ) - sub2.join("test_2.py").write( + sub2.joinpath("test_2.py").write_text( textwrap.dedent( """\ from .. import values @@ -4143,14 +4211,14 @@ def test_2(fix): """ ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_class_fixture_self_instance(self, testdir): + def test_class_fixture_self_instance(self, pytester: Pytester) -> None: """Check that plugin classes which implement fixtures receive the plugin instance as self (see #2270). """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -4168,14 +4236,14 @@ def myfix(self): """ ) - testdir.makepyfile( + pytester.makepyfile( """ class TestClass(object): def test_1(self, myfix): assert myfix == 1 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -4190,9 +4258,9 @@ def fix(): assert fix() == 1 -def test_fixture_param_shadowing(testdir): +def test_fixture_param_shadowing(pytester: Pytester) -> None: """Parametrized arguments would be shadowed if a fixture with the same name also exists (#5036)""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -4225,7 +4293,7 @@ def test_indirect(arg2): """ ) # Only one test should have run - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.assert_outcomes(passed=4) result.stdout.fnmatch_lines(["*::test_direct[[]1[]]*"]) result.stdout.fnmatch_lines(["*::test_normal_fixture[[]a[]]*"]) @@ -4233,9 +4301,9 @@ def test_indirect(arg2): result.stdout.fnmatch_lines(["*::test_indirect[[]1[]]*"]) -def test_fixture_named_request(testdir): - testdir.copy_example("fixtures/test_fixture_named_request.py") - result = testdir.runpytest() +def test_fixture_named_request(pytester: Pytester) -> None: + pytester.copy_example("fixtures/test_fixture_named_request.py") + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*'request' is a reserved word for fixtures, use another name:", @@ -4244,9 +4312,9 @@ def test_fixture_named_request(testdir): ) -def test_indirect_fixture_does_not_break_scope(testdir): +def test_indirect_fixture_does_not_break_scope(pytester: Pytester) -> None: """Ensure that fixture scope is respected when using indirect fixtures (#570)""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest instantiated = [] @@ -4291,14 +4359,14 @@ def test_check_fixture_instantiations(): ] """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=7) -def test_fixture_parametrization_nparray(testdir): +def test_fixture_parametrization_nparray(pytester: Pytester) -> None: pytest.importorskip("numpy") - testdir.makepyfile( + pytester.makepyfile( """ from numpy import linspace from pytest import fixture @@ -4311,18 +4379,18 @@ def test_bug(value): assert value == value """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=10) -def test_fixture_arg_ordering(testdir): +def test_fixture_arg_ordering(pytester: Pytester) -> None: """ This test describes how fixtures in the same scope but without explicit dependencies between them are created. While users should make dependencies explicit, often they rely on this order, so this test exists to catch regressions in this regard. See #6540 and #6492. """ - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import pytest @@ -4346,12 +4414,12 @@ def test_suffix(fix_combined): assert suffixes == ["fix_1", "fix_2", "fix_3", "fix_4", "fix_5"] """ ) - result = testdir.runpytest("-vv", str(p1)) + result = pytester.runpytest("-vv", str(p1)) assert result.ret == 0 -def test_yield_fixture_with_no_value(testdir): - testdir.makepyfile( +def test_yield_fixture_with_no_value(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(name='custom') @@ -4364,7 +4432,7 @@ def test_fixt(custom): """ ) expected = "E ValueError: custom did not yield a value" - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(errors=1) result.stdout.fnmatch_lines([expected]) assert result.ret == ExitCode.TESTS_FAILED diff --git a/testing/python/integration.py b/testing/python/integration.py index f006e5ed4ee..5dce6bdca28 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -3,13 +3,14 @@ import pytest from _pytest import runner from _pytest._code import getfslineno +from _pytest.pytester import Pytester class TestOEJSKITSpecials: def test_funcarg_non_pycollectobj( - self, testdir, recwarn + self, pytester: Pytester, recwarn ) -> None: # rough jstests usage - testdir.makeconftest( + pytester.makeconftest( """ import pytest def pytest_pycollect_makeitem(collector, name, obj): @@ -20,7 +21,7 @@ def reportinfo(self): return self.fspath, 3, "xyz" """ ) - modcol = testdir.getmodulecol( + modcol = pytester.getmodulecol( """ import pytest @pytest.fixture @@ -39,8 +40,10 @@ class MyClass(object): pytest._fillfuncargs(clscol) assert clscol.funcargs["arg1"] == 42 - def test_autouse_fixture(self, testdir, recwarn) -> None: # rough jstests usage - testdir.makeconftest( + def test_autouse_fixture( + self, pytester: Pytester, recwarn + ) -> None: # rough jstests usage + pytester.makeconftest( """ import pytest def pytest_pycollect_makeitem(collector, name, obj): @@ -51,7 +54,7 @@ def reportinfo(self): return self.fspath, 3, "xyz" """ ) - modcol = testdir.getmodulecol( + modcol = pytester.getmodulecol( """ import pytest @pytest.fixture(autouse=True) @@ -125,8 +128,8 @@ def f(x, y, z): values = getfuncargnames(f) assert values == ("y", "z") - def test_unittest_mock(self, testdir): - testdir.makepyfile( + def test_unittest_mock(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import unittest.mock class T(unittest.TestCase): @@ -137,11 +140,11 @@ def test_hello(self, abspath): abspath.assert_any_call("hello") """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_unittest_mock_and_fixture(self, testdir): - testdir.makepyfile( + def test_unittest_mock_and_fixture(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import os.path import unittest.mock @@ -158,12 +161,12 @@ def test_hello(inject_me): os.path.abspath("hello") """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_unittest_mock_and_pypi_mock(self, testdir): + def test_unittest_mock_and_pypi_mock(self, pytester: Pytester) -> None: pytest.importorskip("mock", "1.0.1") - testdir.makepyfile( + pytester.makepyfile( """ import mock import unittest.mock @@ -181,15 +184,15 @@ def test_hello_mock(self, abspath): abspath.assert_any_call("hello") """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_mock_sentinel_check_against_numpy_like(self, testdir): + def test_mock_sentinel_check_against_numpy_like(self, pytester: Pytester) -> None: """Ensure our function that detects mock arguments compares against sentinels using identity to circumvent objects which can't be compared with equality against others in a truth context, like with numpy arrays (#5606). """ - testdir.makepyfile( + pytester.makepyfile( dummy=""" class NumpyLike: def __init__(self, value): @@ -199,7 +202,7 @@ def __eq__(self, other): FOO = NumpyLike(10) """ ) - testdir.makepyfile( + pytester.makepyfile( """ from unittest.mock import patch import dummy @@ -209,12 +212,12 @@ def test_hello(self): assert dummy.FOO.value == 50 """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_mock(self, testdir): + def test_mock(self, pytester: Pytester) -> None: pytest.importorskip("mock", "1.0.1") - testdir.makepyfile( + pytester.makepyfile( """ import os import unittest @@ -237,7 +240,7 @@ def test_someting(normpath, abspath, tmpdir): assert os.path.basename("123") == "mock_basename" """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) calls = reprec.getcalls("pytest_runtest_logreport") funcnames = [ @@ -245,9 +248,9 @@ def test_someting(normpath, abspath, tmpdir): ] assert funcnames == ["T.test_hello", "test_someting"] - def test_mock_sorting(self, testdir): + def test_mock_sorting(self, pytester: Pytester) -> None: pytest.importorskip("mock", "1.0.1") - testdir.makepyfile( + pytester.makepyfile( """ import os import mock @@ -263,15 +266,15 @@ def test_three(abspath): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() calls = reprec.getreports("pytest_runtest_logreport") calls = [x for x in calls if x.when == "call"] names = [x.nodeid.split("::")[-1] for x in calls] assert names == ["test_one", "test_two", "test_three"] - def test_mock_double_patch_issue473(self, testdir): + def test_mock_double_patch_issue473(self, pytester: Pytester) -> None: pytest.importorskip("mock", "1.0.1") - testdir.makepyfile( + pytester.makepyfile( """ from mock import patch from pytest import mark @@ -284,13 +287,13 @@ def test_simple_thing(self, mock_path, mock_getcwd): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) class TestReRunTests: - def test_rerun(self, testdir): - testdir.makeconftest( + def test_rerun(self, pytester: Pytester) -> None: + pytester.makeconftest( """ from _pytest.runner import runtestprotocol def pytest_runtest_protocol(item, nextitem): @@ -298,7 +301,7 @@ def pytest_runtest_protocol(item, nextitem): runtestprotocol(item, log=True, nextitem=nextitem) """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest count = 0 @@ -314,7 +317,7 @@ def test_fix(fix): pass """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") result.stdout.fnmatch_lines( """ *fix count 0* @@ -336,21 +339,21 @@ def test_pytestconfig_is_session_scoped() -> None: class TestNoselikeTestAttribute: - def test_module_with_global_test(self, testdir): - testdir.makepyfile( + def test_module_with_global_test(self, pytester: Pytester) -> None: + pytester.makepyfile( """ __test__ = False def test_hello(): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() assert not reprec.getfailedcollections() calls = reprec.getreports("pytest_runtest_logreport") assert not calls - def test_class_and_method(self, testdir): - testdir.makepyfile( + def test_class_and_method(self, pytester: Pytester) -> None: + pytester.makepyfile( """ __test__ = True def test_func(): @@ -363,13 +366,13 @@ 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_unittest_class(self, testdir): - testdir.makepyfile( + def test_unittest_class(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import unittest class TC(unittest.TestCase): @@ -381,20 +384,20 @@ def test_2(self): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() assert not reprec.getfailedcollections() call = reprec.getcalls("pytest_collection_modifyitems")[0] assert len(call.items) == 1 assert call.items[0].cls.__name__ == "TC" - def test_class_with_nasty_getattr(self, testdir): + def test_class_with_nasty_getattr(self, pytester: Pytester) -> None: """Make sure we handle classes with a custom nasty __getattr__ right. With a custom __getattr__ which e.g. returns a function (like with a RPC wrapper), we shouldn't assume this meant "__test__ = True". """ # https://github.com/pytest-dev/pytest/issues/1204 - testdir.makepyfile( + pytester.makepyfile( """ class MetaModel(type): @@ -413,7 +416,7 @@ def test_blah(self): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() assert not reprec.getfailedcollections() call = reprec.getcalls("pytest_collection_modifyitems")[0] assert not call.items @@ -422,8 +425,8 @@ def test_blah(self): class TestParameterize: """#351""" - def test_idfn_marker(self, testdir): - testdir.makepyfile( + def test_idfn_marker(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -440,11 +443,11 @@ def test_params(a, b): pass """ ) - res = testdir.runpytest("--collect-only") + res = pytester.runpytest("--collect-only") res.stdout.fnmatch_lines(["*spam-2*", "*ham-2*"]) - def test_idfn_fixture(self, testdir): - testdir.makepyfile( + def test_idfn_fixture(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -468,5 +471,5 @@ def test_params(a, b): pass """ ) - res = testdir.runpytest("--collect-only") + res = pytester.runpytest("--collect-only") res.stdout.fnmatch_lines(["*spam-2*", "*ham-2*"]) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 676f1d988bc..c50ea53d255 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -23,7 +23,7 @@ from _pytest.compat import getfuncargnames from _pytest.compat import NOTSET from _pytest.outcomes import fail -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester from _pytest.python import _idval from _pytest.python import idmaker @@ -123,7 +123,7 @@ def func(x): ): metafunc.parametrize("x", [1], scope="doggy") # type: ignore[arg-type] - def test_parametrize_request_name(self, testdir: Testdir) -> None: + def test_parametrize_request_name(self, pytester: Pytester) -> None: """Show proper error when 'request' is used as a parameter name in parametrize (#6183)""" def func(request): @@ -550,12 +550,12 @@ def getini(self, name): ) assert result == [expected] - def test_parametrize_ids_exception(self, testdir: Testdir) -> None: + def test_parametrize_ids_exception(self, pytester: Pytester) -> None: """ - :param testdir: the instance of Testdir class, a temporary + :param pytester: the instance of Pytester class, a temporary test directory. """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -567,7 +567,7 @@ def test_foo(arg): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*Exception: bad ids", @@ -575,8 +575,8 @@ def test_foo(arg): ] ) - def test_parametrize_ids_returns_non_string(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_ids_returns_non_string(self, pytester: Pytester) -> None: + pytester.makepyfile( """\ import pytest @@ -592,7 +592,7 @@ def test_int(arg): assert arg """ ) - result = testdir.runpytest("-vv", "-s") + result = pytester.runpytest("-vv", "-s") result.stdout.fnmatch_lines( [ "test_parametrize_ids_returns_non_string.py::test[arg0] PASSED", @@ -682,7 +682,7 @@ def func(x, y): ): metafunc.parametrize("x, y", [("a", "b")], indirect={}) # type: ignore[arg-type] - def test_parametrize_indirect_list_functional(self, testdir: Testdir) -> None: + def test_parametrize_indirect_list_functional(self, pytester: Pytester) -> None: """ #714 Test parametrization with 'indirect' parameter applied on @@ -690,10 +690,10 @@ def test_parametrize_indirect_list_functional(self, testdir: Testdir) -> None: be used directly rather than being passed to the fixture y. - :param testdir: the instance of Testdir class, a temporary + :param pytester: the instance of Pytester class, a temporary test directory. """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.fixture(scope='function') @@ -708,7 +708,7 @@ def test_simple(x,y): assert len(y) == 1 """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines(["*test_simple*a-b*", "*1 passed*"]) def test_parametrize_indirect_list_error(self) -> None: @@ -722,7 +722,7 @@ def func(x, y): metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "z"]) def test_parametrize_uses_no_fixture_error_indirect_false( - self, testdir: Testdir + self, pytester: Pytester ) -> None: """The 'uses no fixture' error tells the user at collection time that the parametrize data they've set up doesn't correspond to the @@ -731,7 +731,7 @@ def test_parametrize_uses_no_fixture_error_indirect_false( #714 """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -740,14 +740,14 @@ def test_simple(x): assert len(x) == 3 """ ) - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no argument 'y'*"]) def test_parametrize_uses_no_fixture_error_indirect_true( - self, testdir: Testdir + self, pytester: Pytester ) -> None: """#714""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.fixture(scope='function') @@ -762,14 +762,14 @@ def test_simple(x): assert len(x) == 3 """ ) - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no fixture 'y'*"]) def test_parametrize_indirect_uses_no_fixture_error_indirect_string( - self, testdir: Testdir + self, pytester: Pytester ) -> None: """#714""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.fixture(scope='function') @@ -781,14 +781,14 @@ def test_simple(x): assert len(x) == 3 """ ) - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no fixture 'y'*"]) def test_parametrize_indirect_uses_no_fixture_error_indirect_list( - self, testdir: Testdir + self, pytester: Pytester ) -> None: """#714""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.fixture(scope='function') @@ -800,12 +800,14 @@ def test_simple(x): assert len(x) == 3 """ ) - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no fixture 'y'*"]) - def test_parametrize_argument_not_in_indirect_list(self, testdir: Testdir) -> None: + def test_parametrize_argument_not_in_indirect_list( + self, pytester: Pytester + ) -> None: """#714""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.fixture(scope='function') @@ -817,13 +819,13 @@ def test_simple(x): assert len(x) == 3 """ ) - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no argument 'y'*"]) def test_parametrize_gives_indicative_error_on_function_with_default_argument( - self, testdir + self, pytester: Pytester ) -> None: - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -832,13 +834,13 @@ def test_simple(x, y=1): assert len(x) == 1 """ ) - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines( ["*already takes an argument 'y' with a default value"] ) - def test_parametrize_functional(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_functional(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest def pytest_generate_tests(metafunc): @@ -853,7 +855,7 @@ def test_simple(x,y): assert y == 2 """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( ["*test_simple*1-2*", "*test_simple*2-2*", "*2 passed*"] ) @@ -884,8 +886,8 @@ def test_parametrize_twoargs(self) -> None: assert metafunc._calls[1].funcargs == dict(x=3, y=4) assert metafunc._calls[1].id == "3-4" - def test_parametrize_multiple_times(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_multiple_times(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest pytestmark = pytest.mark.parametrize("x", [1,2]) @@ -897,12 +899,12 @@ def test_meth(self, x, y): assert 0, x """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 1 result.assert_outcomes(failed=6) - def test_parametrize_CSV(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_CSV(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.parametrize("x, y,", [(1,2), (2,3)]) @@ -910,11 +912,11 @@ def test_func(x, y): assert x+1 == y """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) - def test_parametrize_class_scenarios(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_class_scenarios(self, pytester: Pytester) -> None: + pytester.makepyfile( """ # same as doc/en/example/parametrize scenario example def pytest_generate_tests(metafunc): @@ -941,7 +943,7 @@ def test_3(self, arg, arg2): pass """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") assert result.ret == 0 result.stdout.fnmatch_lines( """ @@ -978,8 +980,8 @@ def function4(arg1, *args, **kwargs): class TestMetafuncFunctional: - def test_attributes(self, testdir: Testdir) -> None: - p = testdir.makepyfile( + def test_attributes(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ # assumes that generate/provide runs in the same process import sys, pytest @@ -1005,11 +1007,11 @@ def test_method(self, metafunc, pytestconfig): assert metafunc.cls == TestClass """ ) - result = testdir.runpytest(p, "-v") + result = pytester.runpytest(p, "-v") result.assert_outcomes(passed=2) - def test_two_functions(self, testdir: Testdir) -> None: - p = testdir.makepyfile( + def test_two_functions(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def pytest_generate_tests(metafunc): metafunc.parametrize('arg1', [10, 20], ids=['0', '1']) @@ -1021,7 +1023,7 @@ def test_func2(arg1): assert arg1 in (10, 20) """ ) - result = testdir.runpytest("-v", p) + result = pytester.runpytest("-v", p) result.stdout.fnmatch_lines( [ "*test_func1*0*PASS*", @@ -1032,8 +1034,8 @@ def test_func2(arg1): ] ) - def test_noself_in_method(self, testdir: Testdir) -> None: - p = testdir.makepyfile( + def test_noself_in_method(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def pytest_generate_tests(metafunc): assert 'xyz' not in metafunc.fixturenames @@ -1043,11 +1045,11 @@ def test_hello(xyz): pass """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.assert_outcomes(passed=1) - def test_generate_tests_in_class(self, testdir: Testdir) -> None: - p = testdir.makepyfile( + def test_generate_tests_in_class(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ class TestClass(object): def pytest_generate_tests(self, metafunc): @@ -1057,11 +1059,11 @@ def test_myfunc(self, hello): assert hello == "world" """ ) - result = testdir.runpytest("-v", p) + result = pytester.runpytest("-v", p) result.stdout.fnmatch_lines(["*test_myfunc*hello*PASS*", "*1 passed*"]) - def test_two_functions_not_same_instance(self, testdir: Testdir) -> None: - p = testdir.makepyfile( + def test_two_functions_not_same_instance(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def pytest_generate_tests(metafunc): metafunc.parametrize('arg1', [10, 20], ids=["0", "1"]) @@ -1072,13 +1074,13 @@ def test_func(self, arg1): self.x = 1 """ ) - result = testdir.runpytest("-v", p) + result = pytester.runpytest("-v", p) result.stdout.fnmatch_lines( ["*test_func*0*PASS*", "*test_func*1*PASS*", "*2 pass*"] ) - def test_issue28_setup_method_in_generate_tests(self, testdir: Testdir) -> None: - p = testdir.makepyfile( + def test_issue28_setup_method_in_generate_tests(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def pytest_generate_tests(metafunc): metafunc.parametrize('arg1', [1]) @@ -1090,11 +1092,11 @@ def setup_method(self, func): self.val = 1 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.assert_outcomes(passed=1) - def test_parametrize_functional2(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_functional2(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def pytest_generate_tests(metafunc): metafunc.parametrize("arg1", [1,2]) @@ -1103,13 +1105,13 @@ def test_hello(arg1, arg2): assert 0, (arg1, arg2) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( ["*(1, 4)*", "*(1, 5)*", "*(2, 4)*", "*(2, 5)*", "*4 failed*"] ) - def test_parametrize_and_inner_getfixturevalue(self, testdir: Testdir) -> None: - p = testdir.makepyfile( + def test_parametrize_and_inner_getfixturevalue(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def pytest_generate_tests(metafunc): metafunc.parametrize("arg1", [1], indirect=True) @@ -1129,11 +1131,11 @@ def test_func1(arg1, arg2): assert arg1 == 11 """ ) - result = testdir.runpytest("-v", p) + result = pytester.runpytest("-v", p) result.stdout.fnmatch_lines(["*test_func1*1*PASS*", "*1 passed*"]) - def test_parametrize_on_setup_arg(self, testdir: Testdir) -> None: - p = testdir.makepyfile( + def test_parametrize_on_setup_arg(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def pytest_generate_tests(metafunc): assert "arg1" in metafunc.fixturenames @@ -1152,17 +1154,17 @@ def test_func(arg2): assert arg2 == 10 """ ) - result = testdir.runpytest("-v", p) + result = pytester.runpytest("-v", p) result.stdout.fnmatch_lines(["*test_func*1*PASS*", "*1 passed*"]) - def test_parametrize_with_ids(self, testdir: Testdir) -> None: - testdir.makeini( + def test_parametrize_with_ids(self, pytester: Pytester) -> None: + pytester.makeini( """ [pytest] console_output_style=classic """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest def pytest_generate_tests(metafunc): @@ -1173,14 +1175,14 @@ def test_function(a, b): assert a == b """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") assert result.ret == 1 result.stdout.fnmatch_lines_random( ["*test_function*basic*PASSED", "*test_function*advanced*FAILED"] ) - def test_parametrize_without_ids(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_without_ids(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest def pytest_generate_tests(metafunc): @@ -1191,7 +1193,7 @@ def test_function(a, b): assert 1 """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( """ *test_function*1-b0* @@ -1199,8 +1201,8 @@ def test_function(a, b): """ ) - def test_parametrize_with_None_in_ids(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_with_None_in_ids(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest def pytest_generate_tests(metafunc): @@ -1211,7 +1213,7 @@ def test_function(a, b): assert a == b """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") assert result.ret == 1 result.stdout.fnmatch_lines_random( [ @@ -1221,9 +1223,9 @@ def test_function(a, b): ] ) - def test_fixture_parametrized_empty_ids(self, testdir: Testdir) -> None: + def test_fixture_parametrized_empty_ids(self, pytester: Pytester) -> None: """Fixtures parametrized with empty ids cause an internal error (#1849).""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1235,12 +1237,12 @@ def test_temp(temp): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 1 skipped *"]) - def test_parametrized_empty_ids(self, testdir: Testdir) -> None: + def test_parametrized_empty_ids(self, pytester: Pytester) -> None: """Tests parametrized with empty ids cause an internal error (#1849).""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1249,12 +1251,12 @@ def test_temp(temp): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 1 skipped *"]) - def test_parametrized_ids_invalid_type(self, testdir: Testdir) -> None: + def test_parametrized_ids_invalid_type(self, pytester: Pytester) -> None: """Test error with non-strings/non-ints, without generator (#1857).""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1263,7 +1265,7 @@ def test_ids_numbers(x,expected): assert x * 2 == expected """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "In test_ids_numbers: ids must be list of string/float/int/bool," @@ -1272,9 +1274,9 @@ def test_ids_numbers(x,expected): ) def test_parametrize_with_identical_ids_get_unique_names( - self, testdir: Testdir + self, pytester: Pytester ) -> None: - testdir.makepyfile( + pytester.makepyfile( """ import pytest def pytest_generate_tests(metafunc): @@ -1285,7 +1287,7 @@ def test_function(a, b): assert a == b """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") assert result.ret == 1 result.stdout.fnmatch_lines_random( ["*test_function*a0*PASSED*", "*test_function*a1*FAILED*"] @@ -1293,9 +1295,9 @@ def test_function(a, b): @pytest.mark.parametrize(("scope", "length"), [("module", 2), ("function", 4)]) def test_parametrize_scope_overrides( - self, testdir: Testdir, scope: str, length: int + self, pytester: Pytester, scope: str, length: int ) -> None: - testdir.makepyfile( + pytester.makepyfile( """ import pytest values = [] @@ -1316,11 +1318,11 @@ def test_checklength(): """ % (scope, length) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=5) - def test_parametrize_issue323(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_issue323(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1334,11 +1336,11 @@ def test_it2(foo): pass """ ) - reprec = testdir.inline_run("--collect-only") + reprec = pytester.inline_run("--collect-only") assert not reprec.getcalls("pytest_internalerror") - def test_usefixtures_seen_in_generate_tests(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_usefixtures_seen_in_generate_tests(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest def pytest_generate_tests(metafunc): @@ -1350,13 +1352,13 @@ def test_function(): pass """ ) - reprec = testdir.runpytest() + reprec = pytester.runpytest() reprec.assert_outcomes(passed=1) - def test_generate_tests_only_done_in_subdir(self, testdir: Testdir) -> None: - sub1 = testdir.mkpydir("sub1") - sub2 = testdir.mkpydir("sub2") - sub1.join("conftest.py").write( + def test_generate_tests_only_done_in_subdir(self, pytester: Pytester) -> None: + sub1 = pytester.mkpydir("sub1") + sub2 = pytester.mkpydir("sub2") + sub1.joinpath("conftest.py").write_text( textwrap.dedent( """\ def pytest_generate_tests(metafunc): @@ -1364,7 +1366,7 @@ def pytest_generate_tests(metafunc): """ ) ) - sub2.join("conftest.py").write( + sub2.joinpath("conftest.py").write_text( textwrap.dedent( """\ def pytest_generate_tests(metafunc): @@ -1372,13 +1374,13 @@ def pytest_generate_tests(metafunc): """ ) ) - sub1.join("test_in_sub1.py").write("def test_1(): pass") - sub2.join("test_in_sub2.py").write("def test_2(): pass") - result = testdir.runpytest("--keep-duplicates", "-v", "-s", sub1, sub2, sub1) + sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass") + sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass") + result = pytester.runpytest("--keep-duplicates", "-v", "-s", sub1, sub2, sub1) result.assert_outcomes(passed=3) - def test_generate_same_function_names_issue403(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_generate_same_function_names_issue403(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1392,12 +1394,12 @@ def test_foo(x): test_y = make_tests() """ ) - reprec = testdir.runpytest() + reprec = pytester.runpytest() reprec.assert_outcomes(passed=4) - def test_parametrize_misspelling(self, testdir: Testdir) -> None: + def test_parametrize_misspelling(self, pytester: Pytester) -> None: """#463""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1406,7 +1408,7 @@ def test_foo(x): pass """ ) - result = testdir.runpytest("--collectonly") + result = pytester.runpytest("--collectonly") result.stdout.fnmatch_lines( [ "collected 0 items / 1 error", @@ -1426,8 +1428,8 @@ class TestMetafuncFunctionalAuto: """Tests related to automatically find out the correct scope for parametrized tests (#1832).""" - def test_parametrize_auto_scope(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_auto_scope(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1445,11 +1447,11 @@ def test_2(animal): """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 3 passed *"]) - def test_parametrize_auto_scope_indirect(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_auto_scope_indirect(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1468,11 +1470,11 @@ def test_2(animal, echo): assert echo in (1, 2, 3) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 3 passed *"]) - def test_parametrize_auto_scope_override_fixture(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_auto_scope_override_fixture(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1485,11 +1487,11 @@ def test_1(animal): assert animal in ('dog', 'cat') """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 2 passed *"]) - def test_parametrize_all_indirects(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_all_indirects(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1512,11 +1514,11 @@ def test_2(animal, echo): assert echo in (1, 2, 3) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 3 passed *"]) def test_parametrize_some_arguments_auto_scope( - self, testdir: Testdir, monkeypatch + self, pytester: Pytester, monkeypatch ) -> None: """Integration test for (#3941)""" class_fix_setup: List[object] = [] @@ -1524,7 +1526,7 @@ def test_parametrize_some_arguments_auto_scope( func_fix_setup: List[object] = [] monkeypatch.setattr(sys, "func_fix_setup", func_fix_setup, raising=False) - testdir.makepyfile( + pytester.makepyfile( """ import pytest import sys @@ -1545,13 +1547,13 @@ def test_bar(self): pass """ ) - result = testdir.runpytest_inprocess() + result = pytester.runpytest_inprocess() result.stdout.fnmatch_lines(["* 4 passed in *"]) assert func_fix_setup == [True] * 4 assert class_fix_setup == [10, 20] - def test_parametrize_issue634(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_issue634(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1579,7 +1581,7 @@ def pytest_generate_tests(metafunc): metafunc.parametrize('foo', params, indirect=True) """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") output = result.stdout.str() assert output.count("preparing foo-2") == 1 assert output.count("preparing foo-3") == 1 @@ -1588,7 +1590,7 @@ def pytest_generate_tests(metafunc): class TestMarkersWithParametrization: """#308""" - def test_simple_mark(self, testdir: Testdir) -> None: + def test_simple_mark(self, pytester: Pytester) -> None: s = """ import pytest @@ -1601,7 +1603,7 @@ def test_simple_mark(self, testdir: Testdir) -> None: def test_increment(n, expected): assert n + 1 == expected """ - items = testdir.getitems(s) + items = pytester.getitems(s) assert len(items) == 3 for item in items: assert "foo" in item.keywords @@ -1609,7 +1611,7 @@ def test_increment(n, expected): assert "bar" in items[1].keywords assert "bar" not in items[2].keywords - def test_select_based_on_mark(self, testdir: Testdir) -> None: + def test_select_based_on_mark(self, pytester: Pytester) -> None: s = """ import pytest @@ -1621,14 +1623,14 @@ def test_select_based_on_mark(self, testdir: Testdir) -> None: def test_increment(n, expected): assert n + 1 == expected """ - testdir.makepyfile(s) - rec = testdir.inline_run("-m", "foo") + pytester.makepyfile(s) + rec = pytester.inline_run("-m", "foo") passed, skipped, fail = rec.listoutcomes() assert len(passed) == 1 assert len(skipped) == 0 assert len(fail) == 0 - def test_simple_xfail(self, testdir: Testdir) -> None: + def test_simple_xfail(self, pytester: Pytester) -> None: s = """ import pytest @@ -1640,12 +1642,12 @@ def test_simple_xfail(self, testdir: Testdir) -> None: def test_increment(n, expected): assert n + 1 == expected """ - testdir.makepyfile(s) - reprec = testdir.inline_run() + pytester.makepyfile(s) + reprec = pytester.inline_run() # xfail is skip?? reprec.assertoutcome(passed=2, skipped=1) - def test_simple_xfail_single_argname(self, testdir: Testdir) -> None: + def test_simple_xfail_single_argname(self, pytester: Pytester) -> None: s = """ import pytest @@ -1657,11 +1659,11 @@ def test_simple_xfail_single_argname(self, testdir: Testdir) -> None: def test_isEven(n): assert n % 2 == 0 """ - testdir.makepyfile(s) - reprec = testdir.inline_run() + pytester.makepyfile(s) + reprec = pytester.inline_run() reprec.assertoutcome(passed=2, skipped=1) - def test_xfail_with_arg(self, testdir: Testdir) -> None: + def test_xfail_with_arg(self, pytester: Pytester) -> None: s = """ import pytest @@ -1673,11 +1675,11 @@ def test_xfail_with_arg(self, testdir: Testdir) -> None: def test_increment(n, expected): assert n + 1 == expected """ - testdir.makepyfile(s) - reprec = testdir.inline_run() + pytester.makepyfile(s) + reprec = pytester.inline_run() reprec.assertoutcome(passed=2, skipped=1) - def test_xfail_with_kwarg(self, testdir: Testdir) -> None: + def test_xfail_with_kwarg(self, pytester: Pytester) -> None: s = """ import pytest @@ -1689,11 +1691,11 @@ def test_xfail_with_kwarg(self, testdir: Testdir) -> None: def test_increment(n, expected): assert n + 1 == expected """ - testdir.makepyfile(s) - reprec = testdir.inline_run() + pytester.makepyfile(s) + reprec = pytester.inline_run() reprec.assertoutcome(passed=2, skipped=1) - def test_xfail_with_arg_and_kwarg(self, testdir: Testdir) -> None: + def test_xfail_with_arg_and_kwarg(self, pytester: Pytester) -> None: s = """ import pytest @@ -1705,12 +1707,12 @@ def test_xfail_with_arg_and_kwarg(self, testdir: Testdir) -> None: def test_increment(n, expected): assert n + 1 == expected """ - testdir.makepyfile(s) - reprec = testdir.inline_run() + pytester.makepyfile(s) + reprec = pytester.inline_run() reprec.assertoutcome(passed=2, skipped=1) @pytest.mark.parametrize("strict", [True, False]) - def test_xfail_passing_is_xpass(self, testdir: Testdir, strict: bool) -> None: + def test_xfail_passing_is_xpass(self, pytester: Pytester, strict: bool) -> None: s = """ import pytest @@ -1726,12 +1728,12 @@ def test_increment(n, expected): """.format( strict=strict ) - testdir.makepyfile(s) - reprec = testdir.inline_run() + pytester.makepyfile(s) + reprec = pytester.inline_run() passed, failed = (2, 1) if strict else (3, 0) reprec.assertoutcome(passed=passed, failed=failed) - def test_parametrize_called_in_generate_tests(self, testdir: Testdir) -> None: + def test_parametrize_called_in_generate_tests(self, pytester: Pytester) -> None: s = """ import pytest @@ -1750,13 +1752,15 @@ def pytest_generate_tests(metafunc): def test_increment(n, expected): assert n + 1 == expected """ - testdir.makepyfile(s) - reprec = testdir.inline_run() + pytester.makepyfile(s) + reprec = pytester.inline_run() reprec.assertoutcome(passed=2, skipped=2) - def test_parametrize_ID_generation_string_int_works(self, testdir: Testdir) -> None: + def test_parametrize_ID_generation_string_int_works( + self, pytester: Pytester + ) -> None: """#290""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1769,11 +1773,11 @@ def test_limit(limit, myfixture): return """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=2) @pytest.mark.parametrize("strict", [True, False]) - def test_parametrize_marked_value(self, testdir: Testdir, strict: bool) -> None: + def test_parametrize_marked_value(self, pytester: Pytester, strict: bool) -> None: s = """ import pytest @@ -1792,19 +1796,19 @@ def test_increment(n, expected): """.format( strict=strict ) - testdir.makepyfile(s) - reprec = testdir.inline_run() + pytester.makepyfile(s) + reprec = pytester.inline_run() passed, failed = (0, 2) if strict else (2, 0) reprec.assertoutcome(passed=passed, failed=failed) - def test_pytest_make_parametrize_id(self, testdir: Testdir) -> None: - testdir.makeconftest( + def test_pytest_make_parametrize_id(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_make_parametrize_id(config, val): return str(val * 2) """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1813,17 +1817,17 @@ def test_func(x): pass """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines(["*test_func*0*PASS*", "*test_func*2*PASS*"]) - def test_pytest_make_parametrize_id_with_argname(self, testdir: Testdir) -> None: - testdir.makeconftest( + def test_pytest_make_parametrize_id_with_argname(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_make_parametrize_id(config, val, argname): return str(val * 2 if argname == 'x' else val * 10) """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1836,13 +1840,13 @@ def test_func_b(y): pass """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( ["*test_func_a*0*PASS*", "*test_func_a*2*PASS*", "*test_func_b*10*PASS*"] ) - def test_parametrize_positional_args(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_positional_args(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -1851,11 +1855,11 @@ def test_foo(a): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=1) - def test_parametrize_iterator(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_parametrize_iterator(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import itertools import pytest @@ -1877,7 +1881,7 @@ def test_converted_to_str(a, b): pass """ ) - result = testdir.runpytest("-vv", "-s") + result = pytester.runpytest("-vv", "-s") result.stdout.fnmatch_lines( [ "test_parametrize_iterator.py::test1[param0] PASSED", diff --git a/testing/python/raises.py b/testing/python/raises.py index 80634eebfbf..a3991adaef1 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -3,6 +3,7 @@ import pytest from _pytest.outcomes import Failed +from _pytest.pytester import Pytester class TestRaises: @@ -50,8 +51,8 @@ class E(Exception): pprint.pprint(excinfo) raise E() - def test_raises_as_contextmanager(self, testdir): - testdir.makepyfile( + def test_raises_as_contextmanager(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest import _pytest._code @@ -75,11 +76,11 @@ def test_raise_wrong_exception_passes_by(): 1/0 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) - def test_does_not_raise(self, testdir): - testdir.makepyfile( + def test_does_not_raise(self, pytester: Pytester) -> None: + pytester.makepyfile( """ from contextlib import contextmanager import pytest @@ -100,11 +101,11 @@ def test_division(example_input, expectation): assert (6 / example_input) is not None """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*4 passed*"]) - def test_does_not_raise_does_raise(self, testdir): - testdir.makepyfile( + def test_does_not_raise_does_raise(self, pytester: Pytester) -> None: + pytester.makepyfile( """ from contextlib import contextmanager import pytest @@ -123,7 +124,7 @@ def test_division(example_input, expectation): assert (6 / example_input) is not None """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 failed*"]) def test_noclass(self) -> None: diff --git a/testing/python/show_fixtures_per_test.py b/testing/python/show_fixtures_per_test.py index ef841819d09..2a15132738d 100644 --- a/testing/python/show_fixtures_per_test.py +++ b/testing/python/show_fixtures_per_test.py @@ -1,11 +1,14 @@ -def test_no_items_should_not_show_output(testdir): - result = testdir.runpytest("--fixtures-per-test") +from _pytest.pytester import Pytester + + +def test_no_items_should_not_show_output(pytester: Pytester) -> None: + result = pytester.runpytest("--fixtures-per-test") result.stdout.no_fnmatch_line("*fixtures used by*") assert result.ret == 0 -def test_fixtures_in_module(testdir): - p = testdir.makepyfile( +def test_fixtures_in_module(pytester: Pytester) -> None: + p = pytester.makepyfile( ''' import pytest @pytest.fixture @@ -19,7 +22,7 @@ def test_arg1(arg1): ''' ) - result = testdir.runpytest("--fixtures-per-test", p) + result = pytester.runpytest("--fixtures-per-test", p) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -33,8 +36,8 @@ def test_arg1(arg1): result.stdout.no_fnmatch_line("*_arg0*") -def test_fixtures_in_conftest(testdir): - testdir.makeconftest( +def test_fixtures_in_conftest(pytester: Pytester) -> None: + pytester.makeconftest( ''' import pytest @pytest.fixture @@ -50,7 +53,7 @@ def arg3(arg1, arg2): """ ''' ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ def test_arg2(arg2): pass @@ -58,7 +61,7 @@ def test_arg3(arg3): pass """ ) - result = testdir.runpytest("--fixtures-per-test", p) + result = pytester.runpytest("--fixtures-per-test", p) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -80,8 +83,8 @@ def test_arg3(arg3): ) -def test_should_show_fixtures_used_by_test(testdir): - testdir.makeconftest( +def test_should_show_fixtures_used_by_test(pytester: Pytester) -> None: + pytester.makeconftest( ''' import pytest @pytest.fixture @@ -92,7 +95,7 @@ def arg2(): """arg2 from conftest""" ''' ) - p = testdir.makepyfile( + p = pytester.makepyfile( ''' import pytest @pytest.fixture @@ -102,7 +105,7 @@ def test_args(arg1, arg2): pass ''' ) - result = testdir.runpytest("--fixtures-per-test", p) + result = pytester.runpytest("--fixtures-per-test", p) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -117,8 +120,8 @@ def test_args(arg1, arg2): ) -def test_verbose_include_private_fixtures_and_loc(testdir): - testdir.makeconftest( +def test_verbose_include_private_fixtures_and_loc(pytester: Pytester) -> None: + pytester.makeconftest( ''' import pytest @pytest.fixture @@ -129,7 +132,7 @@ def arg2(_arg1): """arg2 from conftest""" ''' ) - p = testdir.makepyfile( + p = pytester.makepyfile( ''' import pytest @pytest.fixture @@ -139,7 +142,7 @@ def test_args(arg2, arg3): pass ''' ) - result = testdir.runpytest("--fixtures-per-test", "-v", p) + result = pytester.runpytest("--fixtures-per-test", "-v", p) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -156,8 +159,8 @@ def test_args(arg2, arg3): ) -def test_doctest_items(testdir): - testdir.makepyfile( +def test_doctest_items(pytester: Pytester) -> None: + pytester.makepyfile( ''' def foo(): """ @@ -166,13 +169,13 @@ def foo(): """ ''' ) - testdir.maketxtfile( + pytester.maketxtfile( """ >>> 1 + 1 2 """ ) - result = testdir.runpytest( + result = pytester.runpytest( "--fixtures-per-test", "--doctest-modules", "--doctest-glob=*.txt", "-v" ) assert result.ret == 0 From 92b444a91494a51dfd4d8a127707e5ab4f42c517 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 03:07:27 +0000 Subject: [PATCH 0323/2846] build(deps): bump pytest-bdd in /testing/plugins_integration Bumps [pytest-bdd](https://github.com/pytest-dev/pytest-bdd) from 4.0.1 to 4.0.2. - [Release notes](https://github.com/pytest-dev/pytest-bdd/releases) - [Changelog](https://github.com/pytest-dev/pytest-bdd/blob/master/CHANGES.rst) - [Commits](https://github.com/pytest-dev/pytest-bdd/compare/4.0.1...4.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 d0ee9b571e2..19cc088db83 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,7 +1,7 @@ anyio[curio,trio]==2.0.2 django==3.1.4 pytest-asyncio==0.14.0 -pytest-bdd==4.0.1 +pytest-bdd==4.0.2 pytest-cov==2.10.1 pytest-django==4.1.0 pytest-flakes==4.0.3 From a09d8b15998b78d9e0bbad38879ccb1b00f95069 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 03:07:30 +0000 Subject: [PATCH 0324/2846] build(deps): bump pytest-html in /testing/plugins_integration Bumps [pytest-html](https://github.com/pytest-dev/pytest-html) from 3.1.0 to 3.1.1. - [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.1.0...v3.1.1) 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 d0ee9b571e2..92a006723ef 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.1.0 +pytest-html==3.1.1 pytest-mock==3.3.1 pytest-rerunfailures==9.1.1 pytest-sugar==0.9.4 From 2cb34a99cbf423c50b7b6592a54f80f68bb9fdc0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 14 Dec 2020 15:54:59 +0200 Subject: [PATCH 0325/2846] Some py.path.local -> pathlib.Path --- src/_pytest/assertion/rewrite.py | 5 ++- src/_pytest/config/argparsing.py | 15 +++++---- src/_pytest/fixtures.py | 15 ++++----- src/_pytest/main.py | 43 +++++++++++++------------ src/_pytest/monkeypatch.py | 13 ++------ src/_pytest/nodes.py | 10 ++++-- src/_pytest/pathlib.py | 6 ++-- testing/test_assertrewrite.py | 6 ++-- testing/test_main.py | 54 ++++++++++++++------------------ testing/test_nodes.py | 11 +++++-- 10 files changed, 84 insertions(+), 94 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 805d4c8b35b..a01be76b4d3 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -27,8 +27,6 @@ from typing import TYPE_CHECKING from typing import Union -import py - from _pytest._io.saferepr import saferepr from _pytest._version import version from _pytest.assertion import util @@ -37,6 +35,7 @@ ) from _pytest.config import Config from _pytest.main import Session +from _pytest.pathlib import absolutepath from _pytest.pathlib import fnmatch_ex from _pytest.store import StoreKey @@ -215,7 +214,7 @@ def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool: return True if self.session is not None: - if self.session.isinitpath(py.path.local(fn)): + if self.session.isinitpath(absolutepath(fn)): state.trace(f"matched test file (was specified on cmdline): {fn!r}") return True diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 9a481965526..5a09ea781e6 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -1,4 +1,5 @@ import argparse +import os import sys import warnings from gettext import gettext @@ -14,8 +15,6 @@ from typing import TYPE_CHECKING from typing import Union -import py - import _pytest._io from _pytest.compat import final from _pytest.config.exceptions import UsageError @@ -97,14 +96,14 @@ def addoption(self, *opts: str, **attrs: Any) -> None: def parse( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], namespace: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: from _pytest._argcomplete import try_argcomplete self.optparser = self._getparser() try_argcomplete(self.optparser) - strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] + strargs = [os.fspath(x) for x in args] return self.optparser.parse_args(strargs, namespace=namespace) def _getparser(self) -> "MyOptionParser": @@ -128,7 +127,7 @@ def _getparser(self) -> "MyOptionParser": def parse_setoption( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], option: argparse.Namespace, namespace: Optional[argparse.Namespace] = None, ) -> List[str]: @@ -139,7 +138,7 @@ def parse_setoption( def parse_known_args( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], namespace: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: """Parse and return a namespace object with known arguments at this point.""" @@ -147,13 +146,13 @@ def parse_known_args( def parse_known_and_unknown_args( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], namespace: Optional[argparse.Namespace] = None, ) -> Tuple[argparse.Namespace, List[str]]: """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] + strargs = [os.fspath(x) for x in args] return optparser.parse_known_args(strargs, namespace=namespace) def addini( diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 273bcafd393..c24ab7069cb 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -648,12 +648,13 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: if has_params: frame = inspect.stack()[3] frameinfo = inspect.getframeinfo(frame[0]) - source_path = py.path.local(frameinfo.filename) + source_path = absolutepath(frameinfo.filename) source_lineno = frameinfo.lineno - rel_source_path = source_path.relto(funcitem.config.rootdir) - if rel_source_path: - source_path_str = rel_source_path - else: + try: + source_path_str = str( + source_path.relative_to(funcitem.config.rootpath) + ) + except ValueError: source_path_str = str(source_path) msg = ( "The requested fixture has no parameter defined for test:\n" @@ -876,7 +877,7 @@ def formatrepr(self) -> "FixtureLookupErrorRepr": class FixtureLookupErrorRepr(TerminalRepr): def __init__( self, - filename: Union[str, py.path.local], + filename: Union[str, "os.PathLike[str]"], firstlineno: int, tblines: Sequence[str], errorstring: str, @@ -903,7 +904,7 @@ def toterminal(self, tw: TerminalWriter) -> None: f"{FormattedExcinfo.flow_marker} {line.strip()}", red=True, ) tw.line() - tw.line("%s:%d" % (self.filename, self.firstlineno + 1)) + tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1)) def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn": diff --git a/src/_pytest/main.py b/src/_pytest/main.py index eab3c9afd27..d536f9d8066 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -467,7 +467,7 @@ def __init__(self, config: Config) -> None: self.shouldfail: Union[bool, str] = False self.trace = config.trace.root.get("collection") self.startdir = config.invocation_dir - self._initialpaths: FrozenSet[py.path.local] = frozenset() + self._initialpaths: FrozenSet[Path] = frozenset() self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath) @@ -510,8 +510,8 @@ def pytest_runtest_logreport( pytest_collectreport = pytest_runtest_logreport - def isinitpath(self, path: py.path.local) -> bool: - return path in self._initialpaths + def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool: + return Path(path) in self._initialpaths def gethookproxy(self, fspath: "os.PathLike[str]"): # Check if we have the common case of running @@ -601,14 +601,14 @@ def perform_collect( self.trace.root.indent += 1 self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = [] - self._initial_parts: List[Tuple[py.path.local, List[str]]] = [] + self._initial_parts: List[Tuple[Path, List[str]]] = [] self.items: List[nodes.Item] = [] hook = self.config.hook items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items try: - initialpaths: List[py.path.local] = [] + initialpaths: List[Path] = [] for arg in args: fspath, parts = resolve_collection_argument( self.config.invocation_params.dir, @@ -669,13 +669,13 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # No point in finding packages when collecting doctests. if not self.config.getoption("doctestmodules", False): pm = self.config.pluginmanager - confcutdir = py.path.local(pm._confcutdir) if pm._confcutdir else None - for parent in reversed(argpath.parts()): - if confcutdir and confcutdir.relto(parent): + confcutdir = pm._confcutdir + for parent in (argpath, *argpath.parents): + if confcutdir and parent in confcutdir.parents: break - if parent.isdir(): - pkginit = parent.join("__init__.py") + if parent.is_dir(): + pkginit = py.path.local(parent / "__init__.py") if pkginit.isfile() and pkginit not in node_cache1: col = self._collectfile(pkginit, handle_dupes=False) if col: @@ -685,7 +685,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # 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): + if argpath.is_dir(): assert not names, "invalid arg {!r}".format((argpath, names)) seen_dirs: Set[py.path.local] = set() @@ -717,15 +717,16 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: node_cache2[key] = x yield x else: - assert argpath.check(file=1) + assert argpath.is_file() - if argpath in node_cache1: - col = node_cache1[argpath] + argpath_ = py.path.local(argpath) + if argpath_ in node_cache1: + col = node_cache1[argpath_] else: - collect_root = pkg_roots.get(argpath.dirname, self) - col = collect_root._collectfile(argpath, handle_dupes=False) + collect_root = pkg_roots.get(argpath_.dirname, self) + col = collect_root._collectfile(argpath_, handle_dupes=False) if col: - node_cache1[argpath] = col + node_cache1[argpath_] = col matching = [] work: List[ @@ -782,9 +783,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # 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 - ): + if argpath.name == "__init__.py" and isinstance(matching[0], Package): try: yield next(iter(matching[0].collect())) except StopIteration: @@ -833,7 +832,7 @@ def search_pypath(module_name: str) -> str: def resolve_collection_argument( invocation_path: Path, arg: str, *, as_pypath: bool = False -) -> Tuple[py.path.local, List[str]]: +) -> Tuple[Path, 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 @@ -875,4 +874,4 @@ def resolve_collection_argument( else "directory argument cannot contain :: selection parts: {arg}" ) raise UsageError(msg.format(arg=arg)) - return py.path.local(str(fspath)), parts + return fspath, parts diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index a052f693ac0..d012b8a535a 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -4,7 +4,6 @@ import sys import warnings from contextlib import contextmanager -from pathlib import Path from typing import Any from typing import Generator from typing import List @@ -325,20 +324,14 @@ def syspath_prepend(self, path) -> None: invalidate_caches() - def chdir(self, path) -> None: + def chdir(self, path: Union[str, "os.PathLike[str]"]) -> None: """Change the current working directory to the specified path. - Path can be a string or a py.path.local object. + Path can be a string or a path object. """ if self._cwd is None: self._cwd = os.getcwd() - if hasattr(path, "chdir"): - path.chdir() - elif isinstance(path, Path): - # Modern python uses the fspath protocol here LEGACY - os.chdir(str(path)) - else: - os.chdir(path) + os.chdir(path) def undo(self) -> None: """Undo previous changes. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 98bd581b96d..fee0770eb2b 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -480,10 +480,14 @@ def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: excinfo.traceback = ntraceback.filter() -def _check_initialpaths_for_relpath(session, fspath): +def _check_initialpaths_for_relpath( + session: "Session", fspath: py.path.local +) -> Optional[str]: for initial_path in session._initialpaths: - if fspath.common(initial_path) == initial_path: - return fspath.relto(initial_path) + initial_path_ = py.path.local(initial_path) + if fspath.common(initial_path_) == initial_path_: + return fspath.relto(initial_path_) + return None class FSCollector(Collector): diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 8875a28f84b..2e452eb1cc9 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -30,8 +30,6 @@ 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 @@ -456,7 +454,7 @@ class ImportPathMismatchError(ImportError): def import_path( - p: Union[str, py.path.local, Path], + p: Union[str, "os.PathLike[str]"], *, mode: Union[str, ImportMode] = ImportMode.prepend, ) -> ModuleType: @@ -482,7 +480,7 @@ def import_path( """ mode = ImportMode(mode) - path = Path(str(p)) + path = Path(p) if not path.exists(): raise ImportError(path) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 84d5276e729..ffe18260f90 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -17,8 +17,6 @@ from typing import Optional from typing import Set -import py - import _pytest._code import pytest from _pytest.assertion import util @@ -1311,7 +1309,7 @@ def hook( import importlib.machinery self.find_spec_calls: List[str] = [] - self.initial_paths: Set[py.path.local] = set() + self.initial_paths: Set[Path] = set() class StubSession: _initialpaths = self.initial_paths @@ -1346,7 +1344,7 @@ def fix(): return 1 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)) + self.initial_paths.add(foobar_path) # conftest files should always be rewritten assert hook.find_spec("conftest") is not None diff --git a/testing/test_main.py b/testing/test_main.py index 3e94668e82f..f45607abc30 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -4,13 +4,12 @@ from pathlib import Path 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 Pytester from _pytest.pytester import Testdir @@ -109,40 +108,37 @@ def test_validate_basetemp_integration(testdir): class TestResolveCollectionArgument: @pytest.fixture - 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() - pkg.join("test.py").ensure() - return testdir.tmpdir + def invocation_path(self, pytester: Pytester) -> Path: + pytester.syspathinsert(pytester.path / "src") + pytester.chdir() - @pytest.fixture - def invocation_path(self, invocation_dir: py.path.local) -> Path: - return Path(str(invocation_dir)) + pkg = pytester.path.joinpath("src/pkg") + pkg.mkdir(parents=True) + pkg.joinpath("__init__.py").touch() + pkg.joinpath("test.py").touch() + return pytester.path - def test_file(self, invocation_dir: py.path.local, invocation_path: Path) -> None: + def test_file(self, invocation_path: Path) -> None: """File and parts.""" assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == ( - invocation_dir / "src/pkg/test.py", + invocation_path / "src/pkg/test.py", [], ) assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == ( - invocation_dir / "src/pkg/test.py", + invocation_path / "src/pkg/test.py", [""], ) assert resolve_collection_argument( invocation_path, "src/pkg/test.py::foo::bar" - ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"]) + ) == (invocation_path / "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", ""]) + ) == (invocation_path / "src/pkg/test.py", ["foo", "bar", ""]) - def test_dir(self, invocation_dir: py.path.local, invocation_path: Path) -> None: + def test_dir(self, invocation_path: Path) -> None: """Directory and parts.""" assert resolve_collection_argument(invocation_path, "src/pkg") == ( - invocation_dir / "src/pkg", + invocation_path / "src/pkg", [], ) @@ -156,16 +152,16 @@ def test_dir(self, invocation_dir: py.path.local, invocation_path: Path) -> None ): resolve_collection_argument(invocation_path, "src/pkg::foo::bar") - def test_pypath(self, invocation_dir: py.path.local, invocation_path: Path) -> None: + def test_pypath(self, invocation_path: Path) -> None: """Dotted name and parts.""" assert resolve_collection_argument( invocation_path, "pkg.test", as_pypath=True - ) == (invocation_dir / "src/pkg/test.py", []) + ) == (invocation_path / "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"]) + ) == (invocation_path / "src/pkg/test.py", ["foo", "bar"]) assert resolve_collection_argument(invocation_path, "pkg", as_pypath=True) == ( - invocation_dir / "src/pkg", + invocation_path / "src/pkg", [], ) @@ -191,13 +187,11 @@ def test_does_not_exist(self, invocation_path: Path) -> None: ): resolve_collection_argument(invocation_path, "foobar", as_pypath=True) - def test_absolute_paths_are_resolved_correctly( - self, invocation_dir: py.path.local, invocation_path: Path - ) -> None: + def test_absolute_paths_are_resolved_correctly(self, invocation_path: Path) -> None: """Absolute paths resolve back to absolute paths.""" - full_path = str(invocation_dir / "src") + full_path = str(invocation_path / "src") assert resolve_collection_argument(invocation_path, full_path) == ( - py.path.local(os.path.abspath("src")), + Path(os.path.abspath("src")), [], ) @@ -206,7 +200,7 @@ def test_absolute_paths_are_resolved_correctly( drive, full_path_without_drive = os.path.splitdrive(full_path) assert resolve_collection_argument( invocation_path, full_path_without_drive - ) == (py.path.local(os.path.abspath("src")), []) + ) == (Path(os.path.abspath("src")), []) def test_module_full_path_without_drive(testdir): diff --git a/testing/test_nodes.py b/testing/test_nodes.py index f3824c57090..bae31f0a39c 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -1,3 +1,4 @@ +from typing import cast from typing import List from typing import Type @@ -73,17 +74,21 @@ def test__check_initialpaths_for_relpath() -> None: class FakeSession1: _initialpaths = [cwd] - assert nodes._check_initialpaths_for_relpath(FakeSession1, cwd) == "" + session = cast(pytest.Session, FakeSession1) + + assert nodes._check_initialpaths_for_relpath(session, cwd) == "" sub = cwd.join("file") class FakeSession2: _initialpaths = [cwd] - assert nodes._check_initialpaths_for_relpath(FakeSession2, sub) == "file" + session = cast(pytest.Session, FakeSession2) + + assert nodes._check_initialpaths_for_relpath(session, sub) == "file" outside = py.path.local("/outside") - assert nodes._check_initialpaths_for_relpath(FakeSession2, outside) is None + assert nodes._check_initialpaths_for_relpath(session, outside) is None def test_failure_with_changed_cwd(pytester: Pytester) -> None: From 592b32bd69cb43aace8cd5525fa0b3712ee767be Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 14 Dec 2020 18:16:14 +0200 Subject: [PATCH 0326/2846] hookspec: add pathlib.Path alternatives to py.path.local parameters in hooks As part of the ongoing migration for py.path to pathlib, make sure all hooks which take a py.path.local also take an equivalent pathlib.Path. --- changelog/8144.feature.rst | 7 +++++++ src/_pytest/hookspec.py | 42 ++++++++++++++++++++++++++++++++------ src/_pytest/main.py | 14 ++++++++----- src/_pytest/python.py | 25 +++++++++++++++-------- src/_pytest/terminal.py | 7 +++++-- testing/test_terminal.py | 6 +++--- 6 files changed, 76 insertions(+), 25 deletions(-) create mode 100644 changelog/8144.feature.rst diff --git a/changelog/8144.feature.rst b/changelog/8144.feature.rst new file mode 100644 index 00000000000..01f40e21521 --- /dev/null +++ b/changelog/8144.feature.rst @@ -0,0 +1,7 @@ +The following hooks now receive an additional ``pathlib.Path`` argument, equivalent to an existing ``py.path.local`` argument: + +- :func:`pytest_ignore_collect <_pytest.hookspec.pytest_ignore_collect>` - The ``fspath`` parameter (equivalent to existing ``path`` parameter). +- :func:`pytest_collect_file <_pytest.hookspec.pytest_collect_file>` - The ``fspath`` parameter (equivalent to existing ``path`` parameter). +- :func:`pytest_pycollect_makemodule <_pytest.hookspec.pytest_pycollect_makemodule>` - The ``fspath`` parameter (equivalent to existing ``path`` parameter). +- :func:`pytest_report_header <_pytest.hookspec.pytest_report_header>` - The ``startpath`` parameter (equivalent to existing ``startdir`` parameter). +- :func:`pytest_report_collectionfinish <_pytest.hookspec.pytest_report_collectionfinish>` - The ``startpath`` parameter (equivalent to existing ``startdir`` parameter). diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index e499b742c7e..22bebf5b783 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,5 +1,6 @@ """Hook specifications for pytest plugins which are invoked by pytest itself and by builtin plugins.""" +from pathlib import Path from typing import Any from typing import Dict from typing import List @@ -261,7 +262,9 @@ def pytest_collection_finish(session: "Session") -> None: @hookspec(firstresult=True) -def pytest_ignore_collect(path: py.path.local, config: "Config") -> Optional[bool]: +def pytest_ignore_collect( + fspath: Path, 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 @@ -269,19 +272,29 @@ def pytest_ignore_collect(path: py.path.local, config: "Config") -> Optional[boo Stops at first non-None result, see :ref:`firstresult`. + :param pathlib.Path fspath: The path to analyze. :param py.path.local path: The path to analyze. :param _pytest.config.Config config: The pytest config object. + + .. versionchanged:: 6.3.0 + The ``fspath`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. """ def pytest_collect_file( - path: py.path.local, parent: "Collector" + fspath: Path, path: py.path.local, parent: "Collector" ) -> "Optional[Collector]": """Create a Collector for the given path, or None if not relevant. The new node needs to have the specified ``parent`` as a parent. + :param pathlib.Path fspath: The path to analyze. :param py.path.local path: The path to collect. + + .. versionchanged:: 6.3.0 + The ``fspath`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. """ @@ -321,7 +334,9 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor @hookspec(firstresult=True) -def pytest_pycollect_makemodule(path: py.path.local, parent) -> Optional["Module"]: +def pytest_pycollect_makemodule( + fspath: Path, 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. @@ -330,7 +345,12 @@ def pytest_pycollect_makemodule(path: py.path.local, parent) -> Optional["Module Stops at first non-None result, see :ref:`firstresult`. - :param py.path.local path: The path of module to collect. + :param pathlib.Path fspath: The path of the module to collect. + :param py.path.local path: The path of the module to collect. + + .. versionchanged:: 6.3.0 + The ``fspath`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. """ @@ -653,11 +673,12 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No def pytest_report_header( - config: "Config", startdir: py.path.local + config: "Config", startpath: Path, 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: The pytest config object. + :param Path startpath: The starting dir. :param py.path.local startdir: The starting dir. .. note:: @@ -672,11 +693,15 @@ def pytest_report_header( This function should be implemented only in plugins or ``conftest.py`` files situated at the tests root directory due to how pytest :ref:`discovers plugins during startup `. + + .. versionchanged:: 6.3.0 + The ``startpath`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``startdir`` parameter. """ def pytest_report_collectionfinish( - config: "Config", startdir: py.path.local, items: Sequence["Item"], + config: "Config", startpath: Path, startdir: py.path.local, items: Sequence["Item"], ) -> Union[str, List[str]]: """Return a string or list of strings to be displayed after collection has finished successfully. @@ -686,6 +711,7 @@ def pytest_report_collectionfinish( .. versionadded:: 3.2 :param _pytest.config.Config config: The pytest config object. + :param Path startpath: The starting path. :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. @@ -695,6 +721,10 @@ def pytest_report_collectionfinish( ran before it. If you want to have your line(s) displayed first, use :ref:`trylast=True `. + + .. versionchanged:: 6.3.0 + The ``startpath`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``startdir`` parameter. """ diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d536f9d8066..e7c31ecc1d5 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -532,9 +532,10 @@ def gethookproxy(self, fspath: "os.PathLike[str]"): 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): + fspath = Path(direntry.path) + path = py.path.local(fspath) + ihook = self.gethookproxy(fspath.parent) + if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): return False norecursepatterns = self.config.getini("norecursedirs") if any(path.check(fnmatch=pat) for pat in norecursepatterns): @@ -544,6 +545,7 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: def _collectfile( self, path: py.path.local, handle_dupes: bool = True ) -> Sequence[nodes.Collector]: + fspath = Path(path) assert ( path.isfile() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( @@ -551,7 +553,9 @@ def _collectfile( ) ihook = self.gethookproxy(path) if not self.isinitpath(path): - if ihook.pytest_ignore_collect(path=path, config=self.config): + if ihook.pytest_ignore_collect( + fspath=fspath, path=path, config=self.config + ): return () if handle_dupes: @@ -563,7 +567,7 @@ def _collectfile( else: duplicate_paths.add(path) - return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] + return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return] @overload def perform_collect( diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 407f924a5f1..18e449b9361 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -10,6 +10,7 @@ from collections import Counter from collections import defaultdict from functools import partial +from pathlib import Path from typing import Any from typing import Callable from typing import Dict @@ -187,17 +188,19 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: def pytest_collect_file( - path: py.path.local, parent: nodes.Collector + fspath: Path, path: py.path.local, parent: nodes.Collector ) -> Optional["Module"]: ext = path.ext if ext == ".py": - if not parent.session.isinitpath(path): + if not parent.session.isinitpath(fspath): if not path_matches_patterns( path, parent.config.getini("python_files") + ["__init__.py"] ): return None - ihook = parent.session.gethookproxy(path) - module: Module = ihook.pytest_pycollect_makemodule(path=path, parent=parent) + ihook = parent.session.gethookproxy(fspath) + module: Module = ihook.pytest_pycollect_makemodule( + fspath=fspath, path=path, parent=parent + ) return module return None @@ -664,9 +667,10 @@ def isinitpath(self, path: py.path.local) -> bool: 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): + fspath = Path(direntry.path) + path = py.path.local(fspath) + ihook = self.session.gethookproxy(fspath.parent) + if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): return False norecursepatterns = self.config.getini("norecursedirs") if any(path.check(fnmatch=pat) for pat in norecursepatterns): @@ -676,6 +680,7 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: def _collectfile( self, path: py.path.local, handle_dupes: bool = True ) -> Sequence[nodes.Collector]: + fspath = Path(path) assert ( path.isfile() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( @@ -683,7 +688,9 @@ def _collectfile( ) ihook = self.session.gethookproxy(path) if not self.session.isinitpath(path): - if ihook.pytest_ignore_collect(path=path, config=self.config): + if ihook.pytest_ignore_collect( + fspath=fspath, path=path, config=self.config + ): return () if handle_dupes: @@ -695,7 +702,7 @@ def _collectfile( else: duplicate_paths.add(path) - return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] + return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return] def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: this_path = self.fspath.dirpath() diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 0e0ed70e5be..39adfaaa310 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -710,7 +710,7 @@ def pytest_sessionstart(self, session: "Session") -> None: msg += " -- " + str(sys.executable) self.write_line(msg) lines = self.config.hook.pytest_report_header( - config=self.config, startdir=self.startdir + config=self.config, startpath=self.startpath, startdir=self.startdir ) self._write_report_lines_from_hooks(lines) @@ -745,7 +745,10 @@ def pytest_collection_finish(self, session: "Session") -> None: self.report_collect(True) lines = self.config.hook.pytest_report_collectionfinish( - config=self.config, startdir=self.startdir, items=session.items + config=self.config, + startpath=self.startpath, + startdir=self.startdir, + items=session.items, ) self._write_report_lines_from_hooks(lines) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 7ad5849d4b9..6319188a75e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1010,7 +1010,7 @@ def test_more_quiet_reporting(self, pytester: Pytester) -> None: def test_report_collectionfinish_hook(self, pytester: Pytester, params) -> None: pytester.makeconftest( """ - def pytest_report_collectionfinish(config, startdir, items): + def pytest_report_collectionfinish(config, startpath, startdir, items): return ['hello from hook: {0} items'.format(len(items))] """ ) @@ -1436,8 +1436,8 @@ def pytest_report_header(config): ) pytester.mkdir("a").joinpath("conftest.py").write_text( """ -def pytest_report_header(config, startdir): - return ["line1", str(startdir)] +def pytest_report_header(config, startdir, startpath): + return ["line1", str(startpath)] """ ) result = pytester.runpytest("a") From 8eef8c6004af955f1074905e9656480eeeb3bb67 Mon Sep 17 00:00:00 2001 From: Anton <44246099+antonblr@users.noreply.github.com> Date: Tue, 15 Dec 2020 03:02:32 -0800 Subject: [PATCH 0327/2846] tests: Migrate to pytester - incremental update (#8145) --- testing/code/test_excinfo.py | 57 ++-- testing/examples/test_issue519.py | 9 +- testing/io/test_terminalwriter.py | 5 +- testing/logging/test_fixture.py | 50 +-- testing/logging/test_reporting.py | 275 +++++++-------- testing/test_cacheprovider.py | 533 ++++++++++++++++-------------- testing/test_conftest.py | 21 +- testing/test_monkeypatch.py | 68 ++-- testing/test_pluginmanager.py | 131 +++++--- testing/test_reports.py | 102 +++--- testing/test_runner.py | 253 +++++++------- 11 files changed, 801 insertions(+), 703 deletions(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 5b9e3eda529..44d7ab549e8 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -5,6 +5,7 @@ import queue import sys import textwrap +from pathlib import Path from typing import Any from typing import Dict from typing import Tuple @@ -19,7 +20,10 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import FormattedExcinfo from _pytest._io import TerminalWriter +from _pytest.pathlib import import_path from _pytest.pytester import LineMatcher +from _pytest.pytester import Pytester + if TYPE_CHECKING: from _pytest._code.code import _TracebackStyle @@ -155,10 +159,10 @@ def test_traceback_cut(self): newtraceback = traceback.cut(path=path, lineno=firstlineno + 2) assert len(newtraceback) == 1 - def test_traceback_cut_excludepath(self, testdir): - p = testdir.makepyfile("def f(): raise ValueError") + def test_traceback_cut_excludepath(self, pytester: Pytester) -> None: + p = pytester.makepyfile("def f(): raise ValueError") with pytest.raises(ValueError) as excinfo: - p.pyimport().f() + import_path(p).f() # type: ignore[attr-defined] basedir = py.path.local(pytest.__file__).dirpath() newtraceback = excinfo.traceback.cut(excludepath=basedir) for x in newtraceback: @@ -406,8 +410,8 @@ def test_match_succeeds(): excinfo.match(r".*zero.*") -def test_match_raises_error(testdir): - testdir.makepyfile( +def test_match_raises_error(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest def test_division_zero(): @@ -416,14 +420,14 @@ def test_division_zero(): excinfo.match(r'[123]+') """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret != 0 exc_msg = "Regex pattern '[[]123[]]+' does not match 'division by zero'." result.stdout.fnmatch_lines([f"E * AssertionError: {exc_msg}"]) result.stdout.no_fnmatch_line("*__tracebackhide__ = True*") - result = testdir.runpytest("--fulltrace") + result = pytester.runpytest("--fulltrace") assert result.ret != 0 result.stdout.fnmatch_lines( ["*__tracebackhide__ = True*", f"E * AssertionError: {exc_msg}"] @@ -432,15 +436,14 @@ def test_division_zero(): class TestFormattedExcinfo: @pytest.fixture - def importasmod(self, request, _sys_snapshot): + def importasmod(self, tmp_path: Path, _sys_snapshot): def importasmod(source): source = textwrap.dedent(source) - tmpdir = request.getfixturevalue("tmpdir") - modpath = tmpdir.join("mod.py") - tmpdir.ensure("__init__.py") - modpath.write(source) + modpath = tmp_path.joinpath("mod.py") + tmp_path.joinpath("__init__.py").touch() + modpath.write_text(source) importlib.invalidate_caches() - return modpath.pyimport() + return import_path(modpath) return importasmod @@ -682,7 +685,7 @@ def entry(): p = FormattedExcinfo(style="short") reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) lines = reprtb.lines - basename = py.path.local(mod.__file__).basename + basename = Path(mod.__file__).name assert lines[0] == " func1()" assert reprtb.reprfileloc is not None assert basename in str(reprtb.reprfileloc.path) @@ -948,7 +951,9 @@ def f(): assert line.endswith("mod.py") assert tw_mock.lines[12] == ":3: ValueError" - def test_toterminal_long_missing_source(self, importasmod, tmpdir, tw_mock): + def test_toterminal_long_missing_source( + self, importasmod, tmp_path: Path, tw_mock + ) -> None: mod = importasmod( """ def g(x): @@ -958,7 +963,7 @@ def f(): """ ) excinfo = pytest.raises(ValueError, mod.f) - tmpdir.join("mod.py").remove() + tmp_path.joinpath("mod.py").unlink() excinfo.traceback = excinfo.traceback.filter() repr = excinfo.getrepr() repr.toterminal(tw_mock) @@ -978,7 +983,9 @@ def f(): assert line.endswith("mod.py") assert tw_mock.lines[10] == ":3: ValueError" - def test_toterminal_long_incomplete_source(self, importasmod, tmpdir, tw_mock): + def test_toterminal_long_incomplete_source( + self, importasmod, tmp_path: Path, tw_mock + ) -> None: mod = importasmod( """ def g(x): @@ -988,7 +995,7 @@ def f(): """ ) excinfo = pytest.raises(ValueError, mod.f) - tmpdir.join("mod.py").write("asdf") + tmp_path.joinpath("mod.py").write_text("asdf") excinfo.traceback = excinfo.traceback.filter() repr = excinfo.getrepr() repr.toterminal(tw_mock) @@ -1374,16 +1381,18 @@ def test_repr_traceback_with_unicode(style, encoding): assert repr_traceback is not None -def test_cwd_deleted(testdir): - testdir.makepyfile( +def test_cwd_deleted(pytester: Pytester) -> None: + pytester.makepyfile( """ - def test(tmpdir): - tmpdir.chdir() - tmpdir.remove() + import os + + def test(tmp_path): + os.chdir(tmp_path) + tmp_path.unlink() assert False """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 1 failed in *"]) result.stdout.no_fnmatch_line("*INTERNALERROR*") result.stderr.no_fnmatch_line("*INTERNALERROR*") diff --git a/testing/examples/test_issue519.py b/testing/examples/test_issue519.py index e83f18fdc93..85ba545e671 100644 --- a/testing/examples/test_issue519.py +++ b/testing/examples/test_issue519.py @@ -1,3 +1,6 @@ -def test_510(testdir): - testdir.copy_example("issue_519.py") - testdir.runpytest("issue_519.py") +from _pytest.pytester import Pytester + + +def test_510(pytester: Pytester) -> None: + pytester.copy_example("issue_519.py") + pytester.runpytest("issue_519.py") diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py index db0ccf06a40..fac7593eadd 100644 --- a/testing/io/test_terminalwriter.py +++ b/testing/io/test_terminalwriter.py @@ -3,6 +3,7 @@ import re import shutil import sys +from pathlib import Path from typing import Generator from unittest import mock @@ -64,10 +65,10 @@ def test_terminalwriter_not_unicode() -> None: class TestTerminalWriter: @pytest.fixture(params=["path", "stringio"]) def tw( - self, request, tmpdir + self, request, tmp_path: Path ) -> Generator[terminalwriter.TerminalWriter, None, None]: if request.param == "path": - p = tmpdir.join("tmpfile") + p = tmp_path.joinpath("tmpfile") f = open(str(p), "w+", encoding="utf8") tw = terminalwriter.TerminalWriter(f) diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index ffd51bcad7a..f82df19715b 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -2,14 +2,14 @@ import pytest from _pytest.logging import caplog_records_key -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester logger = logging.getLogger(__name__) sublogger = logging.getLogger(__name__ + ".baz") -def test_fixture_help(testdir): - result = testdir.runpytest("--fixtures") +def test_fixture_help(pytester: Pytester) -> None: + result = pytester.runpytest("--fixtures") result.stdout.fnmatch_lines(["*caplog*"]) @@ -28,12 +28,12 @@ def test_change_level(caplog): assert "CRITICAL" in caplog.text -def test_change_level_undo(testdir: Testdir) -> None: +def test_change_level_undo(pytester: Pytester) -> 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( + pytester.makepyfile( """ import logging @@ -49,17 +49,17 @@ def test2(caplog): assert 0 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"]) result.stdout.no_fnmatch_line("*log from test2*") -def test_change_level_undos_handler_level(testdir: Testdir) -> None: +def test_change_level_undos_handler_level(pytester: Pytester) -> None: """Ensure that 'set_level' is undone after the end of the test (handler). Issue #7569. Tests the handler level specifically. """ - testdir.makepyfile( + pytester.makepyfile( """ import logging @@ -78,7 +78,7 @@ def test3(caplog): assert caplog.handler.level == 43 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=3) @@ -172,8 +172,8 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow assert set(caplog._item._store[caplog_records_key]) == {"setup", "call"} -def test_ini_controls_global_log_level(testdir): - testdir.makepyfile( +def test_ini_controls_global_log_level(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest import logging @@ -187,20 +187,20 @@ def test_log_level_override(request, caplog): assert 'ERROR' in caplog.text """ ) - testdir.makeini( + pytester.makeini( """ [pytest] log_level=ERROR """ ) - result = testdir.runpytest() + result = pytester.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( +def test_caplog_can_override_global_log_level(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest import logging @@ -227,19 +227,19 @@ def test_log_level_override(request, caplog): assert "message won't be shown" not in caplog.text """ ) - testdir.makeini( + pytester.makeini( """ [pytest] log_level=WARNING """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 -def test_caplog_captures_despite_exception(testdir): - testdir.makepyfile( +def test_caplog_captures_despite_exception(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest import logging @@ -255,26 +255,28 @@ def test_log_level_override(request, caplog): raise Exception() """ ) - testdir.makeini( + pytester.makeini( """ [pytest] log_level=WARNING """ ) - result = testdir.runpytest() + result = pytester.runpytest() 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): +def test_log_report_captures_according_to_config_option_upon_failure( + pytester: Pytester, +) -> None: """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( + pytester.makepyfile( """ import pytest import logging @@ -299,7 +301,7 @@ def test_that_fails(request, caplog): """ ) - result = testdir.runpytest("--log-level=INFO") + result = pytester.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( diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index fc9f1082346..a5ab8b98ba7 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -6,12 +6,13 @@ import pytest from _pytest.capture import CaptureManager from _pytest.config import ExitCode -from _pytest.pytester import Testdir +from _pytest.fixtures import FixtureRequest +from _pytest.pytester import Pytester from _pytest.terminal import TerminalReporter -def test_nothing_logged(testdir): - testdir.makepyfile( +def test_nothing_logged(pytester: Pytester) -> None: + pytester.makepyfile( """ import sys @@ -21,7 +22,7 @@ def test_foo(): assert False """ ) - result = testdir.runpytest() + result = pytester.runpytest() 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"]) @@ -29,8 +30,8 @@ def test_foo(): result.stdout.fnmatch_lines(["*- Captured *log call -*"]) -def test_messages_logged(testdir): - testdir.makepyfile( +def test_messages_logged(pytester: Pytester) -> None: + pytester.makepyfile( """ import sys import logging @@ -44,15 +45,15 @@ def test_foo(): assert False """ ) - result = testdir.runpytest("--log-level=INFO") + result = pytester.runpytest("--log-level=INFO") assert result.ret == 1 result.stdout.fnmatch_lines(["*- Captured *log call -*", "*text going to logger*"]) result.stdout.fnmatch_lines(["*- Captured stdout call -*", "text going to stdout"]) result.stdout.fnmatch_lines(["*- Captured stderr call -*", "text going to stderr"]) -def test_root_logger_affected(testdir): - testdir.makepyfile( +def test_root_logger_affected(pytester: Pytester) -> None: + pytester.makepyfile( """ import logging logger = logging.getLogger() @@ -65,8 +66,8 @@ def test_foo(): assert 0 """ ) - log_file = testdir.tmpdir.join("pytest.log").strpath - result = testdir.runpytest("--log-level=ERROR", "--log-file=pytest.log") + log_file = str(pytester.path.joinpath("pytest.log")) + result = pytester.runpytest("--log-level=ERROR", "--log-file=pytest.log") assert result.ret == 1 # The capture log calls in the stdout section only contain the @@ -87,8 +88,8 @@ def test_foo(): assert "error text going to logger" in contents -def test_log_cli_level_log_level_interaction(testdir): - testdir.makepyfile( +def test_log_cli_level_log_level_interaction(pytester: Pytester) -> None: + pytester.makepyfile( """ import logging logger = logging.getLogger() @@ -102,7 +103,7 @@ def test_foo(): """ ) - result = testdir.runpytest("--log-cli-level=INFO", "--log-level=ERROR") + result = pytester.runpytest("--log-cli-level=INFO", "--log-level=ERROR") assert result.ret == 1 result.stdout.fnmatch_lines( @@ -117,8 +118,8 @@ def test_foo(): result.stdout.no_re_match_line("DEBUG") -def test_setup_logging(testdir): - testdir.makepyfile( +def test_setup_logging(pytester: Pytester) -> None: + pytester.makepyfile( """ import logging @@ -132,7 +133,7 @@ def test_foo(): assert False """ ) - result = testdir.runpytest("--log-level=INFO") + result = pytester.runpytest("--log-level=INFO") assert result.ret == 1 result.stdout.fnmatch_lines( [ @@ -144,8 +145,8 @@ def test_foo(): ) -def test_teardown_logging(testdir): - testdir.makepyfile( +def test_teardown_logging(pytester: Pytester) -> None: + pytester.makepyfile( """ import logging @@ -159,7 +160,7 @@ def teardown_function(function): assert False """ ) - result = testdir.runpytest("--log-level=INFO") + result = pytester.runpytest("--log-level=INFO") assert result.ret == 1 result.stdout.fnmatch_lines( [ @@ -172,9 +173,9 @@ def teardown_function(function): @pytest.mark.parametrize("enabled", [True, False]) -def test_log_cli_enabled_disabled(testdir, enabled): +def test_log_cli_enabled_disabled(pytester: Pytester, enabled: bool) -> None: msg = "critical message logged by test" - testdir.makepyfile( + pytester.makepyfile( """ import logging def test_log_cli(): @@ -184,13 +185,13 @@ def test_log_cli(): ) ) if enabled: - testdir.makeini( + pytester.makeini( """ [pytest] log_cli=true """ ) - result = testdir.runpytest() + result = pytester.runpytest() if enabled: result.stdout.fnmatch_lines( [ @@ -204,9 +205,9 @@ def test_log_cli(): assert msg not in result.stdout.str() -def test_log_cli_default_level(testdir): +def test_log_cli_default_level(pytester: Pytester) -> None: # Default log file level - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging @@ -217,14 +218,14 @@ def test_log_cli(request): logging.getLogger('catchlog').warning("WARNING message will be shown") """ ) - testdir.makeini( + pytester.makeini( """ [pytest] log_cli=true """ ) - result = testdir.runpytest() + result = pytester.runpytest() # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( @@ -238,10 +239,12 @@ def test_log_cli(request): assert result.ret == 0 -def test_log_cli_default_level_multiple_tests(testdir, request): +def test_log_cli_default_level_multiple_tests( + pytester: Pytester, request: FixtureRequest +) -> None: """Ensure we reset the first newline added by the live logger between tests""" filename = request.node.name + ".py" - testdir.makepyfile( + pytester.makepyfile( """ import logging @@ -252,14 +255,14 @@ def test_log_2(): logging.warning("log message from test_log_2") """ ) - testdir.makeini( + pytester.makeini( """ [pytest] log_cli=true """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ f"{filename}::test_log_1 ", @@ -273,11 +276,13 @@ def test_log_2(): ) -def test_log_cli_default_level_sections(testdir, request): +def test_log_cli_default_level_sections( + pytester: Pytester, request: FixtureRequest +) -> None: """Check that with live logging enable we are printing the correct headers during start/setup/call/teardown/finish.""" filename = request.node.name + ".py" - testdir.makeconftest( + pytester.makeconftest( """ import pytest import logging @@ -290,7 +295,7 @@ def pytest_runtest_logfinish(): """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging @@ -308,14 +313,14 @@ def test_log_2(fix): logging.warning("log message from test_log_2") """ ) - testdir.makeini( + pytester.makeini( """ [pytest] log_cli=true """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ f"{filename}::test_log_1 ", @@ -347,11 +352,13 @@ def test_log_2(fix): ) -def test_live_logs_unknown_sections(testdir, request): +def test_live_logs_unknown_sections( + pytester: Pytester, request: FixtureRequest +) -> None: """Check that with live logging enable we are printing the correct headers during start/setup/call/teardown/finish.""" filename = request.node.name + ".py" - testdir.makeconftest( + pytester.makeconftest( """ import pytest import logging @@ -367,7 +374,7 @@ def pytest_runtest_logfinish(): """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging @@ -383,14 +390,14 @@ def test_log_1(fix): """ ) - testdir.makeini( + pytester.makeini( """ [pytest] log_cli=true """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*WARNING*Unknown Section*", @@ -409,11 +416,13 @@ def test_log_1(fix): ) -def test_sections_single_new_line_after_test_outcome(testdir, request): +def test_sections_single_new_line_after_test_outcome( + pytester: Pytester, request: FixtureRequest +) -> None: """Check that only a single new line is written between log messages during teardown/finish.""" filename = request.node.name + ".py" - testdir.makeconftest( + pytester.makeconftest( """ import pytest import logging @@ -427,7 +436,7 @@ def pytest_runtest_logfinish(): """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging @@ -443,14 +452,14 @@ def test_log_1(fix): logging.warning("log message from test_log_1") """ ) - testdir.makeini( + pytester.makeini( """ [pytest] log_cli=true """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ f"{filename}::test_log_1 ", @@ -487,9 +496,9 @@ def test_log_1(fix): ) -def test_log_cli_level(testdir): +def test_log_cli_level(pytester: Pytester) -> None: # Default log file level - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging @@ -501,14 +510,14 @@ def test_log_cli(request): print('PASSED') """ ) - testdir.makeini( + pytester.makeini( """ [pytest] log_cli=true """ ) - result = testdir.runpytest("-s", "--log-cli-level=INFO") + result = pytester.runpytest("-s", "--log-cli-level=INFO") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( @@ -522,7 +531,7 @@ def test_log_cli(request): # make sure that that we get a '0' exit code for the testsuite assert result.ret == 0 - result = testdir.runpytest("-s", "--log-level=INFO") + result = pytester.runpytest("-s", "--log-level=INFO") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( @@ -537,15 +546,15 @@ def test_log_cli(request): assert result.ret == 0 -def test_log_cli_ini_level(testdir): - testdir.makeini( +def test_log_cli_ini_level(pytester: Pytester) -> None: + pytester.makeini( """ [pytest] log_cli=true log_cli_level = INFO """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging @@ -558,7 +567,7 @@ def test_log_cli(request): """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( @@ -577,11 +586,11 @@ def test_log_cli(request): "cli_args", ["", "--log-level=WARNING", "--log-file-level=WARNING", "--log-cli-level=WARNING"], ) -def test_log_cli_auto_enable(testdir, cli_args): +def test_log_cli_auto_enable(pytester: Pytester, cli_args: str) -> None: """Check that live logs are enabled if --log-level or --log-cli-level is passed on the CLI. It should not be auto enabled if the same configs are set on the INI file. """ - testdir.makepyfile( + pytester.makepyfile( """ import logging @@ -591,7 +600,7 @@ def test_log_1(): """ ) - testdir.makeini( + pytester.makeini( """ [pytest] log_level=INFO @@ -599,7 +608,7 @@ def test_log_1(): """ ) - result = testdir.runpytest(cli_args) + result = pytester.runpytest(cli_args) stdout = result.stdout.str() if cli_args == "--log-cli-level=WARNING": result.stdout.fnmatch_lines( @@ -620,9 +629,9 @@ def test_log_1(): assert "WARNING" not in stdout -def test_log_file_cli(testdir): +def test_log_file_cli(pytester: Pytester) -> None: # Default log file level - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging @@ -635,9 +644,9 @@ def test_log_file(request): """ ) - log_file = testdir.tmpdir.join("pytest.log").strpath + log_file = str(pytester.path.joinpath("pytest.log")) - result = testdir.runpytest( + result = pytester.runpytest( "-s", f"--log-file={log_file}", "--log-file-level=WARNING" ) @@ -653,9 +662,9 @@ def test_log_file(request): assert "This log message won't be shown" not in contents -def test_log_file_cli_level(testdir): +def test_log_file_cli_level(pytester: Pytester) -> None: # Default log file level - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging @@ -668,9 +677,9 @@ def test_log_file(request): """ ) - log_file = testdir.tmpdir.join("pytest.log").strpath + log_file = str(pytester.path.joinpath("pytest.log")) - result = testdir.runpytest("-s", f"--log-file={log_file}", "--log-file-level=INFO") + result = pytester.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"]) @@ -684,22 +693,22 @@ def test_log_file(request): assert "This log message won't be shown" not in contents -def test_log_level_not_changed_by_default(testdir): - testdir.makepyfile( +def test_log_level_not_changed_by_default(pytester: Pytester) -> None: + pytester.makepyfile( """ import logging def test_log_file(): assert logging.getLogger().level == logging.WARNING """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") result.stdout.fnmatch_lines(["* 1 passed in *"]) -def test_log_file_ini(testdir): - log_file = testdir.tmpdir.join("pytest.log").strpath +def test_log_file_ini(pytester: Pytester) -> None: + log_file = str(pytester.path.joinpath("pytest.log")) - testdir.makeini( + pytester.makeini( """ [pytest] log_file={} @@ -708,7 +717,7 @@ def test_log_file_ini(testdir): log_file ) ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging @@ -721,7 +730,7 @@ def test_log_file(request): """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines(["test_log_file_ini.py PASSED"]) @@ -735,10 +744,10 @@ def test_log_file(request): assert "This log message won't be shown" not in contents -def test_log_file_ini_level(testdir): - log_file = testdir.tmpdir.join("pytest.log").strpath +def test_log_file_ini_level(pytester: Pytester) -> None: + log_file = str(pytester.path.joinpath("pytest.log")) - testdir.makeini( + pytester.makeini( """ [pytest] log_file={} @@ -747,7 +756,7 @@ def test_log_file_ini_level(testdir): log_file ) ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging @@ -760,7 +769,7 @@ def test_log_file(request): """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines(["test_log_file_ini_level.py PASSED"]) @@ -774,10 +783,10 @@ def test_log_file(request): assert "This log message won't be shown" not in contents -def test_log_file_unicode(testdir): - log_file = testdir.tmpdir.join("pytest.log").strpath +def test_log_file_unicode(pytester: Pytester) -> None: + log_file = str(pytester.path.joinpath("pytest.log")) - testdir.makeini( + pytester.makeini( """ [pytest] log_file={} @@ -786,7 +795,7 @@ def test_log_file_unicode(testdir): log_file ) ) - testdir.makepyfile( + pytester.makepyfile( """\ import logging @@ -797,7 +806,7 @@ def test_log_file(): """ ) - result = testdir.runpytest() + result = pytester.runpytest() # make sure that that we get a '0' exit code for the testsuite assert result.ret == 0 @@ -810,11 +819,13 @@ def test_log_file(): @pytest.mark.parametrize("has_capture_manager", [True, False]) -def test_live_logging_suspends_capture(has_capture_manager: bool, request) -> None: +def test_live_logging_suspends_capture( + has_capture_manager: bool, request: FixtureRequest +) -> 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 - ``testdir`` facilities because they do their own capturing. + ``pytester`` facilities because they do their own capturing. We parametrize the test to also make sure _LiveLoggingStreamHandler works correctly if no capture manager plugin is installed. @@ -856,8 +867,8 @@ def section(self, *args, **kwargs): assert cast(io.StringIO, out_file).getvalue() == "\nsome message\n" -def test_collection_live_logging(testdir): - testdir.makepyfile( +def test_collection_live_logging(pytester: Pytester) -> None: + pytester.makepyfile( """ import logging @@ -865,22 +876,22 @@ def test_collection_live_logging(testdir): """ ) - result = testdir.runpytest("--log-cli-level=INFO") + result = pytester.runpytest("--log-cli-level=INFO") result.stdout.fnmatch_lines( ["*--- live log collection ---*", "*Normal message*", "collected 0 items"] ) @pytest.mark.parametrize("verbose", ["", "-q", "-qq"]) -def test_collection_collect_only_live_logging(testdir, verbose): - testdir.makepyfile( +def test_collection_collect_only_live_logging(pytester: Pytester, verbose: str) -> None: + pytester.makepyfile( """ def test_simple(): pass """ ) - result = testdir.runpytest("--collect-only", "--log-cli-level=INFO", verbose) + result = pytester.runpytest("--collect-only", "--log-cli-level=INFO", verbose) expected_lines = [] @@ -907,10 +918,10 @@ def test_simple(): result.stdout.fnmatch_lines(expected_lines) -def test_collection_logging_to_file(testdir): - log_file = testdir.tmpdir.join("pytest.log").strpath +def test_collection_logging_to_file(pytester: Pytester) -> None: + log_file = str(pytester.path.joinpath("pytest.log")) - testdir.makeini( + pytester.makeini( """ [pytest] log_file={} @@ -920,7 +931,7 @@ def test_collection_logging_to_file(testdir): ) ) - testdir.makepyfile( + pytester.makepyfile( """ import logging @@ -932,7 +943,7 @@ def test_simple(): """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.no_fnmatch_line("*--- live log collection ---*") @@ -945,10 +956,10 @@ def test_simple(): assert "info message in test_simple" in contents -def test_log_in_hooks(testdir): - log_file = testdir.tmpdir.join("pytest.log").strpath +def test_log_in_hooks(pytester: Pytester) -> None: + log_file = str(pytester.path.joinpath("pytest.log")) - testdir.makeini( + pytester.makeini( """ [pytest] log_file={} @@ -958,7 +969,7 @@ def test_log_in_hooks(testdir): log_file ) ) - testdir.makeconftest( + pytester.makeconftest( """ import logging @@ -972,7 +983,7 @@ def pytest_sessionfinish(session, exitstatus): logging.info('sessionfinish') """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*sessionstart*", "*runtestloop*", "*sessionfinish*"]) with open(log_file) as rfh: contents = rfh.read() @@ -981,10 +992,10 @@ def pytest_sessionfinish(session, exitstatus): assert "sessionfinish" in contents -def test_log_in_runtest_logreport(testdir): - log_file = testdir.tmpdir.join("pytest.log").strpath +def test_log_in_runtest_logreport(pytester: Pytester) -> None: + log_file = str(pytester.path.joinpath("pytest.log")) - testdir.makeini( + pytester.makeini( """ [pytest] log_file={} @@ -994,7 +1005,7 @@ def test_log_in_runtest_logreport(testdir): log_file ) ) - testdir.makeconftest( + pytester.makeconftest( """ import logging logger = logging.getLogger(__name__) @@ -1003,29 +1014,29 @@ def pytest_runtest_logreport(report): logger.info("logreport") """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_first(): assert True """ ) - testdir.runpytest() + pytester.runpytest() with open(log_file) as rfh: contents = rfh.read() assert contents.count("logreport") == 3 -def test_log_set_path(testdir): - report_dir_base = testdir.tmpdir.strpath +def test_log_set_path(pytester: Pytester) -> None: + report_dir_base = str(pytester.path) - testdir.makeini( + pytester.makeini( """ [pytest] log_file_level = DEBUG log_cli=true """ ) - testdir.makeconftest( + pytester.makeconftest( """ import os import pytest @@ -1040,7 +1051,7 @@ def pytest_runtest_setup(item): repr(report_dir_base) ) ) - testdir.makepyfile( + pytester.makepyfile( """ import logging logger = logging.getLogger("testcase-logger") @@ -1053,7 +1064,7 @@ def test_second(): assert True """ ) - testdir.runpytest() + pytester.runpytest() with open(os.path.join(report_dir_base, "test_first")) as rfh: content = rfh.read() assert "message from test 1" in content @@ -1063,10 +1074,10 @@ def test_second(): assert "message from test 2" in content -def test_colored_captured_log(testdir): +def test_colored_captured_log(pytester: Pytester) -> None: """Test that the level names of captured log messages of a failing test are colored.""" - testdir.makepyfile( + pytester.makepyfile( """ import logging @@ -1077,7 +1088,7 @@ def test_foo(): assert False """ ) - result = testdir.runpytest("--log-level=INFO", "--color=yes") + result = pytester.runpytest("--log-level=INFO", "--color=yes") assert result.ret == 1 result.stdout.fnmatch_lines( [ @@ -1087,9 +1098,9 @@ def test_foo(): ) -def test_colored_ansi_esc_caplogtext(testdir): +def test_colored_ansi_esc_caplogtext(pytester: Pytester) -> None: """Make sure that caplog.text does not contain ANSI escape sequences.""" - testdir.makepyfile( + pytester.makepyfile( """ import logging @@ -1100,11 +1111,11 @@ def test_foo(caplog): assert '\x1b' not in caplog.text """ ) - result = testdir.runpytest("--log-level=INFO", "--color=yes") + result = pytester.runpytest("--log-level=INFO", "--color=yes") assert result.ret == 0 -def test_logging_emit_error(testdir: Testdir) -> None: +def test_logging_emit_error(pytester: Pytester) -> None: """An exception raised during emit() should fail the test. The default behavior of logging is to print "Logging error" @@ -1112,7 +1123,7 @@ def test_logging_emit_error(testdir: Testdir) -> None: pytest overrides this behavior to propagate the exception. """ - testdir.makepyfile( + pytester.makepyfile( """ import logging @@ -1120,7 +1131,7 @@ def test_bad_log(): logging.warning('oops', 'first', 2) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(failed=1) result.stdout.fnmatch_lines( [ @@ -1130,10 +1141,10 @@ def test_bad_log(): ) -def test_logging_emit_error_supressed(testdir: Testdir) -> None: +def test_logging_emit_error_supressed(pytester: Pytester) -> None: """If logging is configured to silently ignore errors, pytest doesn't propagate errors either.""" - testdir.makepyfile( + pytester.makepyfile( """ import logging @@ -1142,13 +1153,15 @@ def test_bad_log(monkeypatch): logging.warning('oops', 'first', 2) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=1) -def test_log_file_cli_subdirectories_are_successfully_created(testdir): - path = testdir.makepyfile(""" def test_logger(): pass """) +def test_log_file_cli_subdirectories_are_successfully_created( + pytester: Pytester, +) -> None: + path = pytester.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") + result = pytester.runpytest("--log-file=foo/bar/logf.log") assert "logf.log" in os.listdir(expected) assert result.ret == ExitCode.OK diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index ccc7304b02a..7f0827bd488 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -1,31 +1,32 @@ import os import shutil -import stat import sys - -import py +from pathlib import Path +from typing import List import pytest from _pytest.config import ExitCode +from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester -from _pytest.pytester import Testdir pytest_plugins = ("pytester",) class TestNewAPI: - def test_config_cache_makedir(self, testdir): - testdir.makeini("[pytest]") - config = testdir.parseconfigure() + def test_config_cache_makedir(self, pytester: Pytester) -> None: + pytester.makeini("[pytest]") + config = pytester.parseconfigure() + assert config.cache is not None with pytest.raises(ValueError): config.cache.makedir("key/name") p = config.cache.makedir("name") assert p.check() - def test_config_cache_dataerror(self, testdir): - testdir.makeini("[pytest]") - config = testdir.parseconfigure() + def test_config_cache_dataerror(self, pytester: Pytester) -> None: + pytester.makeini("[pytest]") + config = pytester.parseconfigure() + assert config.cache is not None cache = config.cache pytest.raises(TypeError, lambda: cache.set("key/name", cache)) config.cache.set("key/name", 0) @@ -34,39 +35,45 @@ def test_config_cache_dataerror(self, testdir): assert val == -2 @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") - config = testdir.parseconfigure() + def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None: + pytester.makeini("[pytest]") + pytester.path.joinpath(".pytest_cache").write_text("gone wrong") + config = pytester.parseconfigure() cache = config.cache + assert cache is not None cache.set("test/broken", []) @pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows") @pytest.mark.filterwarnings( "ignore:could not create cache path:pytest.PytestWarning" ) - def test_cache_writefail_permissions(self, testdir): - testdir.makeini("[pytest]") - cache_dir = str(testdir.tmpdir.ensure_dir(".pytest_cache")) - mode = os.stat(cache_dir)[stat.ST_MODE] - testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) + def test_cache_writefail_permissions(self, pytester: Pytester) -> None: + pytester.makeini("[pytest]") + cache_dir = pytester.path.joinpath(".pytest_cache") + cache_dir.mkdir() + mode = cache_dir.stat().st_mode + cache_dir.chmod(0) try: - config = testdir.parseconfigure() + config = pytester.parseconfigure() cache = config.cache + assert cache is not None cache.set("test/broken", []) finally: - testdir.tmpdir.ensure_dir(".pytest_cache").chmod(mode) + cache_dir.chmod(mode) @pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows") @pytest.mark.filterwarnings("default") - def test_cache_failure_warns(self, testdir, monkeypatch): + def test_cache_failure_warns( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") - cache_dir = str(testdir.tmpdir.ensure_dir(".pytest_cache")) - mode = os.stat(cache_dir)[stat.ST_MODE] - testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) + cache_dir = pytester.path.joinpath(".pytest_cache") + cache_dir.mkdir() + mode = cache_dir.stat().st_mode + cache_dir.chmod(0) try: - testdir.makepyfile("def test_error(): raise Exception") - result = testdir.runpytest() + pytester.makepyfile("def test_error(): raise Exception") + result = pytester.runpytest() assert result.ret == 1 # warnings from nodeids, lastfailed, and stepwise result.stdout.fnmatch_lines( @@ -81,28 +88,28 @@ def test_cache_failure_warns(self, testdir, monkeypatch): ] ) finally: - testdir.tmpdir.ensure_dir(".pytest_cache").chmod(mode) + cache_dir.chmod(mode) - def test_config_cache(self, testdir): - testdir.makeconftest( + def test_config_cache(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_configure(config): # see that we get cache information early on assert hasattr(config, "cache") """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_session(pytestconfig): assert hasattr(pytestconfig, "cache") """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) - def test_cachefuncarg(self, testdir): - testdir.makepyfile( + def test_cachefuncarg(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest def test_cachefuncarg(cache): @@ -114,13 +121,13 @@ def test_cachefuncarg(cache): assert val == [1] """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) - def test_custom_rel_cache_dir(self, testdir): + def test_custom_rel_cache_dir(self, pytester: Pytester) -> None: rel_cache_dir = os.path.join("custom_cache_dir", "subdir") - testdir.makeini( + pytester.makeini( """ [pytest] cache_dir = {cache_dir} @@ -128,14 +135,14 @@ def test_custom_rel_cache_dir(self, testdir): cache_dir=rel_cache_dir ) ) - testdir.makepyfile(test_errored="def test_error():\n assert False") - testdir.runpytest() - assert testdir.tmpdir.join(rel_cache_dir).isdir() + pytester.makepyfile(test_errored="def test_error():\n assert False") + pytester.runpytest() + assert pytester.path.joinpath(rel_cache_dir).is_dir() - def test_custom_abs_cache_dir(self, testdir, tmpdir_factory): + def test_custom_abs_cache_dir(self, pytester: Pytester, tmpdir_factory) -> None: tmp = str(tmpdir_factory.mktemp("tmp")) abs_cache_dir = os.path.join(tmp, "custom_cache_dir") - testdir.makeini( + pytester.makeini( """ [pytest] cache_dir = {cache_dir} @@ -143,13 +150,15 @@ def test_custom_abs_cache_dir(self, testdir, tmpdir_factory): cache_dir=abs_cache_dir ) ) - testdir.makepyfile(test_errored="def test_error():\n assert False") - testdir.runpytest() - assert py.path.local(abs_cache_dir).isdir() + pytester.makepyfile(test_errored="def test_error():\n assert False") + pytester.runpytest() + assert Path(abs_cache_dir).is_dir() - def test_custom_cache_dir_with_env_var(self, testdir, monkeypatch): + def test_custom_cache_dir_with_env_var( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: monkeypatch.setenv("env_var", "custom_cache_dir") - testdir.makeini( + pytester.makeini( """ [pytest] cache_dir = {cache_dir} @@ -157,31 +166,33 @@ def test_custom_cache_dir_with_env_var(self, testdir, monkeypatch): cache_dir="$env_var" ) ) - testdir.makepyfile(test_errored="def test_error():\n assert False") - testdir.runpytest() - assert testdir.tmpdir.join("custom_cache_dir").isdir() + pytester.makepyfile(test_errored="def test_error():\n assert False") + pytester.runpytest() + assert pytester.path.joinpath("custom_cache_dir").is_dir() @pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "/tox_env_dir"))) -def test_cache_reportheader(env, testdir, monkeypatch): - testdir.makepyfile("""def test_foo(): pass""") +def test_cache_reportheader(env, pytester: Pytester, monkeypatch: MonkeyPatch) -> None: + pytester.makepyfile("""def test_foo(): pass""") if env: monkeypatch.setenv(*env) expected = os.path.join(env[1], ".pytest_cache") else: monkeypatch.delenv("TOX_ENV_DIR", raising=False) expected = ".pytest_cache" - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines(["cachedir: %s" % expected]) -def test_cache_reportheader_external_abspath(testdir, tmpdir_factory): +def test_cache_reportheader_external_abspath( + pytester: Pytester, tmpdir_factory +) -> None: external_cache = tmpdir_factory.mktemp( "test_cache_reportheader_external_abspath_abs" ) - testdir.makepyfile("def test_hello(): pass") - testdir.makeini( + pytester.makepyfile("def test_hello(): pass") + pytester.makeini( """ [pytest] cache_dir = {abscache} @@ -189,15 +200,15 @@ def test_cache_reportheader_external_abspath(testdir, tmpdir_factory): abscache=external_cache ) ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines([f"cachedir: {external_cache}"]) -def test_cache_show(testdir): - result = testdir.runpytest("--cache-show") +def test_cache_show(pytester: Pytester) -> None: + result = pytester.runpytest("--cache-show") assert result.ret == 0 result.stdout.fnmatch_lines(["*cache is empty*"]) - testdir.makeconftest( + pytester.makeconftest( """ def pytest_configure(config): config.cache.set("my/name", [1,2,3]) @@ -208,10 +219,10 @@ def pytest_configure(config): dp.ensure("world") """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 5 # no tests executed - result = testdir.runpytest("--cache-show") + result = pytester.runpytest("--cache-show") result.stdout.fnmatch_lines( [ "*cachedir:*", @@ -228,7 +239,7 @@ def pytest_configure(config): ) assert result.ret == 0 - result = testdir.runpytest("--cache-show", "*/hello") + result = pytester.runpytest("--cache-show", "*/hello") result.stdout.fnmatch_lines( [ "*cachedir:*", @@ -246,25 +257,27 @@ def pytest_configure(config): class TestLastFailed: - def test_lastfailed_usecase(self, testdir, monkeypatch): + def test_lastfailed_usecase( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: monkeypatch.setattr("sys.dont_write_bytecode", True) - p = testdir.makepyfile( + p = pytester.makepyfile( """ def test_1(): assert 0 def test_2(): assert 0 def test_3(): assert 1 """ ) - result = testdir.runpytest(str(p)) + result = pytester.runpytest(str(p)) result.stdout.fnmatch_lines(["*2 failed*"]) - p = testdir.makepyfile( + p = pytester.makepyfile( """ def test_1(): assert 1 def test_2(): assert 1 def test_3(): assert 0 """ ) - result = testdir.runpytest(str(p), "--lf") + result = pytester.runpytest(str(p), "--lf") result.stdout.fnmatch_lines( [ "collected 3 items / 1 deselected / 2 selected", @@ -272,7 +285,7 @@ def test_3(): assert 0 "*= 2 passed, 1 deselected in *", ] ) - result = testdir.runpytest(str(p), "--lf") + result = pytester.runpytest(str(p), "--lf") result.stdout.fnmatch_lines( [ "collected 3 items", @@ -280,27 +293,27 @@ def test_3(): assert 0 "*1 failed*2 passed*", ] ) - testdir.tmpdir.join(".pytest_cache").mkdir(".git") - result = testdir.runpytest(str(p), "--lf", "--cache-clear") + pytester.path.joinpath(".pytest_cache", ".git").mkdir(parents=True) + result = pytester.runpytest(str(p), "--lf", "--cache-clear") result.stdout.fnmatch_lines(["*1 failed*2 passed*"]) - assert testdir.tmpdir.join(".pytest_cache", "README.md").isfile() - assert testdir.tmpdir.join(".pytest_cache", ".git").isdir() + assert pytester.path.joinpath(".pytest_cache", "README.md").is_file() + assert pytester.path.joinpath(".pytest_cache", ".git").is_dir() # Run this again to make sure clear-cache is robust if os.path.isdir(".pytest_cache"): shutil.rmtree(".pytest_cache") - result = testdir.runpytest("--lf", "--cache-clear") + result = pytester.runpytest("--lf", "--cache-clear") result.stdout.fnmatch_lines(["*1 failed*2 passed*"]) - def test_failedfirst_order(self, testdir): - testdir.makepyfile( + def test_failedfirst_order(self, pytester: Pytester) -> None: + pytester.makepyfile( test_a="def test_always_passes(): pass", test_b="def test_always_fails(): assert 0", ) - result = testdir.runpytest() + result = pytester.runpytest() # Test order will be collection order; alphabetical result.stdout.fnmatch_lines(["test_a.py*", "test_b.py*"]) - result = testdir.runpytest("--ff") + result = pytester.runpytest("--ff") # Test order will be failing tests first result.stdout.fnmatch_lines( [ @@ -311,40 +324,42 @@ def test_failedfirst_order(self, testdir): ] ) - def test_lastfailed_failedfirst_order(self, testdir): - testdir.makepyfile( + def test_lastfailed_failedfirst_order(self, pytester: Pytester) -> None: + pytester.makepyfile( test_a="def test_always_passes(): assert 1", test_b="def test_always_fails(): assert 0", ) - result = testdir.runpytest() + result = pytester.runpytest() # Test order will be collection order; alphabetical result.stdout.fnmatch_lines(["test_a.py*", "test_b.py*"]) - result = testdir.runpytest("--lf", "--ff") + result = pytester.runpytest("--lf", "--ff") # Test order will be failing tests first result.stdout.fnmatch_lines(["test_b.py*"]) result.stdout.no_fnmatch_line("*test_a.py*") - def test_lastfailed_difference_invocations(self, testdir, monkeypatch): + def test_lastfailed_difference_invocations( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: monkeypatch.setattr("sys.dont_write_bytecode", True) - testdir.makepyfile( + pytester.makepyfile( test_a=""" def test_a1(): assert 0 def test_a2(): assert 1 """, test_b="def test_b1(): assert 0", ) - p = testdir.tmpdir.join("test_a.py") - p2 = testdir.tmpdir.join("test_b.py") + p = pytester.path.joinpath("test_a.py") + p2 = pytester.path.joinpath("test_b.py") - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 failed*"]) - result = testdir.runpytest("--lf", p2) + result = pytester.runpytest("--lf", p2) result.stdout.fnmatch_lines(["*1 failed*"]) - testdir.makepyfile(test_b="def test_b1(): assert 1") - result = testdir.runpytest("--lf", p2) + pytester.makepyfile(test_b="def test_b1(): assert 1") + result = pytester.runpytest("--lf", p2) result.stdout.fnmatch_lines(["*1 passed*"]) - result = testdir.runpytest("--lf", p) + result = pytester.runpytest("--lf", p) result.stdout.fnmatch_lines( [ "collected 2 items / 1 deselected / 1 selected", @@ -353,21 +368,23 @@ def test_a2(): assert 1 ] ) - def test_lastfailed_usecase_splice(self, testdir, monkeypatch): + def test_lastfailed_usecase_splice( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: monkeypatch.setattr("sys.dont_write_bytecode", True) - testdir.makepyfile( + pytester.makepyfile( "def test_1(): assert 0", test_something="def test_2(): assert 0" ) - p2 = testdir.tmpdir.join("test_something.py") - result = testdir.runpytest() + p2 = pytester.path.joinpath("test_something.py") + result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 failed*"]) - result = testdir.runpytest("--lf", p2) + result = pytester.runpytest("--lf", p2) result.stdout.fnmatch_lines(["*1 failed*"]) - result = testdir.runpytest("--lf") + result = pytester.runpytest("--lf") result.stdout.fnmatch_lines(["*2 failed*"]) - def test_lastfailed_xpass(self, testdir): - testdir.inline_runsource( + def test_lastfailed_xpass(self, pytester: Pytester) -> None: + pytester.inline_runsource( """ import pytest @pytest.mark.xfail @@ -375,15 +392,16 @@ def test_hello(): assert 1 """ ) - config = testdir.parseconfigure() + config = pytester.parseconfigure() + assert config.cache is not None lastfailed = config.cache.get("cache/lastfailed", -1) assert lastfailed == -1 - def test_non_serializable_parametrize(self, testdir): + def test_non_serializable_parametrize(self, pytester: Pytester) -> None: """Test that failed parametrized tests with unmarshable parameters don't break pytest-cache. """ - testdir.makepyfile( + pytester.makepyfile( r""" import pytest @@ -394,26 +412,26 @@ def test_fail(val): assert False """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 failed in*"]) - def test_terminal_report_lastfailed(self, testdir): - test_a = testdir.makepyfile( + def test_terminal_report_lastfailed(self, pytester: Pytester) -> None: + test_a = pytester.makepyfile( test_a=""" def test_a1(): pass def test_a2(): pass """ ) - test_b = testdir.makepyfile( + test_b = pytester.makepyfile( test_b=""" def test_b1(): assert 0 def test_b2(): assert 0 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["collected 4 items", "*2 failed, 2 passed in*"]) - result = testdir.runpytest("--lf") + result = pytester.runpytest("--lf") result.stdout.fnmatch_lines( [ "collected 2 items", @@ -422,7 +440,7 @@ def test_b2(): assert 0 ] ) - result = testdir.runpytest(test_a, "--lf") + result = pytester.runpytest(test_a, "--lf") result.stdout.fnmatch_lines( [ "collected 2 items", @@ -431,7 +449,7 @@ def test_b2(): assert 0 ] ) - result = testdir.runpytest(test_b, "--lf") + result = pytester.runpytest(test_b, "--lf") result.stdout.fnmatch_lines( [ "collected 2 items", @@ -440,7 +458,7 @@ def test_b2(): assert 0 ] ) - result = testdir.runpytest("test_b.py::test_b1", "--lf") + result = pytester.runpytest("test_b.py::test_b1", "--lf") result.stdout.fnmatch_lines( [ "collected 1 item", @@ -449,17 +467,17 @@ def test_b2(): assert 0 ] ) - def test_terminal_report_failedfirst(self, testdir): - testdir.makepyfile( + def test_terminal_report_failedfirst(self, pytester: Pytester) -> None: + pytester.makepyfile( test_a=""" def test_a1(): assert 0 def test_a2(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["collected 2 items", "*1 failed, 1 passed in*"]) - result = testdir.runpytest("--ff") + result = pytester.runpytest("--ff") result.stdout.fnmatch_lines( [ "collected 2 items", @@ -468,9 +486,11 @@ def test_a2(): pass ] ) - def test_lastfailed_collectfailure(self, testdir, monkeypatch): + def test_lastfailed_collectfailure( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: - testdir.makepyfile( + pytester.makepyfile( test_maybe=""" import os env = os.environ @@ -485,8 +505,9 @@ def rlf(fail_import, fail_run): monkeypatch.setenv("FAILIMPORT", str(fail_import)) monkeypatch.setenv("FAILTEST", str(fail_run)) - testdir.runpytest("-q") - config = testdir.parseconfigure() + pytester.runpytest("-q") + config = pytester.parseconfigure() + assert config.cache is not None lastfailed = config.cache.get("cache/lastfailed", -1) return lastfailed @@ -499,8 +520,10 @@ def rlf(fail_import, fail_run): lastfailed = rlf(fail_import=0, fail_run=1) assert list(lastfailed) == ["test_maybe.py::test_hello"] - def test_lastfailed_failure_subset(self, testdir, monkeypatch): - testdir.makepyfile( + def test_lastfailed_failure_subset( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: + pytester.makepyfile( test_maybe=""" import os env = os.environ @@ -511,7 +534,7 @@ def test_hello(): """ ) - testdir.makepyfile( + pytester.makepyfile( test_maybe2=""" import os env = os.environ @@ -530,8 +553,9 @@ def rlf(fail_import, fail_run, args=()): monkeypatch.setenv("FAILIMPORT", str(fail_import)) monkeypatch.setenv("FAILTEST", str(fail_run)) - result = testdir.runpytest("-q", "--lf", *args) - config = testdir.parseconfigure() + result = pytester.runpytest("-q", "--lf", *args) + config = pytester.parseconfigure() + assert config.cache is not None lastfailed = config.cache.get("cache/lastfailed", -1) return result, lastfailed @@ -552,61 +576,63 @@ def rlf(fail_import, fail_run, args=()): assert list(lastfailed) == ["test_maybe.py"] result.stdout.fnmatch_lines(["*2 passed*"]) - def test_lastfailed_creates_cache_when_needed(self, testdir): + def test_lastfailed_creates_cache_when_needed(self, pytester: Pytester) -> None: # Issue #1342 - testdir.makepyfile(test_empty="") - testdir.runpytest("-q", "--lf") + pytester.makepyfile(test_empty="") + pytester.runpytest("-q", "--lf") assert not os.path.exists(".pytest_cache/v/cache/lastfailed") - testdir.makepyfile(test_successful="def test_success():\n assert True") - testdir.runpytest("-q", "--lf") + pytester.makepyfile(test_successful="def test_success():\n assert True") + pytester.runpytest("-q", "--lf") assert not os.path.exists(".pytest_cache/v/cache/lastfailed") - testdir.makepyfile(test_errored="def test_error():\n assert False") - testdir.runpytest("-q", "--lf") + pytester.makepyfile(test_errored="def test_error():\n assert False") + pytester.runpytest("-q", "--lf") assert os.path.exists(".pytest_cache/v/cache/lastfailed") - def test_xfail_not_considered_failure(self, testdir): - testdir.makepyfile( + def test_xfail_not_considered_failure(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.xfail def test(): assert 0 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 xfailed*"]) - assert self.get_cached_last_failed(testdir) == [] + assert self.get_cached_last_failed(pytester) == [] - def test_xfail_strict_considered_failure(self, testdir): - testdir.makepyfile( + def test_xfail_strict_considered_failure(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.xfail(strict=True) def test(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 failed*"]) - assert self.get_cached_last_failed(testdir) == [ + assert self.get_cached_last_failed(pytester) == [ "test_xfail_strict_considered_failure.py::test" ] @pytest.mark.parametrize("mark", ["mark.xfail", "mark.skip"]) - def test_failed_changed_to_xfail_or_skip(self, testdir, mark): - testdir.makepyfile( + def test_failed_changed_to_xfail_or_skip( + self, pytester: Pytester, mark: str + ) -> None: + pytester.makepyfile( """ import pytest def test(): assert 0 """ ) - result = testdir.runpytest() - assert self.get_cached_last_failed(testdir) == [ + result = pytester.runpytest() + assert self.get_cached_last_failed(pytester) == [ "test_failed_changed_to_xfail_or_skip.py::test" ] assert result.ret == 1 - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.{mark} @@ -615,66 +641,69 @@ def test(): assert 0 mark=mark ) ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 - assert self.get_cached_last_failed(testdir) == [] + assert self.get_cached_last_failed(pytester) == [] assert result.ret == 0 @pytest.mark.parametrize("quiet", [True, False]) @pytest.mark.parametrize("opt", ["--ff", "--lf"]) - def test_lf_and_ff_prints_no_needless_message(self, quiet, opt, testdir): + def test_lf_and_ff_prints_no_needless_message( + self, quiet: bool, opt: str, pytester: Pytester + ) -> None: # Issue 3853 - testdir.makepyfile("def test(): assert 0") + pytester.makepyfile("def test(): assert 0") args = [opt] if quiet: args.append("-q") - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) result.stdout.no_fnmatch_line("*run all*") - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) if quiet: result.stdout.no_fnmatch_line("*run all*") else: assert "rerun previous" in result.stdout.str() - def get_cached_last_failed(self, testdir): - config = testdir.parseconfigure() + def get_cached_last_failed(self, pytester: Pytester) -> List[str]: + config = pytester.parseconfigure() + assert config.cache is not None return sorted(config.cache.get("cache/lastfailed", {})) - def test_cache_cumulative(self, testdir): + def test_cache_cumulative(self, pytester: Pytester) -> None: """Test workflow where user fixes errors gradually file by file using --lf.""" # 1. initial run - test_bar = testdir.makepyfile( + test_bar = pytester.makepyfile( test_bar=""" def test_bar_1(): pass def test_bar_2(): assert 0 """ ) - test_foo = testdir.makepyfile( + test_foo = pytester.makepyfile( test_foo=""" def test_foo_3(): pass def test_foo_4(): assert 0 """ ) - testdir.runpytest() - assert self.get_cached_last_failed(testdir) == [ + pytester.runpytest() + assert self.get_cached_last_failed(pytester) == [ "test_bar.py::test_bar_2", "test_foo.py::test_foo_4", ] # 2. fix test_bar_2, run only test_bar.py - testdir.makepyfile( + pytester.makepyfile( test_bar=""" def test_bar_1(): pass def test_bar_2(): pass """ ) - result = testdir.runpytest(test_bar) + result = pytester.runpytest(test_bar) result.stdout.fnmatch_lines(["*2 passed*"]) # ensure cache does not forget that test_foo_4 failed once before - assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"] + assert self.get_cached_last_failed(pytester) == ["test_foo.py::test_foo_4"] - result = testdir.runpytest("--last-failed") + result = pytester.runpytest("--last-failed") result.stdout.fnmatch_lines( [ "collected 1 item", @@ -682,16 +711,16 @@ def test_bar_2(): pass "*= 1 failed in *", ] ) - assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"] + assert self.get_cached_last_failed(pytester) == ["test_foo.py::test_foo_4"] # 3. fix test_foo_4, run only test_foo.py - test_foo = testdir.makepyfile( + test_foo = pytester.makepyfile( test_foo=""" def test_foo_3(): pass def test_foo_4(): pass """ ) - result = testdir.runpytest(test_foo, "--last-failed") + result = pytester.runpytest(test_foo, "--last-failed") result.stdout.fnmatch_lines( [ "collected 2 items / 1 deselected / 1 selected", @@ -699,29 +728,31 @@ def test_foo_4(): pass "*= 1 passed, 1 deselected in *", ] ) - assert self.get_cached_last_failed(testdir) == [] + assert self.get_cached_last_failed(pytester) == [] - result = testdir.runpytest("--last-failed") + result = pytester.runpytest("--last-failed") result.stdout.fnmatch_lines(["*4 passed*"]) - assert self.get_cached_last_failed(testdir) == [] + assert self.get_cached_last_failed(pytester) == [] - def test_lastfailed_no_failures_behavior_all_passed(self, testdir): - testdir.makepyfile( + def test_lastfailed_no_failures_behavior_all_passed( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( """ def test_1(): pass def test_2(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 passed*"]) - result = testdir.runpytest("--lf") + result = pytester.runpytest("--lf") result.stdout.fnmatch_lines(["*2 passed*"]) - result = testdir.runpytest("--lf", "--lfnf", "all") + result = pytester.runpytest("--lf", "--lfnf", "all") result.stdout.fnmatch_lines(["*2 passed*"]) # Ensure the list passed to pytest_deselected is a copy, # and not a reference which is cleared right after. - testdir.makeconftest( + pytester.makeconftest( """ deselected = [] @@ -734,7 +765,7 @@ def pytest_sessionfinish(): """ ) - result = testdir.runpytest("--lf", "--lfnf", "none") + result = pytester.runpytest("--lf", "--lfnf", "none") result.stdout.fnmatch_lines( [ "collected 2 items / 2 deselected", @@ -745,26 +776,28 @@ def pytest_sessionfinish(): ) assert result.ret == ExitCode.NO_TESTS_COLLECTED - def test_lastfailed_no_failures_behavior_empty_cache(self, testdir): - testdir.makepyfile( + def test_lastfailed_no_failures_behavior_empty_cache( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( """ def test_1(): pass def test_2(): assert 0 """ ) - result = testdir.runpytest("--lf", "--cache-clear") + result = pytester.runpytest("--lf", "--cache-clear") result.stdout.fnmatch_lines(["*1 failed*1 passed*"]) - result = testdir.runpytest("--lf", "--cache-clear", "--lfnf", "all") + result = pytester.runpytest("--lf", "--cache-clear", "--lfnf", "all") result.stdout.fnmatch_lines(["*1 failed*1 passed*"]) - result = testdir.runpytest("--lf", "--cache-clear", "--lfnf", "none") + result = pytester.runpytest("--lf", "--cache-clear", "--lfnf", "none") result.stdout.fnmatch_lines(["*2 desel*"]) - def test_lastfailed_skip_collection(self, testdir): + def test_lastfailed_skip_collection(self, pytester: Pytester) -> None: """ Test --lf behavior regarding skipping collection of files that are not marked as failed in the cache (#5172). """ - testdir.makepyfile( + pytester.makepyfile( **{ "pkg1/test_1.py": """ import pytest @@ -782,10 +815,10 @@ def test_1(i): } ) # first run: collects 8 items (test_1: 3, test_2: 5) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["collected 8 items", "*2 failed*6 passed*"]) # second run: collects only 5 items from test_2, because all tests from test_1 have passed - result = testdir.runpytest("--lf") + result = pytester.runpytest("--lf") result.stdout.fnmatch_lines( [ "collected 2 items", @@ -795,14 +828,14 @@ def test_1(i): ) # add another file and check if message is correct when skipping more than 1 file - testdir.makepyfile( + pytester.makepyfile( **{ "pkg1/test_3.py": """ def test_3(): pass """ } ) - result = testdir.runpytest("--lf") + result = pytester.runpytest("--lf") result.stdout.fnmatch_lines( [ "collected 2 items", @@ -811,18 +844,20 @@ def test_3(): pass ] ) - def test_lastfailed_with_known_failures_not_being_selected(self, testdir): - testdir.makepyfile( + def test_lastfailed_with_known_failures_not_being_selected( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( **{ "pkg1/test_1.py": """def test_1(): assert 0""", "pkg1/test_2.py": """def test_2(): pass""", } ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"]) - py.path.local("pkg1/test_1.py").remove() - result = testdir.runpytest("--lf") + Path("pkg1/test_1.py").unlink() + result = pytester.runpytest("--lf") result.stdout.fnmatch_lines( [ "collected 1 item", @@ -832,8 +867,8 @@ def test_lastfailed_with_known_failures_not_being_selected(self, testdir): ) # Recreate file with known failure. - testdir.makepyfile(**{"pkg1/test_1.py": """def test_1(): assert 0"""}) - result = testdir.runpytest("--lf") + pytester.makepyfile(**{"pkg1/test_1.py": """def test_1(): assert 0"""}) + result = pytester.runpytest("--lf") result.stdout.fnmatch_lines( [ "collected 1 item", @@ -843,8 +878,8 @@ def test_lastfailed_with_known_failures_not_being_selected(self, testdir): ) # Remove/rename test: collects the file again. - testdir.makepyfile(**{"pkg1/test_1.py": """def test_renamed(): assert 0"""}) - result = testdir.runpytest("--lf", "-rf") + pytester.makepyfile(**{"pkg1/test_1.py": """def test_renamed(): assert 0"""}) + result = pytester.runpytest("--lf", "-rf") result.stdout.fnmatch_lines( [ "collected 2 items", @@ -856,7 +891,7 @@ def test_lastfailed_with_known_failures_not_being_selected(self, testdir): ] ) - result = testdir.runpytest("--lf", "--co") + result = pytester.runpytest("--lf", "--co") result.stdout.fnmatch_lines( [ "collected 1 item", @@ -867,13 +902,13 @@ def test_lastfailed_with_known_failures_not_being_selected(self, testdir): ] ) - def test_lastfailed_args_with_deselected(self, testdir: Testdir) -> None: + def test_lastfailed_args_with_deselected(self, pytester: Pytester) -> None: """Test regression with --lf running into NoMatch error. This was caused by it not collecting (non-failed) nodes given as arguments. """ - testdir.makepyfile( + pytester.makepyfile( **{ "pkg1/test_1.py": """ def test_pass(): pass @@ -881,11 +916,11 @@ def test_fail(): assert 0 """, } ) - result = testdir.runpytest() + result = pytester.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") + result = pytester.runpytest("pkg1/test_1.py::test_pass", "--lf", "--co") assert result.ret == 0 result.stdout.fnmatch_lines( [ @@ -898,7 +933,7 @@ def test_fail(): assert 0 consecutive=True, ) - result = testdir.runpytest( + result = pytester.runpytest( "pkg1/test_1.py::test_pass", "pkg1/test_1.py::test_fail", "--lf", "--co" ) assert result.ret == 0 @@ -913,9 +948,9 @@ def test_fail(): assert 0 ], ) - def test_lastfailed_with_class_items(self, testdir: Testdir) -> None: + def test_lastfailed_with_class_items(self, pytester: Pytester) -> None: """Test regression with --lf deselecting whole classes.""" - testdir.makepyfile( + pytester.makepyfile( **{ "pkg1/test_1.py": """ class TestFoo: @@ -926,11 +961,11 @@ def test_other(): assert 0 """, } ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["collected 3 items", "* 2 failed, 1 passed in *"]) assert result.ret == 1 - result = testdir.runpytest("--lf", "--co") + result = pytester.runpytest("--lf", "--co") assert result.ret == 0 result.stdout.fnmatch_lines( [ @@ -947,8 +982,8 @@ def test_other(): assert 0 consecutive=True, ) - def test_lastfailed_with_all_filtered(self, testdir: Testdir) -> None: - testdir.makepyfile( + def test_lastfailed_with_all_filtered(self, pytester: Pytester) -> None: + pytester.makepyfile( **{ "pkg1/test_1.py": """ def test_fail(): assert 0 @@ -956,19 +991,19 @@ def test_pass(): pass """, } ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"]) assert result.ret == 1 # Remove known failure. - testdir.makepyfile( + pytester.makepyfile( **{ "pkg1/test_1.py": """ def test_pass(): pass """, } ) - result = testdir.runpytest("--lf", "--co") + result = pytester.runpytest("--lf", "--co") result.stdout.fnmatch_lines( [ "collected 1 item", @@ -1015,8 +1050,8 @@ def test_packages(self, pytester: Pytester) -> None: class TestNewFirst: - def test_newfirst_usecase(self, testdir): - testdir.makepyfile( + def test_newfirst_usecase(self, pytester: Pytester, testdir) -> None: + pytester.makepyfile( **{ "test_1/test_1.py": """ def test_1(): assert 1 @@ -1026,24 +1061,24 @@ def test_1(): assert 1 """, } ) - testdir.tmpdir.join("test_1/test_1.py").setmtime(1) - result = testdir.runpytest("-v") + p1 = pytester.path.joinpath("test_1/test_1.py") + os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) + + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( ["*test_1/test_1.py::test_1 PASSED*", "*test_2/test_2.py::test_1 PASSED*"] ) - result = testdir.runpytest("-v", "--nf") + result = pytester.runpytest("-v", "--nf") result.stdout.fnmatch_lines( ["*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" - ) - testdir.tmpdir.join("test_1/test_1.py").setmtime(1) + p1.write_text("def test_1(): assert 1\n" "def test_2(): assert 1\n") + os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) - result = testdir.runpytest("--nf", "--collect-only", "-q") + result = pytester.runpytest("--nf", "--collect-only", "-q") result.stdout.fnmatch_lines( [ "test_1/test_1.py::test_2", @@ -1053,15 +1088,15 @@ def test_1(): assert 1 ) # Newest first with (plugin) pytest_collection_modifyitems hook. - testdir.makepyfile( + pytester.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") + pytester.syspathinsert() + result = pytester.runpytest("--nf", "-p", "myplugin", "--collect-only", "-q") result.stdout.fnmatch_lines( [ "new_items: *test_1.py*test_1.py*test_2.py*", @@ -1071,8 +1106,8 @@ def pytest_collection_modifyitems(items): ] ) - def test_newfirst_parametrize(self, testdir): - testdir.makepyfile( + def test_newfirst_parametrize(self, pytester: Pytester) -> None: + pytester.makepyfile( **{ "test_1/test_1.py": """ import pytest @@ -1087,9 +1122,10 @@ def test_1(num): assert num } ) - testdir.tmpdir.join("test_1/test_1.py").setmtime(1) + p1 = pytester.path.joinpath("test_1/test_1.py") + os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( [ "*test_1/test_1.py::test_1[1*", @@ -1099,7 +1135,7 @@ def test_1(num): assert num ] ) - result = testdir.runpytest("-v", "--nf") + result = pytester.runpytest("-v", "--nf") result.stdout.fnmatch_lines( [ "*test_2/test_2.py::test_1[1*", @@ -1109,20 +1145,20 @@ def test_1(num): assert num ] ) - testdir.tmpdir.join("test_1/test_1.py").write( + p1.write_text( "import pytest\n" "@pytest.mark.parametrize('num', [1, 2, 3])\n" "def test_1(num): assert num\n" ) - testdir.tmpdir.join("test_1/test_1.py").setmtime(1) + os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) # Running only a subset does not forget about existing ones. - result = testdir.runpytest("-v", "--nf", "test_2/test_2.py") + result = pytester.runpytest("-v", "--nf", "test_2/test_2.py") result.stdout.fnmatch_lines( ["*test_2/test_2.py::test_1[1*", "*test_2/test_2.py::test_1[2*"] ) - result = testdir.runpytest("-v", "--nf") + result = pytester.runpytest("-v", "--nf") result.stdout.fnmatch_lines( [ "*test_1/test_1.py::test_1[3*", @@ -1135,27 +1171,28 @@ def test_1(num): assert num class TestReadme: - def check_readme(self, testdir): - config = testdir.parseconfigure() + def check_readme(self, pytester: Pytester) -> bool: + config = pytester.parseconfigure() + assert config.cache is not None readme = config.cache._cachedir.joinpath("README.md") return readme.is_file() - def test_readme_passed(self, testdir): - testdir.makepyfile("def test_always_passes(): pass") - testdir.runpytest() - assert self.check_readme(testdir) is True + def test_readme_passed(self, pytester: Pytester) -> None: + pytester.makepyfile("def test_always_passes(): pass") + pytester.runpytest() + assert self.check_readme(pytester) is True - def test_readme_failed(self, testdir): - testdir.makepyfile("def test_always_fails(): assert 0") - testdir.runpytest() - assert self.check_readme(testdir) is True + def test_readme_failed(self, pytester: Pytester) -> None: + pytester.makepyfile("def test_always_fails(): assert 0") + pytester.runpytest() + assert self.check_readme(pytester) is True -def test_gitignore(testdir): +def test_gitignore(pytester: Pytester) -> None: """Ensure we automatically create .gitignore file in the pytest_cache directory (#3286).""" from _pytest.cacheprovider import Cache - config = testdir.parseconfig() + config = pytester.parseconfig() cache = Cache.for_config(config, _ispytest=True) cache.set("foo", "bar") msg = "# Created by pytest automatically.\n*\n" @@ -1168,16 +1205,16 @@ def test_gitignore(testdir): assert gitignore_path.read_text(encoding="UTF-8") == "custom" -def test_does_not_create_boilerplate_in_existing_dirs(testdir): +def test_does_not_create_boilerplate_in_existing_dirs(pytester: Pytester) -> None: from _pytest.cacheprovider import Cache - testdir.makeini( + pytester.makeini( """ [pytest] cache_dir = . """ ) - config = testdir.parseconfig() + config = pytester.parseconfig() cache = Cache.for_config(config, _ispytest=True) cache.set("foo", "bar") @@ -1186,12 +1223,12 @@ def test_does_not_create_boilerplate_in_existing_dirs(testdir): assert not os.path.exists("README.md") -def test_cachedir_tag(testdir): +def test_cachedir_tag(pytester: Pytester) -> None: """Ensure we automatically create CACHEDIR.TAG file in the pytest_cache directory (#4278).""" from _pytest.cacheprovider import Cache from _pytest.cacheprovider import CACHEDIR_TAG_CONTENT - config = testdir.parseconfig() + config = pytester.parseconfig() cache = Cache.for_config(config, _ispytest=True) cache.set("foo", "bar") cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG") diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 36e83191bcd..80f2a6d0bc0 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -8,8 +8,6 @@ from typing import List from typing import Optional -import py - import pytest from _pytest.config import ExitCode from _pytest.config import PytestPluginManager @@ -93,9 +91,9 @@ def test_value_access_with_confmod(self, basedir: Path) -> None: conftest = ConftestWithSetinitial(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 / "adir" / "b" - assert path.purebasename.startswith("conftest") + path = Path(mod.__file__) + assert path.parent == basedir / "adir" / "b" + assert path.stem == "conftest" def test_conftest_in_nonpkg_with_init(tmp_path: Path, _sys_snapshot) -> None: @@ -361,11 +359,10 @@ def impct(p, importmode): def test_fixture_dependency(pytester: Pytester) -> None: - ct1 = pytester.makeconftest("") - ct1 = pytester.makepyfile("__init__.py") - ct1.write_text("") + pytester.makeconftest("") + pytester.path.joinpath("__init__.py").touch() sub = pytester.mkdir("sub") - sub.joinpath("__init__.py").write_text("") + sub.joinpath("__init__.py").touch() sub.joinpath("conftest.py").write_text( textwrap.dedent( """\ @@ -387,7 +384,7 @@ def bar(foo): ) subsub = sub.joinpath("subsub") subsub.mkdir() - subsub.joinpath("__init__.py").write_text("") + subsub.joinpath("__init__.py").touch() subsub.joinpath("test_bar.py").write_text( textwrap.dedent( """\ @@ -525,8 +522,8 @@ def test_parsefactories_relative_node_ids( """#616""" 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)) + print("pytestarg : %s" % testarg) + print("expected pass : %s" % expect_ntests_passed) os.chdir(dirs[chdir]) reprec = pytester.inline_run(testarg, "-q", "--traceconfig") reprec.assertoutcome(passed=expect_ntests_passed) diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index c20ff7480a8..0b97a0e5adb 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -2,15 +2,14 @@ import re import sys import textwrap +from pathlib import Path from typing import Dict from typing import Generator from typing import Type -import py - import pytest from _pytest.monkeypatch import MonkeyPatch -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester @pytest.fixture @@ -233,8 +232,8 @@ def test_setenv_prepend() -> None: assert "XYZ123" not in os.environ -def test_monkeypatch_plugin(testdir: Testdir) -> None: - reprec = testdir.inline_runsource( +def test_monkeypatch_plugin(pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ def test_method(monkeypatch): assert monkeypatch.__class__.__name__ == "MonkeyPatch" @@ -268,33 +267,33 @@ def test_syspath_prepend_double_undo(mp: MonkeyPatch) -> None: sys.path[:] = old_syspath -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_path_local(mp: MonkeyPatch, tmp_path: Path) -> None: + mp.chdir(tmp_path) + assert os.getcwd() == str(tmp_path) -def test_chdir_with_str(mp: MonkeyPatch, tmpdir: py.path.local) -> None: - mp.chdir(tmpdir.strpath) - assert os.getcwd() == tmpdir.strpath +def test_chdir_with_str(mp: MonkeyPatch, tmp_path: Path) -> None: + mp.chdir(str(tmp_path)) + assert os.getcwd() == str(tmp_path) -def test_chdir_undo(mp: MonkeyPatch, tmpdir: py.path.local) -> None: +def test_chdir_undo(mp: MonkeyPatch, tmp_path: Path) -> None: cwd = os.getcwd() - mp.chdir(tmpdir) + mp.chdir(tmp_path) mp.undo() assert os.getcwd() == cwd -def test_chdir_double_undo(mp: MonkeyPatch, tmpdir: py.path.local) -> None: - mp.chdir(tmpdir.strpath) +def test_chdir_double_undo(mp: MonkeyPatch, tmp_path: Path) -> None: + mp.chdir(str(tmp_path)) mp.undo() - tmpdir.chdir() + os.chdir(tmp_path) mp.undo() - assert os.getcwd() == tmpdir.strpath + assert os.getcwd() == str(tmp_path) -def test_issue185_time_breaks(testdir: Testdir) -> None: - testdir.makepyfile( +def test_issue185_time_breaks(pytester: Pytester) -> None: + pytester.makepyfile( """ import time def test_m(monkeypatch): @@ -303,7 +302,7 @@ def f(): monkeypatch.setattr(time, "time", f) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( """ *1 passed* @@ -311,9 +310,9 @@ def f(): ) -def test_importerror(testdir: Testdir) -> None: - p = testdir.mkpydir("package") - p.join("a.py").write( +def test_importerror(pytester: Pytester) -> None: + p = pytester.mkpydir("package") + p.joinpath("a.py").write_text( textwrap.dedent( """\ import doesnotexist @@ -322,7 +321,7 @@ def test_importerror(testdir: Testdir) -> None: """ ) ) - testdir.tmpdir.join("test_importerror.py").write( + pytester.path.joinpath("test_importerror.py").write_text( textwrap.dedent( """\ def test_importerror(monkeypatch): @@ -330,7 +329,7 @@ def test_importerror(monkeypatch): """ ) ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( """ *import error in package.a: No module named 'doesnotexist'* @@ -420,16 +419,18 @@ class A: def test_syspath_prepend_with_namespace_packages( - testdir: Testdir, monkeypatch: MonkeyPatch + pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: for dirname in "hello", "world": - d = testdir.mkdir(dirname) - ns = d.mkdir("ns_pkg") - ns.join("__init__.py").write( + d = pytester.mkdir(dirname) + ns = d.joinpath("ns_pkg") + ns.mkdir() + ns.joinpath("__init__.py").write_text( "__import__('pkg_resources').declare_namespace(__name__)" ) - lib = ns.mkdir(dirname) - lib.join("__init__.py").write("def check(): return %r" % dirname) + lib = ns.joinpath(dirname) + lib.mkdir() + lib.joinpath("__init__.py").write_text("def check(): return %r" % dirname) monkeypatch.syspath_prepend("hello") import ns_pkg.hello @@ -446,8 +447,7 @@ def test_syspath_prepend_with_namespace_packages( assert ns_pkg.world.check() == "world" # Should invalidate caches via importlib.invalidate_caches. - tmpdir = testdir.tmpdir - modules_tmpdir = tmpdir.mkdir("modules_tmpdir") + modules_tmpdir = pytester.mkdir("modules_tmpdir") monkeypatch.syspath_prepend(str(modules_tmpdir)) - modules_tmpdir.join("main_app.py").write("app = True") + modules_tmpdir.joinpath("main_app.py").write_text("app = True") from main_app import app # noqa: F401 diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 89f10a7db64..a5282a50795 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -1,13 +1,17 @@ import os +import shutil import sys import types from typing import List import pytest +from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import PytestPluginManager from _pytest.config.exceptions import UsageError from _pytest.main import Session +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pathlib import import_path from _pytest.pytester import Pytester @@ -18,7 +22,7 @@ def pytestpm() -> PytestPluginManager: class TestPytestPluginInteractions: def test_addhooks_conftestplugin( - self, pytester: Pytester, _config_for_test + self, pytester: Pytester, _config_for_test: Config ) -> None: pytester.makepyfile( newhooks=""" @@ -45,15 +49,15 @@ def pytest_myhook(xyz): res = config.hook.pytest_myhook(xyz=10) assert res == [11] - def test_addhooks_nohooks(self, testdir): - testdir.makeconftest( + def test_addhooks_nohooks(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import sys def pytest_addhooks(pluginmanager): pluginmanager.add_hookspecs(sys) """ ) - res = testdir.runpytest() + res = pytester.runpytest() assert res.ret != 0 res.stderr.fnmatch_lines(["*did not find*sys*"]) @@ -70,8 +74,8 @@ def pytest_addoption(parser): config.pluginmanager._importconftest(p, importmode="prepend") assert config.option.test123 - def test_configure(self, testdir): - config = testdir.parseconfig() + def test_configure(self, pytester: Pytester) -> None: + config = pytester.parseconfig() values = [] class A: @@ -90,7 +94,7 @@ def pytest_configure(self): config.pluginmanager.register(A()) assert len(values) == 2 - def test_hook_tracing(self, _config_for_test) -> None: + def test_hook_tracing(self, _config_for_test: Config) -> None: pytestpm = _config_for_test.pluginmanager # fully initialized with plugins saveindent = [] @@ -139,9 +143,9 @@ def test_hook_proxy(self, pytester: Pytester) -> None: ihook_b = session.gethookproxy(pytester.path / "tests") assert ihook_a is not ihook_b - def test_hook_with_addoption(self, testdir): + def test_hook_with_addoption(self, pytester: Pytester) -> None: """Test that hooks can be used in a call to pytest_addoption""" - testdir.makepyfile( + pytester.makepyfile( newhooks=""" import pytest @pytest.hookspec(firstresult=True) @@ -149,7 +153,7 @@ def pytest_default_value(): pass """ ) - testdir.makepyfile( + pytester.makepyfile( myplugin=""" import newhooks def pytest_addhooks(pluginmanager): @@ -159,30 +163,32 @@ def pytest_addoption(parser, pluginmanager): parser.addoption("--config", help="Config, defaults to %(default)s", default=default_value) """ ) - testdir.makeconftest( + pytester.makeconftest( """ pytest_plugins=("myplugin",) def pytest_default_value(): return "default_value" """ ) - res = testdir.runpytest("--help") + res = pytester.runpytest("--help") res.stdout.fnmatch_lines(["*--config=CONFIG*default_value*"]) -def test_default_markers(testdir): - result = testdir.runpytest("--markers") +def test_default_markers(pytester: Pytester) -> None: + result = pytester.runpytest("--markers") result.stdout.fnmatch_lines(["*tryfirst*first*", "*trylast*last*"]) -def test_importplugin_error_message(testdir, pytestpm): +def test_importplugin_error_message( + pytester: Pytester, pytestpm: PytestPluginManager +) -> None: """Don't hide import errors when importing plugins and provide an easy to debug message. See #375 and #1998. """ - testdir.syspathinsert(testdir.tmpdir) - testdir.makepyfile( + pytester.syspathinsert(pytester.path) + pytester.makepyfile( qwe="""\ def test_traceback(): raise ImportError('Not possible to import: ☺') @@ -199,7 +205,7 @@ def test_traceback(): class TestPytestPluginManager: - def test_register_imported_modules(self): + def test_register_imported_modules(self) -> None: pm = PytestPluginManager() mod = types.ModuleType("x.y.pytest_hello") pm.register(mod) @@ -219,23 +225,27 @@ 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: PytestPluginManager) -> None: - testdir.syspathinsert() - testdir.makepyfile(pytest_p1="#") - testdir.makepyfile(pytest_p2="#") + def test_consider_module( + self, pytester: Pytester, pytestpm: PytestPluginManager + ) -> None: + pytester.syspathinsert() + pytester.makepyfile(pytest_p1="#") + pytester.makepyfile(pytest_p2="#") mod = types.ModuleType("temp") 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) -> None: + def test_consider_module_import_module( + self, pytester: Pytester, _config_for_test: Config + ) -> None: pytestpm = _config_for_test.pluginmanager mod = types.ModuleType("x") mod.__dict__["pytest_plugins"] = "pytest_a" - aplugin = testdir.makepyfile(pytest_a="#") - reprec = testdir.make_hook_recorder(pytestpm) - testdir.syspathinsert(aplugin.dirpath()) + aplugin = pytester.makepyfile(pytest_a="#") + reprec = pytester.make_hook_recorder(pytestpm) + pytester.syspathinsert(aplugin.parent) pytestpm.consider_module(mod) call = reprec.getcall(pytestpm.hook.pytest_plugin_registered.name) assert call.plugin.__name__ == "pytest_a" @@ -245,30 +255,37 @@ def test_consider_module_import_module(self, testdir, _config_for_test) -> None: values = reprec.getcalls("pytest_plugin_registered") assert len(values) == 1 - def test_consider_env_fails_to_import(self, monkeypatch, pytestpm): + def test_consider_env_fails_to_import( + self, monkeypatch: MonkeyPatch, pytestpm: PytestPluginManager + ) -> None: monkeypatch.setenv("PYTEST_PLUGINS", "nonexisting", prepend=",") with pytest.raises(ImportError): pytestpm.consider_env() @pytest.mark.filterwarnings("always") - def test_plugin_skip(self, testdir, monkeypatch): - p = testdir.makepyfile( + def test_plugin_skip(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None: + p = pytester.makepyfile( skipping1=""" import pytest pytest.skip("hello", allow_module_level=True) """ ) - p.copy(p.dirpath("skipping2.py")) + shutil.copy(p, p.with_name("skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-p", "skipping1", syspathinsert=True) + result = pytester.runpytest("-p", "skipping1", syspathinsert=True) assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines( ["*skipped plugin*skipping1*hello*", "*skipped plugin*skipping2*hello*"] ) - def test_consider_env_plugin_instantiation(self, testdir, monkeypatch, pytestpm): - testdir.syspathinsert() - testdir.makepyfile(xy123="#") + def test_consider_env_plugin_instantiation( + self, + pytester: Pytester, + monkeypatch: MonkeyPatch, + pytestpm: PytestPluginManager, + ) -> None: + pytester.syspathinsert() + pytester.makepyfile(xy123="#") monkeypatch.setitem(os.environ, "PYTEST_PLUGINS", "xy123") l1 = len(pytestpm.get_plugins()) pytestpm.consider_env() @@ -279,9 +296,11 @@ def test_consider_env_plugin_instantiation(self, testdir, monkeypatch, pytestpm) l3 = len(pytestpm.get_plugins()) assert l2 == l3 - def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): - testdir.makepyfile(pytest_x500="#") - p = testdir.makepyfile( + def test_pluginmanager_ENV_startup( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: + pytester.makepyfile(pytest_x500="#") + p = pytester.makepyfile( """ import pytest def test_hello(pytestconfig): @@ -290,17 +309,19 @@ def test_hello(pytestconfig): """ ) monkeypatch.setenv("PYTEST_PLUGINS", "pytest_x500", prepend=",") - result = testdir.runpytest(p, syspathinsert=True) + result = pytester.runpytest(p, syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) - def test_import_plugin_importname(self, testdir, pytestpm): + def test_import_plugin_importname( + self, pytester: Pytester, pytestpm: PytestPluginManager + ) -> None: pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y") pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwx.y") - testdir.syspathinsert() + pytester.syspathinsert() pluginname = "pytest_hello" - testdir.makepyfile(**{pluginname: ""}) + pytester.makepyfile(**{pluginname: ""}) pytestpm.import_plugin("pytest_hello") len1 = len(pytestpm.get_plugins()) pytestpm.import_plugin("pytest_hello") @@ -311,25 +332,29 @@ def test_import_plugin_importname(self, testdir, pytestpm): plugin2 = pytestpm.get_plugin("pytest_hello") assert plugin2 is plugin1 - def test_import_plugin_dotted_name(self, testdir, pytestpm): + def test_import_plugin_dotted_name( + self, pytester: Pytester, pytestpm: PytestPluginManager + ) -> None: pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y") pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwex.y") - testdir.syspathinsert() - testdir.mkpydir("pkg").join("plug.py").write("x=3") + pytester.syspathinsert() + pytester.mkpydir("pkg").joinpath("plug.py").write_text("x=3") pluginname = "pkg.plug" pytestpm.import_plugin(pluginname) mod = pytestpm.get_plugin("pkg.plug") assert mod.x == 3 - def test_consider_conftest_deps(self, testdir, pytestpm): - mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() + def test_consider_conftest_deps( + self, pytester: Pytester, pytestpm: PytestPluginManager, + ) -> None: + mod = import_path(pytester.makepyfile("pytest_plugins='xyz'")) with pytest.raises(ImportError): pytestpm.consider_conftest(mod) class TestPytestPluginManagerBootstrapming: - def test_preparse_args(self, pytestpm): + def test_preparse_args(self, pytestpm: PytestPluginManager) -> None: pytest.raises( ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"]) ) @@ -346,7 +371,7 @@ def test_preparse_args(self, pytestpm): with pytest.raises(UsageError, match="^plugin main cannot be disabled$"): pytestpm.consider_preparse(["-p", "no:main"]) - def test_plugin_prevent_register(self, pytestpm): + def test_plugin_prevent_register(self, pytestpm: PytestPluginManager) -> None: pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) l1 = pytestpm.get_plugins() pytestpm.register(42, name="abc") @@ -354,7 +379,9 @@ def test_plugin_prevent_register(self, pytestpm): assert len(l2) == len(l1) assert 42 not in l2 - def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm): + def test_plugin_prevent_register_unregistered_alredy_registered( + self, pytestpm: PytestPluginManager + ) -> None: pytestpm.register(42, name="abc") l1 = pytestpm.get_plugins() assert 42 in l1 @@ -363,8 +390,8 @@ def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm): assert 42 not in l2 def test_plugin_prevent_register_stepwise_on_cacheprovider_unregister( - self, pytestpm - ): + self, pytestpm: PytestPluginManager + ) -> None: """From PR #4304: The only way to unregister a module is documented at the end of https://docs.pytest.org/en/stable/plugins.html. @@ -380,7 +407,7 @@ def test_plugin_prevent_register_stepwise_on_cacheprovider_unregister( assert 42 not in l2 assert 43 not in l2 - def test_blocked_plugin_can_be_used(self, pytestpm): + def test_blocked_plugin_can_be_used(self, pytestpm: PytestPluginManager) -> None: pytestpm.consider_preparse(["xyz", "-p", "no:abc", "-p", "abc"]) assert pytestpm.has_plugin("abc") diff --git a/testing/test_reports.py b/testing/test_reports.py index b97b1fc2970..b376f6198ae 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -1,29 +1,30 @@ -from pathlib import Path from typing import Sequence from typing import Union +import py.path + import pytest from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionRepr from _pytest.config import Config -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester from _pytest.reports import CollectReport from _pytest.reports import TestReport class TestReportSerialization: - def test_xdist_longrepr_to_str_issue_241(self, testdir: Testdir) -> None: + def test_xdist_longrepr_to_str_issue_241(self, pytester: Pytester) -> None: """Regarding issue pytest-xdist#241. This test came originally from test_remote.py in xdist (ca03269). """ - testdir.makepyfile( + pytester.makepyfile( """ def test_a(): assert False def test_b(): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 6 test_a_call = reports[1] @@ -35,12 +36,12 @@ 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: Testdir) -> None: + def test_xdist_report_longrepr_reprcrash_130(self, pytester: Pytester) -> None: """Regarding issue pytest-xdist#130 This test came originally from test_remote.py in xdist (ca03269). """ - reprec = testdir.inline_runsource( + reprec = pytester.inline_runsource( """ def test_fail(): assert False, 'Expected Message' @@ -74,14 +75,14 @@ def test_fail(): # Missing section attribute PR171 assert added_section in a.longrepr.sections - def test_reprentries_serialization_170(self, testdir: Testdir) -> None: + def test_reprentries_serialization_170(self, pytester: Pytester) -> None: """Regarding issue pytest-xdist#170 This test came originally from test_remote.py in xdist (ca03269). """ from _pytest._code.code import ReprEntry - reprec = testdir.inline_runsource( + reprec = pytester.inline_runsource( """ def test_repr_entry(): x = 0 @@ -120,14 +121,14 @@ def test_repr_entry(): 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: + def test_reprentries_serialization_196(self, pytester: Pytester) -> None: """Regarding issue pytest-xdist#196 This test came originally from test_remote.py in xdist (ca03269). """ from _pytest._code.code import ReprEntryNative - reprec = testdir.inline_runsource( + reprec = pytester.inline_runsource( """ def test_repr_entry_native(): x = 0 @@ -149,9 +150,9 @@ 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: Testdir) -> None: + def test_itemreport_outcomes(self, pytester: Pytester) -> None: # This test came originally from test_remote.py in xdist (ca03269). - reprec = testdir.inline_runsource( + reprec = pytester.inline_runsource( """ import pytest def test_pass(): pass @@ -183,9 +184,9 @@ def test_xfail_imperative(): if rep.failed: assert newrep.longreprtext == rep.longreprtext - def test_collectreport_passed(self, testdir: Testdir) -> None: + def test_collectreport_passed(self, pytester: Pytester) -> None: """This test came originally from test_remote.py in xdist (ca03269).""" - reprec = testdir.inline_runsource("def test_func(): pass") + reprec = pytester.inline_runsource("def test_func(): pass") reports = reprec.getreports("pytest_collectreport") for rep in reports: d = rep._to_json() @@ -194,9 +195,9 @@ def test_collectreport_passed(self, testdir: Testdir) -> None: assert newrep.failed == rep.failed assert newrep.skipped == rep.skipped - def test_collectreport_fail(self, testdir: Testdir) -> None: + def test_collectreport_fail(self, pytester: Pytester) -> None: """This test came originally from test_remote.py in xdist (ca03269).""" - reprec = testdir.inline_runsource("qwe abc") + reprec = pytester.inline_runsource("qwe abc") reports = reprec.getreports("pytest_collectreport") assert reports for rep in reports: @@ -208,9 +209,9 @@ def test_collectreport_fail(self, testdir: Testdir) -> None: if rep.failed: assert newrep.longrepr == str(rep.longrepr) - def test_extended_report_deserialization(self, testdir: Testdir) -> None: + def test_extended_report_deserialization(self, pytester: Pytester) -> None: """This test came originally from test_remote.py in xdist (ca03269).""" - reprec = testdir.inline_runsource("qwe abc") + reprec = pytester.inline_runsource("qwe abc") reports = reprec.getreports("pytest_collectreport") assert reports for rep in reports: @@ -224,33 +225,33 @@ def test_extended_report_deserialization(self, testdir: Testdir) -> None: if rep.failed: assert newrep.longrepr == str(rep.longrepr) - def test_paths_support(self, testdir: Testdir) -> None: + def test_paths_support(self, pytester: Pytester) -> None: """Report attributes which are py.path or pathlib objects should become strings.""" - testdir.makepyfile( + pytester.makepyfile( """ def test_a(): assert False """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 3 test_a_call = reports[1] - test_a_call.path1 = testdir.tmpdir # type: ignore[attr-defined] - test_a_call.path2 = Path(testdir.tmpdir) # type: ignore[attr-defined] + test_a_call.path1 = py.path.local(pytester.path) # type: ignore[attr-defined] + test_a_call.path2 = pytester.path # type: ignore[attr-defined] data = test_a_call._to_json() - assert data["path1"] == str(testdir.tmpdir) - assert data["path2"] == str(testdir.tmpdir) + assert data["path1"] == str(pytester.path) + assert data["path2"] == str(pytester.path) - def test_deserialization_failure(self, testdir: Testdir) -> None: + def test_deserialization_failure(self, pytester: Pytester) -> None: """Check handling of failure during deserialization of report types.""" - testdir.makepyfile( + pytester.makepyfile( """ def test_a(): assert False """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 3 test_a_call = reports[1] @@ -265,9 +266,11 @@ def test_a(): TestReport._from_json(data) @pytest.mark.parametrize("report_class", [TestReport, CollectReport]) - def test_chained_exceptions(self, testdir: Testdir, tw_mock, report_class) -> None: + def test_chained_exceptions( + self, pytester: Pytester, tw_mock, report_class + ) -> None: """Check serialization/deserialization of report objects containing chained exceptions (#5786)""" - testdir.makepyfile( + pytester.makepyfile( """ def foo(): raise ValueError('value error') @@ -283,7 +286,7 @@ def test_a(): ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() if report_class is TestReport: reports: Union[ Sequence[TestReport], Sequence[CollectReport] @@ -338,14 +341,14 @@ def check_longrepr(longrepr: ExceptionChainRepr) -> None: # 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: Testdir, tw_mock) -> None: + def test_chained_exceptions_no_reprcrash(self, pytester: Pytester, tw_mock) -> None: """Regression test for tracebacks without a reprcrash (#5971) This happens notably on exceptions raised by multiprocess.pool: the exception transfer from subprocess to main process creates an artificial exception, which ExceptionInfo can't obtain the ReprFileLocation from. """ - testdir.makepyfile( + pytester.makepyfile( """ from concurrent.futures import ProcessPoolExecutor @@ -358,8 +361,8 @@ def test_a(): """ ) - testdir.syspathinsert() - reprec = testdir.inline_run() + pytester.syspathinsert() + reprec = pytester.inline_run() reports = reprec.getreports("pytest_runtest_logreport") @@ -396,12 +399,13 @@ def check_longrepr(longrepr: object) -> None: loaded_report.longrepr.toterminal(tw_mock) def test_report_prevent_ConftestImportFailure_hiding_exception( - self, testdir: Testdir + self, pytester: Pytester ) -> None: - sub_dir = testdir.tmpdir.join("ns").ensure_dir() - sub_dir.join("conftest").new(ext=".py").write("import unknown") + sub_dir = pytester.path.joinpath("ns") + sub_dir.mkdir() + sub_dir.joinpath("conftest.py").write_text("import unknown") - result = testdir.runpytest_subprocess(".") + result = pytester.runpytest_subprocess(".") result.stdout.fnmatch_lines(["E *Error: No module named 'unknown'"]) result.stdout.no_fnmatch_line("ERROR - *ConftestImportFailure*") @@ -409,14 +413,14 @@ def test_report_prevent_ConftestImportFailure_hiding_exception( class TestHooks: """Test that the hooks are working correctly for plugins""" - def test_test_report(self, testdir: Testdir, pytestconfig: Config) -> None: - testdir.makepyfile( + def test_test_report(self, pytester: Pytester, pytestconfig: Config) -> None: + pytester.makepyfile( """ def test_a(): assert False def test_b(): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 6 for rep in reports: @@ -431,14 +435,14 @@ def test_b(): pass assert new_rep.when == rep.when assert new_rep.outcome == rep.outcome - def test_collect_report(self, testdir: Testdir, pytestconfig: Config) -> None: - testdir.makepyfile( + def test_collect_report(self, pytester: Pytester, pytestconfig: Config) -> None: + pytester.makepyfile( """ def test_a(): assert False def test_b(): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reports = reprec.getreports("pytest_collectreport") assert len(reports) == 2 for rep in reports: @@ -457,14 +461,14 @@ def test_b(): pass "hook_name", ["pytest_runtest_logreport", "pytest_collectreport"] ) def test_invalid_report_types( - self, testdir: Testdir, pytestconfig: Config, hook_name: str + self, pytester: Pytester, pytestconfig: Config, hook_name: str ) -> None: - testdir.makepyfile( + pytester.makepyfile( """ def test_a(): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reports = reprec.getreports(hook_name) assert reports rep = reports[0] diff --git a/testing/test_runner.py b/testing/test_runner.py index a1f1db48d06..8ce0f67354f 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -2,26 +2,28 @@ import os import sys import types +from pathlib import Path from typing import Dict from typing import List from typing import Tuple from typing import Type -import py - -import _pytest._code import pytest from _pytest import outcomes from _pytest import reports from _pytest import runner +from _pytest._code import ExceptionInfo +from _pytest._code.code import ExceptionChainRepr from _pytest.config import ExitCode +from _pytest.monkeypatch import MonkeyPatch from _pytest.outcomes import OutcomeException +from _pytest.pytester import Pytester class TestSetupState: - def test_setup(self, testdir) -> None: + def test_setup(self, pytester: Pytester) -> None: ss = runner.SetupState() - item = testdir.getitem("def test_func(): pass") + item = pytester.getitem("def test_func(): pass") values = [1] ss.prepare(item) ss.addfinalizer(values.pop, colitem=item) @@ -29,15 +31,15 @@ def test_setup(self, testdir) -> None: ss._pop_and_teardown() assert not values - def test_teardown_exact_stack_empty(self, testdir) -> None: - item = testdir.getitem("def test_func(): pass") + def test_teardown_exact_stack_empty(self, pytester: Pytester) -> None: + item = pytester.getitem("def test_func(): pass") ss = runner.SetupState() ss.teardown_exact(item, None) ss.teardown_exact(item, None) ss.teardown_exact(item, None) - def test_setup_fails_and_failure_is_cached(self, testdir) -> None: - item = testdir.getitem( + def test_setup_fails_and_failure_is_cached(self, pytester: Pytester) -> None: + item = pytester.getitem( """ def setup_module(mod): raise ValueError(42) @@ -48,7 +50,7 @@ def test_func(): pass pytest.raises(ValueError, lambda: ss.prepare(item)) pytest.raises(ValueError, lambda: ss.prepare(item)) - def test_teardown_multiple_one_fails(self, testdir) -> None: + def test_teardown_multiple_one_fails(self, pytester: Pytester) -> None: r = [] def fin1(): @@ -60,7 +62,7 @@ def fin2(): def fin3(): r.append("fin3") - item = testdir.getitem("def test_func(): pass") + item = pytester.getitem("def test_func(): pass") ss = runner.SetupState() ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) @@ -70,7 +72,7 @@ def fin3(): assert err.value.args == ("oops",) assert r == ["fin3", "fin1"] - def test_teardown_multiple_fail(self, testdir) -> None: + def test_teardown_multiple_fail(self, pytester: Pytester) -> None: # Ensure the first exception is the one which is re-raised. # Ideally both would be reported however. def fin1(): @@ -79,7 +81,7 @@ def fin1(): def fin2(): raise Exception("oops2") - item = testdir.getitem("def test_func(): pass") + item = pytester.getitem("def test_func(): pass") ss = runner.SetupState() ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) @@ -87,7 +89,7 @@ def fin2(): ss._callfinalizers(item) assert err.value.args == ("oops2",) - def test_teardown_multiple_scopes_one_fails(self, testdir) -> None: + def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None: module_teardown = [] def fin_func(): @@ -96,7 +98,7 @@ def fin_func(): def fin_module(): module_teardown.append("fin_module") - item = testdir.getitem("def test_func(): pass") + item = pytester.getitem("def test_func(): pass") ss = runner.SetupState() ss.addfinalizer(fin_module, item.listchain()[-2]) ss.addfinalizer(fin_func, item) @@ -107,8 +109,8 @@ def fin_module(): class BaseFunctionalTests: - def test_passfunction(self, testdir) -> None: - reports = testdir.runitem( + def test_passfunction(self, pytester: Pytester) -> None: + reports = pytester.runitem( """ def test_func(): pass @@ -120,8 +122,8 @@ def test_func(): assert rep.outcome == "passed" assert not rep.longrepr - def test_failfunction(self, testdir) -> None: - reports = testdir.runitem( + def test_failfunction(self, pytester: Pytester) -> None: + reports = pytester.runitem( """ def test_func(): assert 0 @@ -135,8 +137,8 @@ def test_func(): assert rep.outcome == "failed" # assert isinstance(rep.longrepr, ReprExceptionInfo) - def test_skipfunction(self, testdir) -> None: - reports = testdir.runitem( + def test_skipfunction(self, pytester: Pytester) -> None: + reports = pytester.runitem( """ import pytest def test_func(): @@ -155,8 +157,8 @@ def test_func(): # assert rep.skipped.location.path # assert not rep.skipped.failurerepr - def test_skip_in_setup_function(self, testdir) -> None: - reports = testdir.runitem( + def test_skip_in_setup_function(self, pytester: Pytester) -> None: + reports = pytester.runitem( """ import pytest def setup_function(func): @@ -176,8 +178,8 @@ def test_func(): assert len(reports) == 2 assert reports[1].passed # teardown - def test_failure_in_setup_function(self, testdir) -> None: - reports = testdir.runitem( + def test_failure_in_setup_function(self, pytester: Pytester) -> None: + reports = pytester.runitem( """ import pytest def setup_function(func): @@ -193,8 +195,8 @@ def test_func(): assert rep.when == "setup" assert len(reports) == 2 - def test_failure_in_teardown_function(self, testdir) -> None: - reports = testdir.runitem( + def test_failure_in_teardown_function(self, pytester: Pytester) -> None: + reports = pytester.runitem( """ import pytest def teardown_function(func): @@ -213,8 +215,8 @@ def test_func(): # assert rep.longrepr.reprcrash.lineno == 3 # assert rep.longrepr.reprtraceback.reprentries - def test_custom_failure_repr(self, testdir) -> None: - testdir.makepyfile( + def test_custom_failure_repr(self, pytester: Pytester) -> None: + pytester.makepyfile( conftest=""" import pytest class Function(pytest.Function): @@ -222,7 +224,7 @@ def repr_failure(self, excinfo): return "hello" """ ) - reports = testdir.runitem( + reports = pytester.runitem( """ import pytest def test_func(): @@ -238,8 +240,8 @@ def test_func(): # assert rep.failed.where.path.basename == "test_func.py" # assert rep.failed.failurerepr == "hello" - def test_teardown_final_returncode(self, testdir) -> None: - rec = testdir.inline_runsource( + def test_teardown_final_returncode(self, pytester: Pytester) -> None: + rec = pytester.inline_runsource( """ def test_func(): pass @@ -249,8 +251,8 @@ def teardown_function(func): ) assert rec.ret == 1 - def test_logstart_logfinish_hooks(self, testdir) -> None: - rec = testdir.inline_runsource( + def test_logstart_logfinish_hooks(self, pytester: Pytester) -> None: + rec = pytester.inline_runsource( """ import pytest def test_func(): @@ -266,8 +268,8 @@ def test_func(): assert rep.nodeid == "test_logstart_logfinish_hooks.py::test_func" assert rep.location == ("test_logstart_logfinish_hooks.py", 1, "test_func") - def test_exact_teardown_issue90(self, testdir) -> None: - rec = testdir.inline_runsource( + def test_exact_teardown_issue90(self, pytester: Pytester) -> None: + rec = pytester.inline_runsource( """ import pytest @@ -306,9 +308,9 @@ def teardown_function(func): assert reps[5].nodeid.endswith("test_func") assert reps[5].failed - def test_exact_teardown_issue1206(self, testdir) -> None: + def test_exact_teardown_issue1206(self, pytester: Pytester) -> None: """Issue shadowing error with wrong number of arguments on teardown_method.""" - rec = testdir.inline_runsource( + rec = pytester.inline_runsource( """ import pytest @@ -335,14 +337,19 @@ def test_method(self): assert reps[2].nodeid.endswith("test_method") assert reps[2].failed assert reps[2].when == "teardown" - assert reps[2].longrepr.reprcrash.message in ( + longrepr = reps[2].longrepr + assert isinstance(longrepr, ExceptionChainRepr) + assert longrepr.reprcrash + assert longrepr.reprcrash.message in ( "TypeError: teardown_method() missing 2 required positional arguments: 'y' and 'z'", # 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: - testdir.makepyfile( + def test_failure_in_setup_function_ignores_custom_repr( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( conftest=""" import pytest class Function(pytest.Function): @@ -350,7 +357,7 @@ def repr_failure(self, excinfo): assert 0 """ ) - reports = testdir.runitem( + reports = pytester.runitem( """ def setup_function(func): raise ValueError(42) @@ -369,9 +376,9 @@ def test_func(): # assert rep.outcome.where.path.basename == "test_func.py" # assert instanace(rep.failed.failurerepr, PythonFailureRepr) - def test_systemexit_does_not_bail_out(self, testdir) -> None: + def test_systemexit_does_not_bail_out(self, pytester: Pytester) -> None: try: - reports = testdir.runitem( + reports = pytester.runitem( """ def test_func(): raise SystemExit(42) @@ -383,9 +390,9 @@ def test_func(): assert rep.failed assert rep.when == "call" - def test_exit_propagates(self, testdir) -> None: + def test_exit_propagates(self, pytester: Pytester) -> None: try: - testdir.runitem( + pytester.runitem( """ import pytest def test_func(): @@ -405,9 +412,9 @@ def f(item): return f - def test_keyboardinterrupt_propagates(self, testdir) -> None: + def test_keyboardinterrupt_propagates(self, pytester: Pytester) -> None: try: - testdir.runitem( + pytester.runitem( """ def test_func(): raise KeyboardInterrupt("fake") @@ -420,8 +427,8 @@ def test_func(): class TestSessionReports: - def test_collect_result(self, testdir) -> None: - col = testdir.getmodulecol( + def test_collect_result(self, pytester: Pytester) -> None: + col = pytester.getmodulecol( """ def test_func1(): pass @@ -489,8 +496,8 @@ def raise_assertion(): @pytest.mark.xfail -def test_runtest_in_module_ordering(testdir) -> None: - p1 = testdir.makepyfile( +def test_runtest_in_module_ordering(pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest def pytest_runtest_setup(item): # runs after class-level! @@ -517,7 +524,7 @@ def pytest_runtest_teardown(item): del item.function.mylist """ ) - result = testdir.runpytest(p1) + result = pytester.runpytest(p1) result.stdout.fnmatch_lines(["*2 passed*"]) @@ -547,8 +554,8 @@ def test_pytest_fail() -> None: assert s.startswith("Failed") -def test_pytest_exit_msg(testdir) -> None: - testdir.makeconftest( +def test_pytest_exit_msg(pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @@ -556,7 +563,7 @@ def pytest_configure(config): pytest.exit('oh noes') """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stderr.fnmatch_lines(["Exit: oh noes"]) @@ -570,22 +577,22 @@ def _strip_resource_warnings(lines): ] -def test_pytest_exit_returncode(testdir) -> None: - testdir.makepyfile( +def test_pytest_exit_returncode(pytester: Pytester) -> None: + pytester.makepyfile( """\ import pytest def test_foo(): pytest.exit("some exit msg", 99) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*! *Exit: some exit msg !*"]) assert _strip_resource_warnings(result.stderr.lines) == [] assert result.ret == 99 # It prints to stderr also in case of exit during pytest_sessionstart. - testdir.makeconftest( + pytester.makeconftest( """\ import pytest @@ -593,7 +600,7 @@ def pytest_sessionstart(): pytest.exit("during_sessionstart", 98) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*! *Exit: during_sessionstart !*"]) assert _strip_resource_warnings(result.stderr.lines) == [ "Exit: during_sessionstart" @@ -601,9 +608,9 @@ def pytest_sessionstart(): assert result.ret == 98 -def test_pytest_fail_notrace_runtest(testdir) -> None: +def test_pytest_fail_notrace_runtest(pytester: Pytester) -> None: """Test pytest.fail(..., pytrace=False) does not show tracebacks during test run.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest def test_hello(): @@ -612,14 +619,14 @@ def teardown_function(function): pytest.fail("world", pytrace=False) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["world", "hello"]) result.stdout.no_fnmatch_line("*def teardown_function*") -def test_pytest_fail_notrace_collection(testdir) -> None: +def test_pytest_fail_notrace_collection(pytester: Pytester) -> None: """Test pytest.fail(..., pytrace=False) does not show tracebacks during collection.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest def some_internal_function(): @@ -627,17 +634,17 @@ def some_internal_function(): some_internal_function() """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["hello"]) result.stdout.no_fnmatch_line("*def some_internal_function()*") -def test_pytest_fail_notrace_non_ascii(testdir) -> None: +def test_pytest_fail_notrace_non_ascii(pytester: Pytester) -> None: """Fix pytest.fail with pytrace=False with non-ascii characters (#1178). This tests with native and unicode strings containing non-ascii chars. """ - testdir.makepyfile( + pytester.makepyfile( """\ import pytest @@ -645,28 +652,28 @@ def test_hello(): pytest.fail('oh oh: ☺', pytrace=False) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*test_hello*", "oh oh: ☺"]) result.stdout.no_fnmatch_line("*def test_hello*") -def test_pytest_no_tests_collected_exit_status(testdir) -> None: - result = testdir.runpytest() +def test_pytest_no_tests_collected_exit_status(pytester: Pytester) -> None: + result = pytester.runpytest() result.stdout.fnmatch_lines(["*collected 0 items*"]) assert result.ret == ExitCode.NO_TESTS_COLLECTED - testdir.makepyfile( + pytester.makepyfile( test_foo=""" def test_foo(): assert 1 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*collected 1 item*"]) result.stdout.fnmatch_lines(["*1 passed*"]) assert result.ret == ExitCode.OK - result = testdir.runpytest("-k nonmatch") + result = pytester.runpytest("-k nonmatch") result.stdout.fnmatch_lines(["*collected 1 item*"]) result.stdout.fnmatch_lines(["*1 deselected*"]) assert result.ret == ExitCode.NO_TESTS_COLLECTED @@ -677,7 +684,7 @@ def test_exception_printing_skip() -> None: try: pytest.skip("hello") except pytest.skip.Exception: - excinfo = _pytest._code.ExceptionInfo.from_current() + excinfo = ExceptionInfo.from_current() s = excinfo.exconly(tryshort=True) assert s.startswith("Skipped") @@ -698,10 +705,10 @@ def f(): excrepr = excinfo.getrepr() assert excrepr is not None assert excrepr.reprcrash is not None - path = py.path.local(excrepr.reprcrash.path) + path = Path(excrepr.reprcrash.path) # check that importorskip reports the actual call # in this test the test_runner.py file - assert path.purebasename == "test_runner" + assert path.stem == "test_runner" pytest.raises(SyntaxError, pytest.importorskip, "x y z") pytest.raises(SyntaxError, pytest.importorskip, "x=y") mod = types.ModuleType("hello123") @@ -712,9 +719,7 @@ def f(): mod2 = pytest.importorskip("hello123", minversion="1.3") assert mod2 == mod except pytest.skip.Exception: # pragma: no cover - assert False, "spurious skip: {}".format( - _pytest._code.ExceptionInfo.from_current() - ) + assert False, f"spurious skip: {ExceptionInfo.from_current()}" def test_importorskip_imports_last_module_part() -> None: @@ -732,14 +737,12 @@ def test_importorskip_dev_module(monkeypatch) -> None: with pytest.raises(pytest.skip.Exception): pytest.importorskip("mockmodule1", minversion="0.14.0") except pytest.skip.Exception: # pragma: no cover - assert False, "spurious skip: {}".format( - _pytest._code.ExceptionInfo.from_current() - ) + assert False, f"spurious skip: {ExceptionInfo.from_current()}" -def test_importorskip_module_level(testdir) -> None: +def test_importorskip_module_level(pytester: Pytester) -> None: """`importorskip` must be able to skip entire modules when used at module level.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest foobarbaz = pytest.importorskip("foobarbaz") @@ -748,13 +751,13 @@ def test_foo(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*collected 0 items / 1 skipped*"]) -def test_importorskip_custom_reason(testdir) -> None: +def test_importorskip_custom_reason(pytester: Pytester) -> None: """Make sure custom reasons are used.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest foobarbaz = pytest.importorskip("foobarbaz2", reason="just because") @@ -763,13 +766,13 @@ def test_foo(): pass """ ) - result = testdir.runpytest("-ra") + result = pytester.runpytest("-ra") result.stdout.fnmatch_lines(["*just because*"]) result.stdout.fnmatch_lines(["*collected 0 items / 1 skipped*"]) -def test_pytest_cmdline_main(testdir) -> None: - p = testdir.makepyfile( +def test_pytest_cmdline_main(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest def test_hello(): @@ -786,8 +789,8 @@ def test_hello(): assert ret == 0 -def test_unicode_in_longrepr(testdir) -> None: - testdir.makeconftest( +def test_unicode_in_longrepr(pytester: Pytester) -> None: + pytester.makeconftest( """\ import pytest @pytest.hookimpl(hookwrapper=True) @@ -798,19 +801,19 @@ def pytest_runtest_makereport(): rep.longrepr = 'ä' """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_out(): assert 0 """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 1 assert "UnicodeEncodeError" not in result.stderr.str() -def test_failure_in_setup(testdir) -> None: - testdir.makepyfile( +def test_failure_in_setup(pytester: Pytester) -> None: + pytester.makepyfile( """ def setup_module(): 0/0 @@ -818,24 +821,26 @@ def test_func(): pass """ ) - result = testdir.runpytest("--tb=line") + result = pytester.runpytest("--tb=line") result.stdout.no_fnmatch_line("*def setup_module*") -def test_makereport_getsource(testdir) -> None: - testdir.makepyfile( +def test_makereport_getsource(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_foo(): if False: pass else: assert False """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.no_fnmatch_line("*INTERNALERROR*") result.stdout.fnmatch_lines(["*else: assert False*"]) -def test_makereport_getsource_dynamic_code(testdir, monkeypatch) -> None: +def test_makereport_getsource_dynamic_code( + pytester: Pytester, monkeypatch: MonkeyPatch +) -> None: """Test that exception in dynamically generated code doesn't break getting the source line.""" import inspect @@ -849,7 +854,7 @@ def findsource(obj): monkeypatch.setattr(inspect, "findsource", findsource) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -861,7 +866,7 @@ def test_fix(foo): assert False """ ) - result = testdir.runpytest("-vv") + result = pytester.runpytest("-vv") result.stdout.no_fnmatch_line("*INTERNALERROR*") result.stdout.fnmatch_lines(["*test_fix*", "*fixture*'missing'*not found*"]) @@ -896,12 +901,12 @@ def runtest(self): assert not hasattr(sys, "last_traceback") -def test_current_test_env_var(testdir, monkeypatch) -> None: +def test_current_test_env_var(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: pytest_current_test_vars: List[Tuple[str, str]] = [] monkeypatch.setattr( sys, "pytest_current_test_vars", pytest_current_test_vars, raising=False ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest import sys @@ -917,7 +922,7 @@ def test(fix): sys.pytest_current_test_vars.append(('call', os.environ['PYTEST_CURRENT_TEST'])) """ ) - result = testdir.runpytest_inprocess() + result = pytester.runpytest_inprocess() assert result.ret == 0 test_id = "test_current_test_env_var.py::test" assert pytest_current_test_vars == [ @@ -934,8 +939,8 @@ class TestReportContents: def getrunner(self): return lambda item: runner.runtestprotocol(item, log=False) - def test_longreprtext_pass(self, testdir) -> None: - reports = testdir.runitem( + def test_longreprtext_pass(self, pytester: Pytester) -> None: + reports = pytester.runitem( """ def test_func(): pass @@ -944,9 +949,9 @@ def test_func(): rep = reports[1] assert rep.longreprtext == "" - def test_longreprtext_skip(self, testdir) -> None: + def test_longreprtext_skip(self, pytester: Pytester) -> None: """TestReport.longreprtext can handle non-str ``longrepr`` attributes (#7559)""" - reports = testdir.runitem( + reports = pytester.runitem( """ import pytest def test_func(): @@ -957,22 +962,22 @@ def test_func(): assert isinstance(call_rep.longrepr, tuple) assert "Skipped" in call_rep.longreprtext - def test_longreprtext_collect_skip(self, testdir) -> None: + def test_longreprtext_collect_skip(self, pytester: Pytester) -> None: """CollectReport.longreprtext can handle non-str ``longrepr`` attributes (#7559)""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest pytest.skip(allow_module_level=True) """ ) - rec = testdir.inline_run() + rec = pytester.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( + def test_longreprtext_failure(self, pytester: Pytester) -> None: + reports = pytester.runitem( """ def test_func(): x = 1 @@ -982,8 +987,8 @@ def test_func(): rep = reports[1] assert "assert 1 == 4" in rep.longreprtext - def test_captured_text(self, testdir) -> None: - reports = testdir.runitem( + def test_captured_text(self, pytester: Pytester) -> None: + reports = pytester.runitem( """ import pytest import sys @@ -1012,8 +1017,8 @@ def test_func(fix): assert call.capstderr == "setup: stderr\ncall: stderr\n" assert teardown.capstderr == "setup: stderr\ncall: stderr\nteardown: stderr\n" - def test_no_captured_text(self, testdir) -> None: - reports = testdir.runitem( + def test_no_captured_text(self, pytester: Pytester) -> None: + reports = pytester.runitem( """ def test_func(): pass @@ -1023,8 +1028,8 @@ def test_func(): assert rep.capstdout == "" assert rep.capstderr == "" - def test_longrepr_type(self, testdir) -> None: - reports = testdir.runitem( + def test_longrepr_type(self, pytester: Pytester) -> None: + reports = pytester.runitem( """ import pytest def test_func(): @@ -1032,7 +1037,7 @@ def test_func(): """ ) rep = reports[1] - assert isinstance(rep.longrepr, _pytest._code.code.ExceptionRepr) + assert isinstance(rep.longrepr, ExceptionChainRepr) def test_outcome_exception_bad_msg() -> None: From 9ccbf5b89906604dc76daee1fc07c1d26b6b5128 Mon Sep 17 00:00:00 2001 From: Jakob van Santen Date: Tue, 15 Dec 2020 12:49:29 +0100 Subject: [PATCH 0328/2846] 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 56600414df31431f1e0c2d964d0ad6761f83dd5d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 15 Dec 2020 12:39:06 -0300 Subject: [PATCH 0329/2846] Merge pull request #8149 from pytest-dev/release-6.2.1 Prepare release 6.2.1 (cherry picked from commit a566eb9c7085d7732127420bd7ce5ec1f7319fba) --- 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`: From d46ecbc18b74b895b71e257bf07836cd2cfae89e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 15 Dec 2020 20:16:28 +0200 Subject: [PATCH 0330/2846] terminal: fix "()" skip reason in test status line --- changelog/8152.bugfix.rst | 1 + src/_pytest/outcomes.py | 2 +- src/_pytest/terminal.py | 2 ++ testing/test_terminal.py | 26 ++++++++++++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 changelog/8152.bugfix.rst diff --git a/changelog/8152.bugfix.rst b/changelog/8152.bugfix.rst new file mode 100644 index 00000000000..d79a832de41 --- /dev/null +++ b/changelog/8152.bugfix.rst @@ -0,0 +1 @@ +Fixed "()" being shown as a skip reason in the verbose test summary line when the reason is empty. diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index f0607cbd849..8f6203fd7fa 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -38,7 +38,7 @@ def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None: self.pytrace = pytrace def __repr__(self) -> str: - if self.msg: + if self.msg is not None: return self.msg return f"<{self.__class__.__name__} instance>" diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 39adfaaa310..f5d4e1f8ddc 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1403,4 +1403,6 @@ def _get_raw_skip_reason(report: TestReport) -> str: _, _, reason = report.longrepr if reason.startswith("Skipped: "): reason = reason[len("Skipped: ") :] + elif reason == "Skipped": + reason = "" return reason diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 6319188a75e..6d0a23fe0f1 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -366,6 +366,26 @@ def test_3(): @pytest.mark.xfail(reason="") def test_4(): assert False + + @pytest.mark.skip + def test_5(): + pass + + @pytest.mark.xfail + def test_6(): + pass + + def test_7(): + pytest.skip() + + def test_8(): + pytest.skip("888 is great") + + def test_9(): + pytest.xfail() + + def test_10(): + pytest.xfail("It's 🕙 o'clock") """ ) result = pytester.runpytest("-v") @@ -375,6 +395,12 @@ def test_4(): "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 *", + "test_verbose_skip_reason.py::test_5 SKIPPED (unconditional skip) *", + "test_verbose_skip_reason.py::test_6 XPASS *", + "test_verbose_skip_reason.py::test_7 SKIPPED *", + "test_verbose_skip_reason.py::test_8 SKIPPED (888 is great) *", + "test_verbose_skip_reason.py::test_9 XFAIL *", + "test_verbose_skip_reason.py::test_10 XFAIL (It's 🕙 o'clock) *", ] ) From f1a1de22579c11fd1e4301e0c6648ae46a74a1d3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 16 Dec 2020 07:50:02 -0300 Subject: [PATCH 0331/2846] Use manual trigger to prepare release PRs (#8150) Co-authored-by: Ran Benita --- .github/workflows/prepare-release-pr.yml | 42 +++++++ scripts/prepare-release-pr.py | 151 +++++++++++++++++++++++ tox.ini | 7 ++ 3 files changed, 200 insertions(+) create mode 100644 .github/workflows/prepare-release-pr.yml create mode 100644 scripts/prepare-release-pr.py diff --git a/.github/workflows/prepare-release-pr.yml b/.github/workflows/prepare-release-pr.yml new file mode 100644 index 00000000000..848dd78a413 --- /dev/null +++ b/.github/workflows/prepare-release-pr.yml @@ -0,0 +1,42 @@ +name: prepare release pr + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch to base the release from' + required: true + default: '' + major: + description: 'Major release? (yes/no)' + required: true + default: 'no' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.8" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade setuptools tox + + - name: Prepare release PR (minor/patch release) + if: github.event.inputs.branch.major == 'no' + run: | + tox -e prepare-release-pr -- ${{ github.event.inputs.branch }} ${{ secrets.chatops }} + + - name: Prepare release PR (major release) + if: github.event.inputs.branch.major == 'yes' + run: | + tox -e prepare-release-pr -- ${{ github.event.inputs.branch }} ${{ secrets.chatops }} --major diff --git a/scripts/prepare-release-pr.py b/scripts/prepare-release-pr.py new file mode 100644 index 00000000000..538a5af5a41 --- /dev/null +++ b/scripts/prepare-release-pr.py @@ -0,0 +1,151 @@ +""" +This script is part of the pytest release process which is triggered manually in the Actions +tab of the repository. + +The user will need to enter the base branch to start the release from (for example +``6.1.x`` or ``master``) and if it should be a major release. + +The appropriate version will be obtained based on the given branch automatically. + +After that, it will create a release using the `release` tox environment, and push a new PR. + +**Secret**: currently the secret is defined in the @pytestbot account, +which the core maintainers have access to. There we created a new secret named `chatops` +with write access to the repository. +""" +import argparse +import re +from pathlib import Path +from subprocess import check_call +from subprocess import check_output +from subprocess import run + +from colorama import Fore +from colorama import init +from github3.repos import Repository + + +class InvalidFeatureRelease(Exception): + pass + + +SLUG = "pytest-dev/pytest" + +PR_BODY = """\ +Created automatically from manual trigger. + +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. +""" + + +def login(token: str) -> Repository: + import github3 + + github = github3.login(token=token) + owner, repo = SLUG.split("/") + return github.repository(owner, repo) + + +def prepare_release_pr(base_branch: str, is_major: bool, token: str) -> None: + print() + print(f"Processing release for branch {Fore.CYAN}{base_branch}") + + check_call(["git", "checkout", f"origin/{base_branch}"]) + + try: + version = find_next_version(base_branch, is_major) + except InvalidFeatureRelease as e: + print(f"{Fore.RED}{e}") + raise SystemExit(1) + + print(f"Version: {Fore.CYAN}{version}") + + release_branch = f"release-{version}" + + 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, + ) + + 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.") + + # 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( + cmdline, text=True, check=True, capture_output=True, + ) + + oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git" + 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(version=version) + repo = login(token) + pr = repo.create_pull( + f"Prepare release {version}", base=base_branch, head=release_branch, body=body, + ) + print(f"Pull request {Fore.CYAN}{pr.url}{Fore.RESET} created.") + + +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(): + m = re.match(r"\d.\d.\d+$", v.strip()) + if m: + valid_versions.append(tuple(int(x) for x in v.split("."))) + + valid_versions.sort() + last_version = valid_versions[-1] + + changelog = Path("changelog") + + features = list(changelog.glob("*.feature.rst")) + breaking = list(changelog.glob("*.breaking.rst")) + is_feature_release = features or breaking + + 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}" + + +def main() -> None: + init(autoreset=True) + parser = argparse.ArgumentParser() + parser.add_argument("base_branch") + parser.add_argument("token") + parser.add_argument("--major", action="store_true", default=False) + options = parser.parse_args() + prepare_release_pr( + base_branch=options.base_branch, is_major=options.major, token=options.token + ) + + +if __name__ == "__main__": + main() diff --git a/tox.ini b/tox.ini index f0cfaa460fb..43e151c07aa 100644 --- a/tox.ini +++ b/tox.ini @@ -157,6 +157,13 @@ passenv = {[testenv:release]passenv} deps = {[testenv:release]deps} commands = python scripts/release-on-comment.py {posargs} +[testenv:prepare-release-pr] +decription = prepare a release PR from a manual trigger in GitHub actions +usedevelop = {[testenv:release]usedevelop} +passenv = {[testenv:release]passenv} +deps = {[testenv:release]deps} +commands = python scripts/prepare-release-pr.py {posargs} + [testenv:publish-gh-release-notes] description = create GitHub release after deployment basepython = python3 From a1c5111a404aada1444f28cf23bf52cbb00402d3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 16 Dec 2020 07:53:05 -0300 Subject: [PATCH 0332/2846] Fix events variable in prepare-release-pr.yml --- .github/workflows/prepare-release-pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prepare-release-pr.yml b/.github/workflows/prepare-release-pr.yml index 848dd78a413..dec35236430 100644 --- a/.github/workflows/prepare-release-pr.yml +++ b/.github/workflows/prepare-release-pr.yml @@ -32,11 +32,11 @@ jobs: pip install --upgrade setuptools tox - name: Prepare release PR (minor/patch release) - if: github.event.inputs.branch.major == 'no' + if: github.event.inputs.major == 'no' run: | tox -e prepare-release-pr -- ${{ github.event.inputs.branch }} ${{ secrets.chatops }} - name: Prepare release PR (major release) - if: github.event.inputs.branch.major == 'yes' + if: github.event.inputs.major == 'yes' run: | tox -e prepare-release-pr -- ${{ github.event.inputs.branch }} ${{ secrets.chatops }} --major From cab16f3aac4f81090fb376b3cb520804b4ebdd15 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 16 Dec 2020 11:19:45 -0300 Subject: [PATCH 0333/2846] Use transparent PNG for logo (#8159) --- README.rst | 3 ++- doc/en/conf.py | 2 +- doc/en/img/pytest1.png | Bin 6010 -> 40974 bytes doc/en/img/pytest_logo_curves.svg | 29 +++++++++++++++++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 doc/en/img/pytest_logo_curves.svg diff --git a/README.rst b/README.rst index 398d6451c58..0fb4e363b1c 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,7 @@ -.. image:: https://docs.pytest.org/en/stable/_static/pytest1.png +.. image:: https://github.com/pytest-dev/pytest/raw/master/doc/en/img/pytest_logo_curves.svg :target: https://docs.pytest.org/en/stable/ :align: center + :height: 200 :alt: pytest diff --git a/doc/en/conf.py b/doc/en/conf.py index 2f3a2baf44b..e34ae6856f0 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -159,7 +159,7 @@ # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = "img/pytest1.png" +html_logo = "img/pytest_logo_curves.svg" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 diff --git a/doc/en/img/pytest1.png b/doc/en/img/pytest1.png index e8064a694ca8eff0f2167d29ad13609f9a99a46e..498e70485d214c242ab75ce24f93225223fc5f8c 100644 GIT binary patch literal 40974 zcmeFYc|6o@^gpbkER`+EUfGHeLb8QYDf_-x3XweqV@Z;oHpw26o$O;7#-6grjAbw= z+b~Rw!I<$}qx<`N{(AoYJdMpL0d&>)txS$jwMYLv!NJ?Hh(P zG<0a{Fa1$)<*MAhN8o=)AKtxngJz%l-zRw9I~tnIwmUb}js3<~CUqjyAzYm^Pk!#I zyW9{ixXJP+I!m3U-LyTN#b@Sw_z?|-bDgqzj@Ryc$Va~l(=YtQw3OXYv#9Y@A)C#b zWAgK4!UryV zVgonW(R9Sq;HhKaLO!@L34Y->!Bf;gBldqT39$PAtv_3FV^4j_>5!=%Tsd>k!bU=T znSiXU?1a<=8m(MII?e-KD=^BNTI@av*lcvJsi`>`|24Rao}HLLc38=@S8{N0Fl(_r zLn1#c-=5mOCeUo$H1*0cUO-OxeF*NmN&N(SZBtV)0y@##)3eps>{oppT)T(% zOYCvH!6qwP`3>i50j?lk#$8=!3I#MTRWMmuSskXB4MOTdkl_ae zuVf=?+gZW8i~ODssPEFoVJI#SsPEP<)YjIzdptg%n5OLiN5p`7xj8N_E}`KHxO!hh zUO^!t>O7z*?w>%qm_(J&y>Ik~y{QjA6>jLyJ@|&qLqrlC^&5#R=l**p@{k?-jh>z! z(+YwANNF91S+PjV%35^?{~7{xo-=SsV`5%JCMS<+Z`u6!+Qagzl93;ilS>{=KYMb( zSL~VfbLt0QORf9MQ>Q4}omU+rkPTk7@ZH$ilmGj2PbAJU{9vV$#bspzf7jI>dtqIe zy4oIrdvt){mXmxc-1}u{%q;b72Ob`t%x4E;xdB9ayHu!&z9I&1NC|ufEzSY+~xOe zE`xPLfAf@+izKP$_vuUvhNm?OtXzCNnLr>MDWwNjF={rltu3`3>_Cv><2e%(6AD+U zPVoHhIQjh)Jw5Tj?2;x04>V5oCB*&}jn;*(3{GRPJ_R71MKK%CbpNwR(vhSytaBz?xR8jCHZr|LDlRN_M-tX^(I@_sb z_UpxxpWTSyZ0D4fZ5UU`@J~qi)}%rqpH8CE{>oN4wldFp4(GR&P^2oe)m6SR3*2+{ zC)ssnj1|=%Uq(!DM0c+9>jhFD|KI#`zl>j*xITVl!S^X0G%+n`53!r0XWd}Gqq6lY zRIhAz0YcfF`5pc<0`ip3ir;dnf9>B6^>3^Fmv;P1!TzPA|3~N5|7whX^%lro{?(rU zuc^cP_*Ax$c?rC>mcbz~!WVn~0iYiQMG(e>69K=}M3d$0zkmFb!9P3r=L`R@iU(UE zCl10BVnpL0ucl&{G>89!nAAVdUjKLbpG^K41T7W*|K|;$`SH&i{vW-87$krefO@SW zStF7@Oyh{i5H_mSAQD;8#Gt=|!&FlPDixikp=bHqRQellsof-+=QRJdk^WlR|DPWV zsf38$_cD3R{akz{l`;DHb0!$m@>w^`h3NdN=i{|wWTM}+)83X5;=bwm+Lz~)!J)@j z-Wo%0sf&t9UXX-kVU9*-KV?6+RJbhi{P|g>I{}o!stX@KH~btKSwNGT`n@wUJX=aP z_7_Z15WVz?qlY1(N>mBcxSsXkgi#@7nt^{8|4$Ogz|X~K57@-V=LcRzUHQMCe-ij7 zf&ULlV6SpYt9r<&*+HW-d%RNTGL1g{-DRuJX@w-cfoYv)g%+@uI@0V=MCY-_hKI*> zzL^j2mx*xETzEOPixpjQ|B|s=B60?#agdQtE7A3^90VWD1@LC|J_mJ6_?p2SPw=J# z^^l#livi{&&8gpO zUn;3D(-d2|uPTG>20?#%a{Kjm!@Tan-s4hx--D#>PF6j*RY*|K+n&C6803}DV+mK{ zR}jZ;fgAeIn^T>q+5Q(hne-r8(ArIXGFF3E=$)iKPJ<-0c;;>E*BXMaULFGM7x3+* z-U0uhd5t~RwVQS_$m0|sP`5w1X>;&pEOKJI?tpp6EqBh4ezYJp8M(jO=d{8tuxQFa z{e*sr-rDGY{P3z>m7n+dE1z#>e-}eTgGCzcC;k-ymfvEjWLoqumD9|sBC0(lr@<#< zKUpdj=7h*nDbmRNvShxBOwm5$l!G<>eB z8?5&^FJXA$*`B)y^~QBw@IseP>w$D$&w$l3(H;nSAH4`eZjl0Q!~buM3&b6(%~22#flzClK}Zy_Ls(5*qp#$eJgK&yyO3^@j$EWOfFDe z%TY-{&uhpkgeXLr6r$3;y&q;jr%C;!!D4~U`TtoC)(otWMr*(dy7!sV`}axfX@u>& zzjpB}t9Fa3@&f^?Q7z?iVRLG*)WPeM0oc2L8Mw0k>(2gvTKt+Z?#;H1xd0xK*={HZ zenDmHxlfV2?tzds^vGU+Wu|cDim+;x1T~?F{gg-96rwmBn2u24wXg2y1$Y7J>*E1D z0)OqvO@&}YU3boI!;CxU)=xnF1bKdn6s;E~cGRU zX(~OMuf9x@Q&&_!A1LaKdUccC+y9}lgBiVgVURf<=p9ZOYrGto-K^w41{em6^X(vYJzuGpWqldu}h!;e}6o-w-`S{P5%RpjG)%-x}8g-1x8S3gz9h|NQRmO@ z-`ul4J=b?e)0G?21(WYgus0TjsWt61j~)NL(34zQr%iLua`Ee63Qbn9$&h*jBK^F% z;k1lKljE(|l+MKkVtapoNB>$E>{_R*tBp9EK$=~+PD^Nxl%xFKZq|#I7VQjWWmC13 zzEJGIGR`=_;~vLjwiYAj;anV1nN18~oBMM{;hE6l(*Oz#Qbj8-r;$mY+KOS^Sr4qW}7-x+!>SURu4nGexe^w9j~=&2@qdzlIi^Z5^41EW;N3* z&HdV1-w}l}rq^LvSr<$Ae)q1|*KYOA*sJud>3^ImEx*>f9k93eBsRSK^fYVP@85~C z+znzLFa9u&17ErP=6qwY1MS#8$6Vu?T2YqL1Yh(Z`h>utK*+|`)Y8(@I1~0yCn)?? zcF86Kk{+TyOh;=I_=9X>jm}?f5GwZw+B_Fn9qhJ8>FV;C-6_fWa+VGRakC{ZKf*dI zf3JHuI_)=f^rKtkO%^-XH1w`kPwOuGl@=jI@+;R@VOzXVUCc?ubO);-bKF;-hHh?F zoZAO1S}ttK?*ysiE1ql3hmmZn|#xVdfI1N)8fU>-!ZY)pO;(z>;|i&)h;XenZVt;nXLi z)5qO7W_F8aJNrdvMF!k+n9Vqp?+f(8E>3kdbF9s6*z0C2kRQCLZps~Q-1$i8O`p&v zC>ZruxXMW)0&ZOOToQ_CSo`MDm%)0DbP>Pt>Tvi9mK)OHLe}0>9vzfrN9wKhe_~e6 zH9StlIszvBY&&8^TwkQvHMrv#R|@RJiobadXV@o-egT1TW@Di2qKQvSR`b6F!-J74>$Cu&_hEf(M5_q zKdYYnbqt=|K*Q=ouCxu5)iim|q9L6*{8UTzs6%tIpWoSq`hRrH)QUIM`WOV0ws18e(?)f+KO8PmfP^A@Cb4#Cky4 zON)M@MwMi6wBeOqq2X^NS}~)gdg=2WhH+Hdxz)BLM7k&4YBgu4WAssr7hl!jp(<*f zX0oIH%47CVVg=LjDyp7F3+u<7;rhAS(|LJcrt}Di_?>c5GOWJ$B z=aK5A_LF@ovk=Skw*@v96E5RE3h|qM%-Y#laOd;(bD5QE@>w1(u*x@mqVuG5<303B zsk)i(Bay-*$LN9I#Tt2|R#Vm=>rcnI6vs~faIEgDkC0O*#mtEy6;!sbB(IbAM#-uT zf5Lw!h&@)57x*Ub1VTDJTGaK{ELIfC@6ewCw|XgOSCtclo*O^o?)J*t6!j4`+j`uW zx!E*Vdm^*HAG5nbVPGDIVmWu73wt;!KfqK-ylP82L`EXZuW7(5#X*P~d?Y+#1>IR6 zTS02p&ye)&({;-()#J~FuXWAWCI#@wqK(0(2EGe1dq{i)r_7{xh<3nknNJ}-AH>2- zzDk#La60ItgvCt8DSLiL`?>svUec|q5T>{9gHQ6NFq2kF^w_KP)}7Kr>_|_4$GSj< zp9ve3tZ`QF=TiEe(D`i0-?aZ7l z*(QImcmXA*LY8t-`KZ$T37+)|?9oF^XtU1Jv65?>0nCw}>1PAI;swhmg2=O3l@0PJ z%~_wlQtd=s(5xU=V5&osXY&A35`EQfbO_p_3g`T%_G&4gvuzm}aq12i(HHrEh z*W%l%{ezU4UxX!c1i9otm5G0EJy3?RY4ZXY&%&n)k6+hOM@j zwLrC^`8$XFx+1ux+DPq9ZI}aeS)gQd1ac>jGa_tjD!?X)7AM(9!$pgeo{|qO^CX_( zWNLWFB`%17+P!y(=71p)WC^Zej}O|3Uj&Ffn2$MNW_G}~WPkLSh$d!AWMtexI-wSu z(++7M%2Pyr8{Bgrc-KX7pmH;IM*?bx=St5oi63OT0+KMlpDq1cz2Vm>v&a!RXFn%_o+q+`(U>_jlrDx#!n6t+1b`QpbCrmg;NxG zkiHBpZk6$2slWM(?_M;ed-W-?v)O5?oq%+>S%MoZ>s&XGC47II6(_pX*^hU=57JJV z<7mM^atm!^O>HO43YNYJlkZy#iB8bKZf} z@3`vo1)9FRewUzI+Th@F%W3wM`@;?emH@2XKu+h8BgAo%NQo$PSu{WNoX4?mmp$B7 zzg5Laxo96{-jSuKy2Y1gj%-6bk1X5C=1)LP3P)$hxb=E4sWR?tFrLH2ZpQo0whzAsnH1vgHe^Yxv} zIOAS(Uf`>InRcT?Qjy^dbRE_XU5}?n*RoP6pTzQy3&jg6+BBbXPP4NxhxAN8!j`D0 zI^#oj2e%g{SL2p{P2J}Jk3yb5Jwl|{wUwFrszOoR7i{eFEsp2E+F95b;z}`FmUB_n zH_D9`Uxwb9Rq>mD6#|MrK>tL=VrN9Vb&yrrsXu{>xvceS>hJWBN<;}`?_L@4x_2?=7i@l^+Ih>ayEw-hbxyY*y!Ch$iObYaa{+ z_5BRwht{H(-*OlK8J^%T(AcHNwwW9Ay%RF?m=En?{dx^=Pv1qaj0=ZhmxHOVO+~%V z-D>@qYDLhxqe)yw96H@X`$Y9~jb(#ICKq;k(iUpRy+-%<1KyY^-iCx3NXX-(j@CSL zJj9NxuwO1$2#)M%mVW)w_K_<6P*RAVsrfq#)jViIp-P?kts`8r4$8uW^k%Jz&U9&8^V#(cIxgP*4Mww{8U*xC zR7lb6FZ_STK`49U7iq6YKCSYgs_$9ZN!1_OPC3qVkAfb@&aP}ZUt44NADQs0e~+|F zO_g^YZ<|wVFlMnS^fW?0@(;8?Jt>Y6XD`z$D|#Y*FH<$B>v)r5RV~#U^qjs91xC(g z62kD$@8_1`g<^6f9+1xMcApu!y`+_N;4r`VU|rjPc7vzsZ4~16d-P}ra(SCGsg;ND z3G{T5pR1cohQeC3WhC*XAp2T%?&{88RbuH)GS19hgWuoVaGTg2dK6##&BQKnYZjjj=>&Rr6Zvj=21*w!lDLcxBM zdA2v=>eg@>@>pj;r<8b}#3Z`m_t7)lDTW2e{t?a2MXj8>7&j22K&%P6J)frNEpOMg z7T^&(@?v2qVeUK3RM2Tpeb#3uMcjGHp*MvPIdhC7j~Aw9LVZgV@ghbcPI3DU|7ERm z-2k|~+jD38FH_Q5JFag7uql@g_&rSYji>G@8!gR_yuuqFw~3TXPvsvLNApcfNlsYC zq4or#P``0?-V1Kn!W5qbzBhgeHs~g56{?A$ATEDxx=xOhE>6sWyTrF}p>St5d@Gxz zwdxvF;Jr8hYBB`FpVeCejP&6>cIfz182{@A#og4;O{cLmJ$0LG~n+SpkO16S0W z=SZ>)q5Is;q@CX9%AJFr%58V&x^f!o>*d`XduNZfYk98pmEv`tCw1)4H#|(^nk!^` zg|m~K2#BNX2F}VU_M6D*;U%&ogo9<>O*ZOLHDK%vy0;!d?B+KUAwMXz-1j`cF`PWO z5>rudl9#0{=y8i?sEM>>oTv6ne|J~94wWCi&UP#u`^89Qe4ePo}6bNpH+oMw#En3A> z9zeX!++R-Xxj??En&L?-r{)H>(@(d*e(SnFlg^=1A!7HV=SBND0ml>073q5*ws!Mxq)}<4qBBzN) z(r%YLFu`|MdV-9*HHhNuA^E?hON%Xf1MInia-OQ&>i4UCZ({w@ z0QhmIm+*G?Ly1kZN}Ry)wHhhY#VKX42%qq0A}G77SK96FFUXGf%Sh2KKj(P4_;g}whs$C#KIa868u76d6-j)+ z-uFZ~DNol4k!4@URuvz2-a!I=D1b-g3*~hsZ?KG7HA{6`lz*^&-__XF`y9!I3?C;1 z-{M^bA=BBFP`ZD9W88Ku;JTY#j`>=P%8!~es=C z*f><`>9#&k$B4{epIez96rd{_arsfnfa z3=t8geM+ZOmw7P=l@gIA3AO$p2k>t4x=um^BU^o@+Jgn31)iFbCuob3u)wnix_ z@(HLyq$9Jyex=u2H-4nmw6X?OAaKMSiuF3Y{_Xl3X;*qzqS$CWF?3#2;BJ*$bLRGm z*K;;DmCJd;QDtjww7A2NagI&fjjzZfCo6+#oRy8g`fCNSUg8hD*O(#2@#}VG&ZV!% zXmJY;=3T#8Rhw&ueU#?L`K;h*qkRL%He#pe>D~(<9Dlpu#0uLF+GIYaWc1x|e?BhU zulEO2#Sij(wvvf9HnlFYwi!zKUivkkn3wTumJ_fX|6e)}n9h3S%{{pJa%6{Jz}q0L ztq~%P9onz(3%V=IJn1gc6ZaZ#*%(;c(}#6G`a`r+L8`{EF+?AZd*j*jBX%Zztw>~( zN2zyTLd&xRVC%2-JVZUzmh(yH2NcV9UL$3;5bHa=nHLe#vcyh5uPBWnD>mlI{SFTK zpx(KUY|U?cO|z&RRFZu?zNq8UrQ-`OagD=9&#gw6%=rYXZVZrlPFs4H$f>?J=yi*9 zY5T$9O51Din1ag6*v)14@P&@hCcH)PJ-2?Z;^Ch0lV6R9WGJz{dbX30jM_|N8*x#e zb-~)3n(WSYe&b|s_S{&0aqQD!$Tj<31DPHA&l4; z%{;+p7y7+RU6j98o)Xxc^})UKlW?A#oaF!X;j166<%>aHDmyMKHD+^QxjFRAf{i&L`l%_UdRiAiey}o!#da5^pM~ws&~)rUco#@}8PY zEw^2pz+Af=01_Q9gCPVNNB$dn4s z7xBnrKXiX3V`Z^zzca{`6vU6<0zj0&1|EA)lc@OH#+_g!o8P}vA{gtGIj=w$;IbA| z2ltHyesyo^%z<@zT(rVWDH8+K5(Ng|(|b2~mw4~tB0fAKH!0vP_Y!j7KifBEx9gI+ z7|2SCyH|zFinHv}`o%sx#eZnH=#-l|)|%MK49Xh!!nCnwo26wrfYIARCGg{$(CZ7d zcHTutjgUL$OsqREa&`U)5WWynFBPxRRBl zU8QeHI*!03%QWdOKB>E$u`3;g`eh#TgzZvYU?n}OVOG6}b5BzO7w-cP>*m_vW(WSG zV-9)3)w*>xY42j8N~!A3?N`;!g~yd1a%_5I% z>x;Ckz~kR=g*SByJ}jk3RSu9p8;;~#swvG_T8}UWC$I4P+YZn6fL7O|r!-Wnr7O6} zj9!bpzoaOp;%4+vvibdtw1)eofCpc6)9X9dD;W$++EzCf`ib{TUtSRqLn~JZ&t&Wv zZKN2*UpL0|-3VtZ$vN)UfA+Nv*R2HJ?E5(-Sd0;p7yeEM6MUwT1s2_+TMTica=UmZN|J2&4UB}4LUyZJa@XSpHqhsT^?C6*E#RW`}|UNnsr zCToy&W3|$PwxU;do-1|!M0BHXU8Z?-{UzSOr>%0ZM#6QoZxpx==HzwNmLIuvs@n5m zVrX0GyoC5VvbSy!3-k9NgF!4?m;1^)Kfb0X1R*y2gQkY=uVJi z=pU-(nDYdz=7(I%L6GOaFDX1V+B^GoYmGq-;rr#LrZh4c7(n2Qr0k!z`%z%q&=FS^ra~_6yjcP^*&MLlQ^a1 zkZHb|(jFI16tX?mbyVnVn7d|Ukue=B?5A5y(%Od>+?JJPm;2=@%n~Bjw+_V^5|*2X z&%NR9yN7s~bsePw6g@_pck?)Uy0C)1XosceJI=^gIxY(t%hmhcloBdq`iSeNDZ)yn4v1$(y%l60SVK%_gAy zy@8F57A6m4$swX>P?gFBMhtP>#OO9$(X5`2AipPd51wW}bzNfO{5<6By8&|h3na#~ zJ0qPr+|mUE9vTMwW=!OXr^6U*^di;eme6?)y_u`XH-^W5OQVV1c## zVn<95=2qljxI4=gRo1VNBrC)L`*{?k z%QxYry1N(NGR%9BJd%BX54$W;koFSE?j`=9_C? zD*leAL!s{E;?~}vYR}u*8}RnlPxsd#Kr9su529IM0tIj16>)vI$>;X%Q4*<1*c{dM z#X5{y>rE6c`RVOR&tz+y53NrTYU3)3VjBAt&8z651A3cQ?anU1x zy#j%ub}t=gtU*#;h`sd}Kg@1AQ2(>GZqrrd!pnOB^HtfXhg&OSN?lm%WRDZ74?3Rd zZj8}gEbHDT+Zv6}(xbV`!ow2cEK9Uu2~9b-(;L4G_fxqFIpsW6R9!2~4cjlfsh&mM z{BGhx8#X@uWbN8jmT~rSJgM-czM(wr!(~)Od)}qr%9#Uw-Wx*C& zT&z-HK40dxrc7qpZ9ZeZQ7{=nPJ0pl++FWkX|t<={?}>|fCZ=OO9C(mNB`E>u;cUY z6Lk@|_w_*+R+qVfcJ~kP_UNV2`aRcHxQA9jj35BgU%mQrBc1ZzW0Z2_W(t_N;NW_R zgP2hpeTRE@CfL#!GR;{|MT4Lf`{E-2$*#8ky2NqsZdxz7=icsu?9H;98~M3@=?bEa z!D{Xu_ZqL-I@Nw$8(|=}P;w<-0}wkrkU$3Llw6@hS>ld^6uWKE>DErN);HN!}mi?npGxxAIsh0e$$Do3G(I z!wd1pt}ooq_bh-y0&S~mX`9qQ7 ztu7&JlA4LONAb@{K->diMw)&JU4SxP8Ot)B+p(a~P;4-}sL@Yq!i(Y1veIt^@R@#_k&h0P}w4on}G4prhgbu}${JsIsNt}hm3n3~e9h$C@4vYC2| z<3-uptLwk^q52Io*HN5;5AeHUFN;m&1Fp@Ve`JQwBYPb1miuYzwWjeE567D*!SB~= zxu;4rK#>K|DS$hT?+H&f74T)LyZ74}fTrGw+6Z z26Zsu6|H=CUicxHpg0Sz3CPTU5QOAbhaR%^kf%{ zmLBKde3{m-vva6MW3;BUVTl2?yCAlia0s}Mbi@e(9hhTq!S!y!4?P)rw7c^p@3)Xr zJ?*_i)tOaiFCeZRWR92LnP5~wgQ}~@QuX`|m2+WDme$z;)*n3h;;SEy``YZgwJPW& zm}Z5X^>AEy(6;t-HM!H1@%^~m7YhDTe4>SUh{l$;O$l5A?;mf2`vIxE@NG{;A0%1w zE%$UoNuQ#B3m={$17`Qd@2aFYIs!>dTS(6-1it^N!v8w;?impe$K$6*eBTSwsswI0gp!hsFG=C!UY4+(+{dZ)2?h7B z{?JbVU81#*>tj=zo?F}eroG$kI*D!Uwk`{W17SeMR-Yg@hSb~Mo@1`o)A3&L1G~+t z(O5s>?GJ?*lo#ZvH(84*EO7c5HR(KGi3lo8k9^MpnPkCt5IZbBa(W4z?OmI*Xj<@ApgH3qaag+@b!(Yoc3Zsn{|!q zcKl%vC8=|5qKMxO6F|MwJ4=}QxLz*PU+FCGR|1!wNwwTB(r`KF;W&J*t)feqAH$b_ zyI@Oq)ym=}Vg$>dOtv=TIrf+hR<>iRy>W*3+j0QadDyw=a6gR2C-^5MI}6)M*nZ)n zZbvqX$d8Mry|1F`NZPF~E7K-U5Aad@izt-V|wBTk0?`iWA&mL|gUW-}ZdmBF9{5{tW}CS~S!xb1a zJ1J~MTGNK{n-n?p5R_$S0{A~^V1egc>06mD^?7=?9`03qUbwgn|M{NAv^SIVbd1>w z3v(!3s$l@m^=J#p@80tI3fOPB!mrfvUfgh^a=V5&zPrNoxUhS3=_e!>73f}g>vd16 zg8N-@$7g{3O8=fQJGPSFKKM^laVAfw>}&a8p11dh2FIPx<<*y%kbk$)9^-w*U}IiA zI27u=mpfz@QgTzX9!1T@lfMah>=$YOF7a;@3R5N7FrfAo8KxgW(-UQ{^}5se)4P2E z6btp1*K`~7j%F0!X5isYdD|VPy!Mn_+5xK(NPB{5nm_jqUae>P!MZ8AL(}VT^=*^V z;wh6hum0nLFsiQny3zEG@> z_A~nVGhqKLy~Ik7eCH|;meSiECqLle4n?HD3G`gj%sE{IR#A4*@Ms}HV8JsUI`?{( zHjLGfILtE^feUFUH+5ZcKPSC|)dt2RBBR(A6tB&ZjhcT1l3bgb`kpk!jWGt)FOq3r zB-UBIHI$SgcI9xWlHe2M*(JWOgKfW93?;e+(Ip(%HLA&ViS4im!`%)zo;Vs&4fMY(tvYz|upF*r3xn?Fj<(`yOF5heO3qQW5 zv-vU8&RhlC7#t!pg7u}vB^YHH05HP~w!T^g{k-+`=J)Ib-S2?@Sa>Q4m!&i7L$3J* ztY*^;%!P=7!Ni(@MZ^LB@}a%CQJDpBjYP%~ca$(cY}#UWJnh}O4~D_CKTNe50&zd( zE&JN3S5iOAfDI=5Fkv`JWKuAQU~}qXgsOAkg?D_oj==rJ0u@o)>Ff3WPO-JYiXY9m-W29(PO#yELec334p znDMxDm`Gy7y)h}7Ybb~Clv>J_>O()Hp17_X*z!b8;q-KQiG{@{#U?Qlj0z3#IfAvdGS zj^znkr#hOeGv|*~M)6 zboUe>-diFV2iizO8jto4+;-=sJWz99zMzXQ{a zC>j9pp+!hYkGAsd7);LK<42bRu#S(|`lhV*eA0jT@45;=y3DOD%!$_gAl}fU+3DS< z@&Lo=>DPc{V0RfqvtE@v8IRQ|kG7QCb;_2jcj=7fVm3MlibTVbkmAoRX$H&XPh&L7 zumE(NJ^D>ixD*e^dyF(8-&maWICdSQ`o#W~7)KX%xUn0PRgfv+9cM^6t+An=Wom$} z4>HLn>_jiqqn|j24EM7&ap&>?7m?ZjLXLsNeI zM8Ll@@tIRPX8Iit4?7S43a?V3)bvGoir^96T!SGzoRW}E9We?($DP?No z2-qUOtpzfp=nFrr%Tui1r~sKfqjaVD$=iole-EDiWd1&@{6$K7|UDUKFt344G0^mg30RY zeUHVAmCI%8ln4lw;4ZVGVeayicBxM3rX6cFckdv38gICG)N!2maLm>Sw=5d~6f5W9 z*_2%{M6%iSDz>u%skjD0+Q{t#T^uIL%_WH$f6Gvk3`3Ch6Dobul>hFz!F zk$}fxU8|m*qyp-Av-1Ve&*Xbh#76)V^9D(cu?WJ=M$nFf$$#f zXE>=UFsya&QNDh|^~*ph7Pl7t?#KS#eQZ64gY^ZMR`}KR9{Ias2yGFXI%)1|o8ba|05=&rO8Y`V7a)PSO3f-$zJq_dd++WVb|1 zmqkk44VNwai_*k3;d<3CmM4H3m5pH*>yJU{_pWJI(zCME(XY0~mU|jjMDQ5Nc&zl~ z5>?=!tI-`(&!5tAlE_>%Y+1&+pW)Pdl#0=b4O#c6D?ujwY=}ZvQIHM;1?Ea_Ab7ug zk5t4DU~B4yWWAWL+t%da51!Ci+;`9h=f3;b7j!Gm_XN8hRYL0-7lfS7+7gjg5C^@~ zV7dnRW;pJoO8#)+~o18V<#gcUQpESs(;TE613gt)~PWh?tg^Vfgpc6 z&Y!aDnP|BnH)j-OE2V1o)t_RTJx%e0n;cMTRnd^t#Pq?a=RO?Y3b5Ylz4X)0m5Z}; zP^Evw{6A|j#MYTPu)spJbCGTU-qh2pa!{ZmkftvcRc9jw`G=J2g^DD7!^XD1+(6wd!-0n7A%G+^RDs2^KLqej$KVCohowBbuIRb?tInMY`v(!} z4^I4Gq)70}`sX|IFwK9(DbQX4|#O(6@S`H)4J{I8^B{q>Tl}dE2`STCJ+$>nt@AG8d4)9Eabyd;%PX zdd_>8IGwM5?g+6B_~smB?~`_%-|BWiHbLpq1_7YfF0()DZ85qL$4}_6-sb|{;zFPI zBZ#88Lq$o8ZJu!*V3O8_O6a=N2QLi83WQ6HssxB`@qJg-bF~_M_n?53=!QSpFgujg zL?v}gGd%HyI8g0p+O;48N=qWRL_opM+7_H^(WD*`iM`-u~X z6I_svt6J=T$-;8}XaffOd%Hna0X24hB(0ZuuPRtp)`sVpXn=&OK`j${2{=Oo;yVlM z-8aHw6xbQ|+xwL^7tQ;9-Hoa9U)KN8y7Hk={~Q#@2S#t#j0+6ta8MD;woHe5`CYd3 ze#b>{w2Y6L!|jgp?fS~0pRm!(<7(&8!a7V~?9~P3s|P=KPoOOy8Dd~@83sQJDzdX4 zdXFy|Wof*sS_7%6ns%;M(1{(_7jo8oUEh`E^Ze7O=TPv_>Gv3G+pHgSxO-ae!|wN# zZ|thweDT)OYsaD7o3swoC98i@9S4opTsM*GP!M}#TPxllA-)3p?8~vPJg_gaaz_*m zFSc9ry>mLT;3%JPOr)Ma{^xJ@+nBz3&G2E!Yumir*ResMX{l??Zpwtb#T!xz=EAT4 zjK#viv9#;_AZ>Ej&(hdJhd6@bF558YXW;cGJ!6v$!FOlg_8>dBqDsR>7ymE_c{omH zwI)WbA@_>TA0lFTM#W2+>>{yk@D~I^ywdE%p|YXNJbToDd^Yv9=x+pBm#Uf|d0;!8 z;wH|U=ijR+bzB2|z>O!0J$7K4x)V>b&BX9h3?X`#LzS>6IfCE$IaSpUsZ_nuIK=Fs zWskaAxo7Vgw;kgVls<>^+YhO882d!jI;g2LFM=|d^@i&g?Qm?J6jc(XtDFVHiyeV* zkwMGgvk^NnliMWG-AiHn!;|cw1OUwJiGjbC{Z zp}Yg>?eA~$^gh?;#8DVi1wyYfyBpG>>p_IQ|J zP!D~;GsyvNdMRXxr$H;#`x zQ}l9`n9yt-KyuyWBL8JpqvA7|82*t_K;^awIDH;K&vUfnba>S&9J2{~e&07vqhLX7 z>9w^>%O}C6fYYCmoVcyF}C?;EJUiaajh<3gBeeH%+iJ*yU46 zVgX4NrTc9+7K87+AQX3$$qUaAJPcoW`x2=M{m39613+_%ZL0AzzckM;FdvyK$D~Ab zRR8*T=0p~h-1`0Pk08pKC?HpB03S4KCo-bEi9%D}6!Y=xy%A4Ec6E}2*ld~2MEyPw zjn&s_0$NJOAKwLDAGT65&w7rxTl?V3sx+0?<@5Xd+X!6VlUJ0|*r0wO<5asJx4(2INDa z2xq<3w|`#Yss*pK1{}1(z={)$b7v#@20(EOvnw{7tN#)x(UNk~7Zlp>F#%W4wba$S zf}O`|`PWI%DgbiqphgR`3!hdkTcBj0axA%4`1s`7=JjPl(}5I4c<-ussF_mh_x{@| z9trLyd)IS2P3FQ>m>&*%6B=k23iLQ3pgHAZ(YRO8eN1y?_i*@Ok)DBp_kI~2>YmA&f3$?aN`&2+fYsP#Cq}DX#&te?6hBrQeEH>m$uDtb%(6rF}dOa<6H@O|zI>!c| zdG<*XcNRDm$ZXb+Yh<~LpBi8t2|dEbY&EfWBD?eWO*MQgUO(TXbd7+R z)XAu^HdB3b-__k|>iBIZi5@-Ac!!>k=I4ZTZ^G$6x2Go8Z7tam|YL3@!x4z z{~q`9d_mBeZJeP(MQ56S6w*TNVB=2!gdlKfPY8e6$tTxw>@*|Yy*@dwu^N!Qc6&k#x;8Wg%C%s-v%AyYQ5!j_ zv)4Wqj{!!Pj#y4Ne`ti~0Lxnd`5SPaRrYD;QK9)wV3m7?-6`+rYx{LQ zR4I3Bb=iP4Pn`a+`d*!7^F;VzHux(yR#g5^q%ua4G(c`(v}6ULGU1*ttod#(0;pzNcq$unC`Iqh^Y&>MJATK8 zvTw>Bb{q;U{0l17eNIGXY=+{WD0ZVP{k?kGj&opYM;b2qeu+C68C*V{o)H6%>cT}@ z>b9#T(}!7ra_Citw*CK{8vrRYkmG7>XQ?N={_=+X<3L?1e=&f+{To#N790j#+>Hvh zVr+1nxu#f$Z50NW`ww2I{9o<8`9IX}_Xn($rAQ^pIxX+0Bq7;NMM<({-(}zTeT+$p zWJ^N!q%v7%EZK%J_OUNn#!h6N>@gTK?rU^EzW3w)A3ozJ*X#8<=Un@B&Uv15LG{Dc zF9Vxs*vZmv0m#uU)<0i0?`^0XV**S{IPG{b=EhtAcD_PV0uzyD-gZL*BXq$eePH`_ z-^y}N02=M{Q<*Cx`U1t8Q zzl!71tw1Z{y;Ut0dU_%I#BuAdMlj=yEZFjus|bz5&Fkq=62J=_JS^SdV6zz$3{`NV z#@%^XD)8u4mAOjc9HaM|5jpM{IfXo&Q#sK%tdZL!bG{*MCJ)E@27eqEK@W%gpD;-JDC9h27wozXR>XkY zMHn_6nYDs2Da@QG^Y%W$$L0j&FNhPoaeD6w=-cT!{IzrM7A3YR$;+MyxPuco_g*{? z=$#qbM(Pd1x3gISoy(r6`9p%_>PDc5&*pw_*`lK-7g=S*Ub6vE?V~KC)J$sVzGL`} zbML+mhU)N`cU^No@c&0Co9<>3aFpliWrX?NJ&@SqPX)G`NF683Cf?OJi?#itQzmw} z%pqb8oVvejOl&y@6hYluP1tjWOFh>?4>+yAEx6J`RTRh~@6N)Zv`&bKI!daVF$h!B zy$L;-oPfU=*3~1^^B?{BA@}pk<2ss!<3dAgOB~lfzj-NRhMwg=L zMUUPmtDJ5cqP{u)@Eq*q8?z%EAsxeZ4>;cOKi)-S=U=I+K2F|%!*Hvc`F59;Ji82? z5Q6jNq+MUBu=!DeF2S>BE0VT!_}V?2CsOKs#a^pN$1;7I zOtU!1lnd)7aWHr7-xm-~U;}9$2A?53%r+!9QcHec;y!Ne@<~b|GH-1DR=`t5GRS6e zkuCZ7id#Q$R!;2{u(*EQ6He!b&>Z~=bwV|h5@Ak%#u_t-12w`QfS1xqUZ~0LCtJwK zIE~WtqKEl*%PcFY>z^NOD}(jnmgkAFnH&Axiw3@T-%gkUgD>?|0XMx zFVNevWa2F$_&xJe07&yW$sL?jS0;AwP3sVALS4lx6#remxHGh!9VlVs4P6V-!e5$IB! zqqX^AR|eW}R4DVIodT|*zeEe+(JdC=<`xEbh|A04SBj)7g73Hq$bhKcf*D)@%#w7& zeb&JdG+!ij`(z(3&fFv8(UH>InkMhp0?DMXZ6%sEB34~bF)?jd{1Zvd=e51@pygn& zfY}%koH=y!kGN5{Fx43S_)n#&_4HYwkC>);uiN$}@c`7dzIUfEKiUvfwYRC28C|;k z=#bu{5Petu3N#3F=MmexJVm~DYV%y|<}ZxIbMcM2#s`bB(bFS=o|b|_lTOpm_NPAX za9#=(mi(ZuR_TUzv7u|W!ph(L<+D9vOT~WQvBDtiZn%2)(76*duYuz-ON?2Lmtn_X z09QxfaT$p$@nX~d0-x#=a#Wb&_0GVc8EZ-Qq-jR{HX{fnA0<0Ja}@6jRNDw4TX=X7 z+7fsiINy~9G-y2bFEAbH!AS}~sSqrewx_Fby7s{o1n>+Iyl?GR209CDdcSv7%ir)< zcM!0Iknk=GUMWC%)IMu0eqizQ8D;8%{?Q!141T0a#O_9uu|8xe9@mDYICmZ`9xV5a zH|TcN@J$znm~T=i{#Jb{*P(x*>&Y^f?V1J_BTh`xUlXiQyqhX-t}(liYsTLD#yQK8 zFom3tdF59*E4sxTB9`2+g3CSLI~HnBJY3(d0xglv{&3v?v?dI19CO~>ThIG4N#;b2 z4c(`|ceSq;JI4QOmPUBCFukJ~T(;wiHfGPOs)EfhAtPVSP4i^mK7c-6*u5D`@Dq$r z{KJU1I?JYRo~5GF)tM?bgr_7{1muzoE|Rvk@rx5f)umSSCo= z`yUZAv>Quvmh>sehvtom zxcugCxCj3*YV{SWcg`1+M1eD7h?0NuFV5IfnPj z7~XaZRMlk2{4m|z>s1h5Bvl6#V0rJ*<(~*IW~HrVnSe(dc1t`t6St&iH0`f&Ics;P zl_CH8^>itzaqFwDh5k0v#xH}O#h)!*d#RSKO!yDrsLF3e<&6nCTzL1DI^EZbLZ3wW zTg>cBULXq7MB8NAy^+ z&@B7KfpW|U+P8;JvlXaIl=nDfq{9MjISDMj9A7zXjDK$utFCJQ=#&Wo4>>{-(#CW* z?WNtZNMk((m0bVy_o>uBph}wRl?VgGl;PMxj|ki(-{Pku7P_?w zK2!WYCGv0kG&A;FtZl=lf4P3KQt9dmVeV|j6RhMZ4<5t@kl=ng=<&W*RrtJ zB73kncY3>vP)ZXi9(&z~owaNtahq&Po;k%$r*~Ybl5vH{J5=$JvdM6jV!%d&ubA_j zip7;LAHQCXlQ#UpPh5naP1y{6s>8r(QlvP2f3Y*=L(m0pe*yhUHowlQKp|H8VlmA$ z;OqH*(9;q8LqIS%dkrmXPiMe!df<{f&bIBtb%jQ?PfRKrEep0)2Pi8Qv)sUL^0B$G`Kwc?^M`R+gY?sVi^<4Pt_hmq95G$ zz^}r!A9zuZ$A5?mnI8E9mFp78_^uMcb5kMZ;go{GSg!<{atAQEjV`sSANB5Zu-2F> z&x{95Bu??t-SA-^%SNp%E)G9Z7PJnFXP#J%W{^-^7c>_Lbfo{OSLHi8cxqRz5aHBWA4U(UF16~j}fLWSjk^Le|@6=w6@%J6grE@lR6q@p`N<2kv)2sMCUr87z>Asyc)lMqoX@ZPk5^P7`b{`F%b#sVP62EYugc;BFpI6rp;p9-#)e)etCOcEQ(CUPk`mAE zffw;?99=}^B)RYt`wJpanI5l6HlufUuq&|DiOh%O@wTq9x|>t|vP75bn+zstUa6=5o5(Vz-9^gCB*!bjZr4 zC-q(PJ?;grb~Z2y!t0BBu?}#-byE3r#?y4AV4Fcj312snkO;eMk@g&cXny8ATkP?- zAiefakeynr*I>S(apP*`fLe&*0b5KF!EW!@LL7HOMBTQ?#)&veU{=Hz; zo=jmMQMZ-Uj?bcI;ceII)<+MC*9072nKf%%HjAczFN&i~8KfqDoojE}3LZr{2(b`aH~17K8w-h&H#Tv7b%e zd?+BGJ)6kAxWZnQiy|8N*L_P#h^XH76mc@tM-XSZ8UM?|Nyx5CY}VT1@T|+3`{WUe zU(?VR-|_(3*vGg(10xrB*(Askj8gsRu~elr|DEyDHc zvq&%UX@?V3c&}6r1jFcw>&+)u2j-2)=X9HHU5qYR%s<7cF9?6>*>!dF4o2B4NlqY? z^dq(43|)P`4C9IoD|&HBXVA=!7$qx4JXrO)MV4?h%7ta)p$N^(_(o5n$sEI5H|s*e z_NPZS651z=M~%LX)z>AgZ1t9VKJ%Grts6A#9i4F^?e7_%Zyw~#j7fd6`eoQ>$;rcc z#Mqr<-q5(M^lQa+O5WHg+V|d(JF*?Quqm^Z`CwPBsKxEQ$U->5Ul`q4NTPm@uN=~| zOzo9uQn<{pD9Vw zi2vHHN>y{>c`%t2)8~CvJO0>V_QD}wcavo+DJ={0tPyUy8(Psf4kmQ=WkN|MZjR^f zg-TptSLG*C!(GfWb;yE>|CB9G*-iU0`^p8 zyRK7Jtg>>JM?xs)-e*b{3q7e=uTi&mb+C49u&CX4h1(t>x93NJ|NYSgqJS{iJ=@m( zGM4|hBxoz$xszi>tMsn11h|9Dc|phz&J4YvXiJTV@403^~7}y| z-Reh-#7lv@pCdAI8rY!w&q+nW-wJX2%d;w?+V3mlMc@Vk_ZKp&^iX4u8^nqVhb{;D zI(*9_x80~tE3sCMeriz3-{H2ZJ$|up%s6lh>-=D z049)Osn97+ zioqVqz_4ON5kPq(kSl1jdE0k(Hq~x8>?pBZ5AOfcZs}u{LPnR`EA5XYLP|`^g~|ta z!r5YRo(VA9y+}73eoPYV2CjAN>uJv>Urw8a@)Z)Jz2nrx_ic^=Ckt4_-wrpZ-~__0 zW9F6Lx+N>opu%%zz}Y;(k}vx?T_;OX$_1d9mNO+l+>WjYqsHKmtM4LOD#{U_J}^pS z+4ZW2Zu(CxrWD*`OE)Wy*-~sJU!ipU#l=(UybJxmhS^xxy;rtlzgAG$td0(i%u&jDV=SsKRuKmD{`HGQx%!8n*@y$6mgI|Rmx$`s> zj4vw3^8rNi%L8%R0jrVd>Cgp_&fxzPUuudL*a$v~tHxVN3x>FP1~uvQx9m@U=Q^I5 z)R=g-%&uLflA=rnr%OrYtz?|2@6V=GA66EK$?FFCCgJQJ$q+J@=r(Xu#GpDdB=w!d z`lqP+?Tv49x>sd#i9$!` zCYaL-g!JWOb{Luj)^To6fJyfIcVt{mLVPY(VUt+_n&-fEz1)iHT8(jh4>li%SIJd$ z5f^{k25)YcKWMkG|5V}ofqd2794;ISFdt5NJbdD(oSqZ0>w)MNd#v#pL0ex!HH<&h z!)iJ$&>-XY5gK~ZZxDL9I`BEWods>j_#nvV*wY|uIeJD?FRy55#Xi-OCm|xGXro&M zqTDgu$NrJz1Sa|TtAgMoUEh;mXP|q2q|FhF!<9b<`b0kX0i=WJrHuB%GIOa$JOcy6U&3@?u+{cy??o}jY_j^A;| zfd2C--iiZKt!pngtQFO2zOQj9dtVvM*A{f|+P(=<>=;R9|F|ptEh{F(o+y!g{0KqU z_}9Bw&jX4P2>lw|_1J$}G|*x6QF8y)P==)*!Gf6Il`%cj3ke}(-0pj9wM4v=%F=ON zZ`c#kFpF}|^HEGB8Xv3$r&Ns0xZk&>?5h#xQJx1^UxdBVKT3iTw$3O)w9R%+2{m}L zWm(^s?Hm%!qv(&oy15;*uqQ-X$gCAMG3zswoEHRY*~zDhteutCsXMoQVYU!{^is-b zVDX%dwRQWu1c`#YX~!OR3`2O(j7tL8IAhYH-dH#mUK2oyfK)Ts4S3s%SpSOFl zg8R=vaOVAMTj$9+8gDbo1?;;fMhzto?)ExqI}4T+IUrr6)@Jq<}brEWB|P;v~?oA9W1B zY^qY~rWzjfP`dV1`;{>aVcY0!VYFHWe|HX!(&g_u`Nk-1jVR&{sp5WH>KEKm$HRkz z{;eL2Sr4q1H;X;#RP0I_99VYpxfv|pM{O=LmTR9(1@I?FYvOxzyeCHrM!bmPR3 zf11c!{CeupeKLW-%{Kq9&tG0H|DHq8Y@{QPs@RAc=UZS2? ze{1InWih^8eJJeEkDx?U{1G*5h{iMZ~q}FQGnj3m(?s9~9RS~7r z_RPIi$|$jJuQ)P3v3`Ui ztm`kWX%Y~Nr=lNqj@%<+Sy{$bi3|ynG0sq+2dgUu-42$QE+a~P8aY`b$9#80Tnabz z9Ydli=t0z`tJ3LmJmyFyuN$nI zDM~XM4{|N6mZr=0 z_h069SSAlHIAA}mch?PGUj4K5`>11Mlu2AenL4d%(h0vQ{?{a!3Gg9de|rzBo7=h`8iKj1>l zJs%iU32nNb?vjaeos)E({!f(nZG;=;Te3!2QhI+%FW%dchvxd79zXd=vbozRd< zk}}yV-3_ly7~NS}#U4)C`Ln6L^0AR)ugv ztX(-2qF|J~B5{4!@j^Xwi2jqQiP~DzXnZ+K8%(yaCWP$N(eo)VX`@-z#YK;dSO`6&@!)HdH40o3VK=^SBp=q^n6Jdi5`>d+70F6BG z>R6KUZe%yZwQ>`#o44*N1J7g#c}a0VWSn3nO!AFI;B?l;hKzX0zR-THc6s$?IP%ga zBF0rx^PtCyFfEYzWJ+P={xm8bzSbFfNYP&hLH_H3tMr!_(eW<{00J_N>6=d)Hq6LB zT^nn`Ty@k?zDYVa+V*GURVpJs;3nraYG`n>YR;rM zS~5ec5S-r4y06-T;EIdxxYfOJirgIg@t){hn=^O$m+Wm=o$5uLf?38U`ua9cr@JD7 z5o&SlT6?%m2zh4-GIB+I=bY^WW{~X0k!8~B&!|Eh-WHZpc_`d!RoKe>bGLIx{=B$| z5tZnRYfEYL)ppC>6?G50B}r|VnGgEJUZK&!oav}R!lj&~;rH^i(4$oh#*F^iTsCka z$q2&snGX z6OLnK3qkixCa=il8K2$gVNC$&tKu#-IzukQ_40IF;JRt%&l1kVSbubF)d8YK;uq?^b7|3}9C;@*!qet&g6s)2sU9p(kqnUq*xnU=Nt}gC zW`mY8U#ZJJf|EJdN!#o7a(kY=EIv0R)e1VZ0naHirVjr@ZP-o=X3ZYWH(mU#UH4~|LDWb9Tbu&`tN9duX z2QLX%2wRojDDQ*HDVb3j^`Wk?H$F4#qRK*!z7YC~Do^8BU{DFF1sz{;97J-V32Yrv zG>T!|H(jGL83X%1BCWQH(`UAD6E^|)=h(`X5HjQ7{O$++(B^{!o_b=wqv5egWhrj1 zuM*4WCH9)}3Nmeu3Uc7@_YZgrjc$UF&Wnk17q+I;f@R6ik+0zdwH$ z^7q(^!z-=^7~ZLTXdj@DpZN`vFzS3GT|7#c86ru^Dj~A>X6Q-CzPo=bAKF8&P-V+c zmc|!E!eFq>xog&Un#xVeVgRU!VYD^qfn?W6XU&hq03~nZ(YvLHUk}-kH31gt0J6k-YOI^5 zVE%%BwSw47QYLd{JW}>^%!;R%PIC_knDDMc41UCt^)Q*9j64zA{i=)FoO%6PRJ8Kr1+xr->t5xfwya zn?-sx@ESE@ljeMG)7POH-&SPQBExfY;+JA(t$dxPCjt3-F4ti|rhH^0*KJmyG<`Xs zfN$o@&ol$rYR_sjVOR8Ickz_n?=O*(#3i>(S6@&GHIU_oKXRT&zeeFY$``7?BabS* z?AQg8h(Q+KSXDQ+rnCJH>}bTYZui_D6d7~YV5c&T^~qi8pO7>gg{Gp72pa~7t~Wdj zD}?-E{=jE~6<*NjxdCW)%paqQpK2>!KY%l0W>qaG*$VV--%(ORyr9NJ(9H}lnrZtC z2EvYLR;ynt!sW`Vd&*75HT$Omci64;c=<{`@%cr59!?C~G6eDhwRuEyDkIHvbqr)U zKhwrJm;Xq@VC=$HGODs^CKbEQsS#l`^{tMz^19>hau~H8;`qFKtR-3}*0&yDt$L8P z@(5D)c`eYF3VuETszV!9uwJ zRR7Lp_t?NC|7wOaERfTP!OT~`v!AuM$6t__mydP#7VVuL%19aAR`vMoVV7>D!(vhv zU=l)dfE+u%qSC!^PgLBoWz0sK@F-p9bz8n+C7x_SY*-I8wQ?;+1Dj6LvE@>P#)$OZ zKF=nIAwj~Y_@56e%M z3|?9fu^(c55CSU(PW&bA3kp*8ttB1i)A}XdhQ(h)Ok>$qLz{>o>u$1(P;p+KC4$P< z#QZIt#Zys-j5!al$pUvKAc4O{Kd{EL+B|k>5db~zsI-yO z`g=iNRG_(Y&3n{jGv}3XT|rg*ZVRPIhzR^XlJPUQOU^wVaQ&l_(YL-78D z5P^=~;=XtY};>YThe;ex&85 zd+hQ>+pA}XweY<<7EPhtmaU<8K>QFb&~h#Hdw~U&jJesG%~6K>@rEO zwaO*0S>sBBx@NsriNw)HEyXb6SP>Vm9hz{x8QS73f2HOw6sjOKJ~BiJXy^IG%Cc&oVx6IpbsoyEE_X1hbrP+=P||iZDzCha#Z@y!YT@0|epv`EdMUwd%kkrt>SiFV6fk4;=CrI@$nEJI`w1hTHxqbrh2YcjX2TCAIBRIv2B$wG9XQ-e!l zGc5D8*H#>Zv!oYnAHH~g-HgmXDtshA8^nK6dCeMVerrhVR;iYGH0IXKJ4&%9F>UD9 z+*ewnuKM2l>5V2JDTUSQ9lpz4vt?&P3l=3#_}z;BH2VQX-R5~METI=Xi}XKvdTAj!vg#4lfeb5rzt}bQr|Ms3(Ys7p&Y~x)hY$QfzUe_EaX9a zXlBYN(a^Q>Y%Hvsi8S#n%znM(@!wKYetoq0x!6_JSv}5mqOvqMa@R;w{#WVBX}-&y zlBGKs%2ZFrdL3~&J@3sy!cKV8uO$yXb3a$eZ%4B_Ym)>&1Hm9;of3-;4AVe&bmqy+ z7Xy^{gCg9E>WF{Grq@N;!W+|YLQ8qN3nrx0B0ZA#HMgS^d)B46?fqZi98j`wgyAK# zgk5W*M{oTR(QLe5Ov7mWDMB9k(GQKtnT3LZ=(Kh`TP^anCct!0A{(5>z`1vUsf;ap zezOa?tA&fi1XsV@LhAKPYh}hRMWog$IDyy@wmXzN&IPHd3o?GDz-*L!DlXa)@#|Np zY+aYbKybzt$Vl_%i3wS)qC&1}M6qj#yk*yrACQFP zF6&xGzKJW)dp7>oA)tf8vA!vi&J&b`UlLuHqvl~qZ>&J(KEaSS9t7Jt^s@8)O)N5N zT|RNz^3~Gl?Sa;c8b`?9hG@bh3%$itMI?ari!beuB#~>C<*#JS-=8&GJktizUNMH? zsGj?K#C2kcj4s39$j`SH`ai3un^MU;l*zZ5g81%OrOp_02q%<{<|r?%sv70F9<4vI zCiQDMV{Ym6un@XX&Sw3}BUBi=e#c=zcWxk>EAqA0Ie$hx-W|RKxTS)+PqZL)(1H zUBMTO;hDyrh+R_p2KMkyY<6AvoW9sm@US1;E`uz|j*)R3HPCQX^-d>$IYqmLt{0RS||>Ke-R-mT@JaT+Tmh$7Zh2Ciz@7fjhhzJhT8BDv-e|7o*>J$$Gan-CS6#~6J%jAE8S^+_AoklKwM>%--ka@hBXq5tL;JtJu zBxOs9@WU7u7+BVO2^7Der3mz)T>?>HwOnzvSHdOhAL%qeI;OZRW3H5>1B*gz||0d$q+hpQB2!ItC7Ge&ELqa-Q~E@X6l#wQQa7uLW5_a(S_G zt^D&z0+<-nX9^0!wEnhd3Xy$RQen8{rES_UT?Bvp?{mpgs? zqCPm;vArpsUQ!|yLYEFDY5)?MG;w>l*GsCx^ZtnwRm<1nPmvMs-Es;5jUE60mG*wP zu3h%lXL?SFfEM>TY}XLbuHT#pJnR$db6fvd-2<$YXZPMMjQI4?4(Q0kc47r1z?0pn zA=@(A0Q7e0y?>H|p0%!UWG^EmiMBtVYsdv(Vly zv&BmNO8v_JZ#J+3ZQS!Lv7U6+v?Ng@Q-LD-PY4Sj#Q7otxf+%-c2jgs&OaGI7m697 z)t~swCM*6M7r%h~r);~@Dt+=#N`k;%;YDydQToSCed^S2i;*7=`2T`@$R3AuL9aeG z)6nQnED|tDUN9>CJZ5Zm_%cY*-x^tJp;xb8p85TdnGT@bc6k2DXKY|*ToD<(N~^;S zAec?jsp>1g3Mpx4K`^lN=7qYSRIQTuas+gZk&>(h|Gw z;7_+`GsGQShxP8W6cqv?))st6X#lvt?EOO_DnNo=7qfxOKwEF9%QK> za?J!pZcKk%Rh4rVAwqplfLC9TL8m_J86B-c@RHiu*+voHwQ0oj6KnZC+S9@iJb5<> zUxR&MY3KYSTxnzqjgI^d(+mP7X!eU#QrrWXa0;w{((ej zAvWKYj~+a*h(5Mbd#pe{HX8F^c%gh*^{$t>`Q(45|M$#4659hdzUuVWgymTy?@y>h zd2!$?|F<)H93DaDnW{tlK4pjHg?hB|H^PQ@BWCt?kQ`jEmuIcUwMl>xodJj|u*h%X z6w`>b+9PotU(Q?789!tL()fiG{p{;2vEjwTJ>*0>Z#$cF#~yfKXp^AU2&^L6E&%tQ zs+M|p`3>@N!zXO6|DVhr6&lq+5QcO?I9+HYHfp9k-Kl5d`!D}nqYKw)&xl~AM%Bv{ z+wr0^V=@WW{6Ekb3`NtF))5vUthVjQvmVAN%(H%uRBt}(I^?Mxp#cs z0Ltt0obSPQRe!nK+(7X0N=AdCN> zvj}SY=Lz<$aU_ouXE8dLIHOdrE^r1=c(HZaJ={BAz};MX{G5+N|LU~=uGxMXWNRJi z_h{?Gub}-ux5^(!WRiWBhG#i%+cZHDuJf6E{kw4O>3MMN*8KGR;w++^CCiB$HA&DD zQ~vmXCf8tUE#J=~eqf%%?x|INjlE7A^%huK4UdwT^LQ*%Gsd4@q`e7Z;ZH4s9RCK` ziNA*ct7pK z+W_|qKCFU|(*A4t2n4v~4k?%E`f>Pl=^xpOe*KxR6D|M0vY|ncJRtZk*cLjUa)WQ_ ztiXBMh#0&0sH~G2W8+ z{kEc*q9Sk-6u$7d1X`JCOu3X(d8j7!J8ha;jQu02m4~C{x``7woMIkV|GoINi-uln zzH=!1%0Jl|2x&~C5R3OTHXlzP^?Mv%3&#=@kqu92i#r`VST2FoXnoV6(NXf9KAMQN zuhr@P&=voycC3Mx zD<3>tf`_d(!JO=R@I6ISX@sA2VJ8M9A$Wx8V7^y$-bZ9l^v1 zxJJLkldzFBM5xAxgHYo9Y`Cc)T9hn9++3IG7m>ORnXM6g+el?o&$ zd|9V?YY2wY=YfSk03goyZxIE2R(VcvQXq5@T}?I9fSH~A zKm^lNW>5R$iHY3tr0A@y?xIk#NGb43i{#`e&uYdcY2QEHx^GmC@_d&J@>UJj`2Ful zzZE^=#~Y--mG6G8PVC!5PV*Hb)uRpWh7|*wdN~Y- zmNu7dPaNY~ciYZw8KF#C#*o)9x6Ip#E=95z7afjiQvgDfYwv>+*#V>8SY*n(a5En= zWJ`G!@7?VIaCA)FCa|d?j1f7)wx8d#rSq!cN(BJ;xNfxkail7ab+0$PGfC2$+CvK+M>0$b1J!gC%F^9`@dj&hbQ2 zClNP(aUFeFz-w|$> zqG8rQ*8-x-q_uH)8&>ZoAS@zWQVq$lnqjt$Iik$*YE<)zTIko%*rQ&W{$l7z6nk~K ziIf?9K#Q%#`jSRl=^>$yYMVwsE{zmHQ%&!%h6FbkZl6|Yyg7<79A^&8D`6|#cm^VRpxzZg ze5dU#D|xh;{Jp`Fo&xe{6OraZy%U3Q{QjSa{(mkUD{dO= z>_*X2f*B)Y>|MYkO)o~$MayD=@+8D-?HEc_s!nXy_mp#gW5-7(aINLjWeq??S_B|B zwU{aq-E8B9kdNBi<0WMPLgu1yDTo-GO7IqW=oEV-ec5~99x^@@>lF}y*Hip{bv2@B z2@Ch~eJOiOK!L=TOXsXbCg3qLmBrViL%*TitV64rdwnNNSX8TZl(8EFcXgmdNHs>I zl9sP0aeBig)!$0VJUw@Onyg3NWZo#q(ycN(8JC*A_LJtPtQn>=qW$j4;!I7&lSUt>aL@EchW%Di|rxY zXBkY>A;edcxbB`~3Py!%k%8L1Z}uMxhsSmo@!94=ZDxwm3#73=4sRJo%yXa~OYiM( zZX1)l1K&6b*t6cD51Omd`MUkIXaOTEotO+Na~CJ^bac zzBPy9<##AbrAM~4g_)XWbnPB5cSLH)Blhl-5=SQJSlD1ss4-~|190u(jED|A_kd*nk0uhl(o@2zw z_$T=d%R0rL)u#@r{o%oIruJ6J#PzbllO9p&q9RDc84|Mef<*_vbWc(IB`H(;?%{J5 zTkjViy89MQPD@Xo*Ru)AQUhH4N%xZJ;;JwGmT(dp1KKGyn$YMWXA-~#zFus)`DwTT zGR0Y8!Qt**gsZk|KfRQS_v)8kPh(G#x(k znEOH$)T+Eh0c*y=TP~Va9*|<^#*sNWWTS4&Nh%@A1%rkh$>b|6Ds}}< zCemr+zrKJw-FLhj_@~QqoU5v9l^}(w0K01*rHj6t_+%3(R9SEog*ZO2LyR*tREyhq zq$kYU*MP-_XaE5%9TC0MG!E^Gqsu)G!NF4OjgxkChWec%19gmBpH=QTbS+9kYxohv zPf-T2w<0^%-Y=T=9(4idcl`9bZM^rFMRX|exxsaUUx3$FX@-Jjfo$v^5LtVZZ@NcZ zsO;XXTNEir!M^sRNzZlYzXdT*4n0U~@iK2Buiq}xO&JQWV|=EeoPe4=RlVWI+~|oB zxwvvyn$<69c_%!IrS#$C%!jF@sUq;DXSgX-89USOFqG)_Mc-;ZOi*RdhRfe zm(QJr*jrNJMt=RfEdy%8H2*e>iQP~#bL_BB6896`1TNUUi5D3JeXaQNzJoPzR7PuH z$vx9Kq~eZ@-u>zANJR)(N2Nx7PK~Hjl6p9aSDS>`uSzIYb{E%Neb#yIP;~Rrqepvt zeD1Ga=klFngX2;)L!ZFmk%J1ddET!q3KFU@9<1!OdMTTSK^d8un%v|fN=4Dx5-Eiu z9x_MXFN&AnCiWGwZx7LQQp>)D++d92125G(Q(Qo!!)CTpA>%HERK*>Ai%A0HN+$58 zMc4dz^rKSKlWrk%5#@w2mu@6M6?ioro#H>8sozk4JTMzIZEZwe4OW5gO6obEH{1A$ zN=uX6N?MJ*Yh9oiUr}MyU-O7DPKb&&ao@MyR#C1i^&$q=_O>KRFhzh^LuEcSV3v*Q9l`O3+8QNmD z5;F&0r{^o*ThVBihSUVf{sR5TSJ=_nncq=dzssHj$|NN( z8Sla&ot?qNBX-|#rkYEFokIemmpHTYbD^QgS#6G!7yA5#o{ikVtHvV3$`mergTKcJ zt^Q_a_IoSEdSdnkalK9VTjkvkwG6W7l+gBOzRjJe6>Z?ceOn1+=LY4E!2Pu!<#XTa z4yCHh!f`E{)zS2(=4Sj_Fl^=fLBBT>G~+a#B~Lo4vmYPyt}(uMS}k}nI-rxs`f!o; z!pFABI^K(G!W*$vU}Wj5kXWxTR~r%G#O4Fa9_>dk_!)aOP=tj8*pmhQX5ioN{0Z9j z?5`QfR+O^mj_d~o9xOd?9{c%IkdtNw_9`oyH5pi|<`^U3hrP5&Nn-B&;D=HmjKTwE+cZ|!ochKIMWOdq(Kz$HHkejX!N zQbJ1;6EP6(Q=!$17N7bI9dW9P#7_NVM|5w}^vqT?Sg-vCc8hV(t&XHhPEk@tBkCz? zFrWo2>(%+NNyO1YaB&f$0fc2`ev{*g-F?y$khZdJ2aZ2G+lM|0UmLvlR$c@p{Cd(Q zC1S5;(r43*=IRR?Iiz1_?3{wLF`_x$Tlo;Sa#c#JsA$c4NX=bPE!f=I?HjJsW%ob} z3jMGG^EaVZ;(FLEqtSIr-d}TYd-IKQsM}9=F-SbnTaZ|F$ojfqR8u^7MpNth-XhGkBK33Vav|m z!r|1yuK~`(c|6J9ma_{!4;Wv^key1h(&#c~X{~97!wJ;P3eaPP!Gl!|{w2Dt-Fs#v(Ix z1?7l-T^Fb~GI@-QEmqCdRVoE^Pq@FOK>0Au0G6E0!oUAEntrg=-RO}`n(mOb1xDIy zt$=dqqBke1na3-RUXpa$*cqLzmt4Ym0PhMW+3+D>ph3wUPdtVM^$1V)(u zwn)N*?09=r_G;`*P-do0d7d?3MJt6e6tzg4Z!@-WZtOPaTCO4F_HCoBsKE&oz3m70 zeM;X6Q!E6?Q_}ylFzgcHVdJW1mZN|!G3ibnnOo>1Rsqw=m>e*O z(5?`Vzb}EYKMicP)ioMe_4_DR4)z%}v5HIUn_`u_D?5OV?f)e2YGG2RDpyvn7&zo+(*mwy%6~a# z*Yd*cQC;77ZEZx`SlIs1?4~KrQDwizXA7~q>Zh@x{ft?V#Es1z{Rc2wxQUeL0Po%2 zLKw`s&O%NB1Y!l%yVMWaLB46SXmm=hnLnbshx>6s#r&y@DCpQz#Oib94*fpJS; zs_`-=-9qz-(%Y4PnE~>_N1zbf0#Tolp@HOWrA4#i$wm0VFthzvAO$0@FcLRgEWFKlne8-Hn+!5i`B`9pNW~Lmhzc|#1CiC(#0E4Ef!-%`ZE30j88|TWEVC4kv*i7=l3p%!vbwB>0M2wOC%pE z34uz#dIZO9pGA`aa__bazwJuu?u?4VuW=|k_SV(bI-1DpRp^CX8GI_Tts}1x^W50N zWwm(W=C-1R*@|2+@!yJ`Dzj!x1oS3$nXhi*H$zS@Rvr$|)U4%mfi@C2jjXHB@s|oB zle@d$?<+R>RQEDIi@@*u%gxo*Q5PtVr%vT)Gh=sk%(=N{r;?=Mvs5b_rX>cs@-Ja| zViLk9&z6BHblsKL>GeK@CL`coT<>z^)b{tI9dG!^yGz*D5Xv6|1FFDC2++mVvy1RH z=y0yn@i-`M-!nx!&)WBEYipC|8BM|19qjCk(>X-M#q~Dk?gcTgJ#};Zj))7oPv`_= zSlsWts)W$+%6Gta%Iq7p{xh-nNIFK__<2_veKRjjU_k!o?MUR4rm)6^*mmwL7kSVXrKo<%HjGa9_6d z<(zL83(Zxhb0 zw*LI!h304GAq_@BSp0tGohM5O1*k1FWkmUu?Nf1aZ+5dCt5WR|&-l1#BMy0)o^HQ^ zNGk@RlSr0VD`~`RVQ)SR%JLm`hqOg@b;!)fAYh_G|BUi|>OVElV6!=6GTV*V{#1qi>qZ=$CNZ?b| z?)ZY<31h?Fo1a`+36S=DaZf~l7zF6#;-}_9`XehUZpah!mfLdistaJ8X-SfdVxEZq zh&a`w(qP^sErje83chA22tVsLw(Hy93urM$-NS$u9IGC0<2KXs+sj)JKQ+k!Zxx<< z1hzsFtItR`U`Qt1S$U;a6?!+@RzL3L$*J)-DGj^E;mbzCyn#>!V{6?f2RNA)+HQX0 zxaa&+Kb?~2#J-f_i72h-y5l~cUJGn9Ak4umOgY?JOzg$Pl}bUoEGI~$P8OMvLY^sS z8=KY2v)m;+MB`>evOaw`bYV%nd=O>6Glr+WIN|aT(3aC zHW6=BuD#B_c=33Z?fztAW$os;Zr~&#`FV4_tc=RyzETX8zWU?GcT?J~iCzlwiDEDS zo(11W8do;B_pmSakj*qC-SJL`= zp@l;|5e8~%>VM-HJ4x2`ul_WvJ*t{%VTa64e?L5tGy2AJp~Cl$wCkZS{VZW_4$##y K(yUasL;e>})_utU diff --git a/doc/en/img/pytest_logo_curves.svg b/doc/en/img/pytest_logo_curves.svg new file mode 100644 index 00000000000..e05ceb11233 --- /dev/null +++ b/doc/en/img/pytest_logo_curves.svg @@ -0,0 +1,29 @@ + + + + + From 534d174fd24f8066518e396fbdd559eb7cf7b5a9 Mon Sep 17 00:00:00 2001 From: Chris NeJame Date: Wed, 16 Dec 2020 11:53:14 -0500 Subject: [PATCH 0334/2846] Clarify fixture execution order and provide visual aids (#7381) Co-authored-by: Bruno Oliveira Co-authored-by: Ran Benita --- AUTHORS | 1 + .../example/fixtures/fixture_availability.svg | 132 ++ .../fixtures/fixture_availability_plugins.svg | 142 ++ .../example/fixtures/test_fixtures_order.py | 38 - .../fixtures/test_fixtures_order_autouse.py | 45 + .../fixtures/test_fixtures_order_autouse.svg | 64 + ..._fixtures_order_autouse_multiple_scopes.py | 31 + ...fixtures_order_autouse_multiple_scopes.svg | 76 + ...est_fixtures_order_autouse_temp_effects.py | 36 + ...st_fixtures_order_autouse_temp_effects.svg | 100 + .../test_fixtures_order_dependencies.py | 45 + .../test_fixtures_order_dependencies.svg | 60 + .../test_fixtures_order_dependencies_flat.svg | 51 + ...st_fixtures_order_dependencies_unclear.svg | 60 + .../fixtures/test_fixtures_order_scope.py | 36 + .../fixtures/test_fixtures_order_scope.svg | 55 + .../test_fixtures_request_different_scope.py | 29 + .../test_fixtures_request_different_scope.svg | 115 ++ doc/en/fixture.rst | 1751 +++++++++++++---- 19 files changed, 2413 insertions(+), 454 deletions(-) create mode 100644 doc/en/example/fixtures/fixture_availability.svg create mode 100644 doc/en/example/fixtures/fixture_availability_plugins.svg delete mode 100644 doc/en/example/fixtures/test_fixtures_order.py create mode 100644 doc/en/example/fixtures/test_fixtures_order_autouse.py create mode 100644 doc/en/example/fixtures/test_fixtures_order_autouse.svg create mode 100644 doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py create mode 100644 doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.svg create mode 100644 doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py create mode 100644 doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.svg create mode 100644 doc/en/example/fixtures/test_fixtures_order_dependencies.py create mode 100644 doc/en/example/fixtures/test_fixtures_order_dependencies.svg create mode 100644 doc/en/example/fixtures/test_fixtures_order_dependencies_flat.svg create mode 100644 doc/en/example/fixtures/test_fixtures_order_dependencies_unclear.svg create mode 100644 doc/en/example/fixtures/test_fixtures_order_scope.py create mode 100644 doc/en/example/fixtures/test_fixtures_order_scope.svg create mode 100644 doc/en/example/fixtures/test_fixtures_request_different_scope.py create mode 100644 doc/en/example/fixtures/test_fixtures_request_different_scope.svg diff --git a/AUTHORS b/AUTHORS index 72391122eb5..20798f3093d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -56,6 +56,7 @@ Charles Cloud Charles Machalow Charnjit SiNGH (CCSJ) Chris Lamb +Chris NeJame Christian Boelsen Christian Fetzer Christian Neumüller diff --git a/doc/en/example/fixtures/fixture_availability.svg b/doc/en/example/fixtures/fixture_availability.svg new file mode 100644 index 00000000000..3ca28447c45 --- /dev/null +++ b/doc/en/example/fixtures/fixture_availability.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + tests + + + + + + + + + + subpackage + + + + + + + + + + test_subpackage.py + + + + + + + + + + + innermost + + test_order + + mid + + + + + + + 1 + + + + + + + 2 + + + + + + + 3 + + + + + + + + + test_top.py + + + + + + + + + innermost + + test_order + + + + + + + 1 + + + 2 + + + top + + order + diff --git a/doc/en/example/fixtures/fixture_availability_plugins.svg b/doc/en/example/fixtures/fixture_availability_plugins.svg new file mode 100644 index 00000000000..88e32d90809 --- /dev/null +++ b/doc/en/example/fixtures/fixture_availability_plugins.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + plugin_a + + + + + + + + 4 + + + + + + + + + plugin_b + + + + + + + + 4 + + + + + + + + + tests + + + + + + + + 3 + + + + + + + + + subpackage + + + + + + + + 2 + + + + + + + + + test_subpackage.py + + + + + + + + 1 + + + + + + + + + + + + + inner + + test_order + + mid + + order + + + a_fix + + b_fix + diff --git a/doc/en/example/fixtures/test_fixtures_order.py b/doc/en/example/fixtures/test_fixtures_order.py deleted file mode 100644 index 97b3e80052b..00000000000 --- a/doc/en/example/fixtures/test_fixtures_order.py +++ /dev/null @@ -1,38 +0,0 @@ -import pytest - -# fixtures documentation order example -order = [] - - -@pytest.fixture(scope="session") -def s1(): - order.append("s1") - - -@pytest.fixture(scope="module") -def m1(): - order.append("m1") - - -@pytest.fixture -def f1(f3): - order.append("f1") - - -@pytest.fixture -def f3(): - order.append("f3") - - -@pytest.fixture(autouse=True) -def a1(): - order.append("a1") - - -@pytest.fixture -def f2(): - order.append("f2") - - -def test_order(f1, m1, f2, s1): - assert order == ["s1", "m1", "a1", "f3", "f1", "f2"] diff --git a/doc/en/example/fixtures/test_fixtures_order_autouse.py b/doc/en/example/fixtures/test_fixtures_order_autouse.py new file mode 100644 index 00000000000..ec282ab4b2b --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_autouse.py @@ -0,0 +1,45 @@ +import pytest + + +@pytest.fixture +def order(): + return [] + + +@pytest.fixture +def a(order): + order.append("a") + + +@pytest.fixture +def b(a, order): + order.append("b") + + +@pytest.fixture(autouse=True) +def c(b, order): + order.append("c") + + +@pytest.fixture +def d(b, order): + order.append("d") + + +@pytest.fixture +def e(d, order): + order.append("e") + + +@pytest.fixture +def f(e, order): + order.append("f") + + +@pytest.fixture +def g(f, c, order): + order.append("g") + + +def test_order_and_g(g, order): + assert order == ["a", "b", "c", "d", "e", "f", "g"] diff --git a/doc/en/example/fixtures/test_fixtures_order_autouse.svg b/doc/en/example/fixtures/test_fixtures_order_autouse.svg new file mode 100644 index 00000000000..36362e4fb00 --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_autouse.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + autouse + + order + + a + + b + + c + + d + + e + + f + + g + + test_order + diff --git a/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py b/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py new file mode 100644 index 00000000000..de0c2642793 --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py @@ -0,0 +1,31 @@ +import pytest + + +@pytest.fixture(scope="class") +def order(): + return [] + + +@pytest.fixture(scope="class", autouse=True) +def c1(order): + order.append("c1") + + +@pytest.fixture(scope="class") +def c2(order): + order.append("c2") + + +@pytest.fixture(scope="class") +def c3(order, c1): + order.append("c3") + + +class TestClassWithC1Request: + def test_order(self, order, c1, c3): + assert order == ["c1", "c3"] + + +class TestClassWithoutC1Request: + def test_order(self, order, c2): + assert order == ["c1", "c2"] diff --git a/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.svg b/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.svg new file mode 100644 index 00000000000..9f2180fe548 --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.svg @@ -0,0 +1,76 @@ + + + + + + + + order + + c1 + + c3 + + test_order + + + + + + TestWithC1Request + + + + + + + order + + c1 + + c2 + + test_order + + + + + + TestWithoutC1Request + + + + + autouse + diff --git a/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py b/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py new file mode 100644 index 00000000000..ba01ad32f57 --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py @@ -0,0 +1,36 @@ +import pytest + + +@pytest.fixture +def order(): + return [] + + +@pytest.fixture +def c1(order): + order.append("c1") + + +@pytest.fixture +def c2(order): + order.append("c2") + + +class TestClassWithAutouse: + @pytest.fixture(autouse=True) + def c3(self, order, c2): + order.append("c3") + + def test_req(self, order, c1): + assert order == ["c2", "c3", "c1"] + + def test_no_req(self, order): + assert order == ["c2", "c3"] + + +class TestClassWithoutAutouse: + def test_req(self, order, c1): + assert order == ["c1"] + + def test_no_req(self, order): + assert order == [] diff --git a/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.svg b/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.svg new file mode 100644 index 00000000000..ac62ae46b40 --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + TestWithAutouse + + + + + + + + + order + + c2 + + c3 + + c1 + + test_req + + + + + order + + c2 + + c3 + + test_no_req + + + autouse + + + + + + + + + TestWithoutAutouse + + + + + + order + + c1 + + test_req + + + + + order + + test_no_req + diff --git a/doc/en/example/fixtures/test_fixtures_order_dependencies.py b/doc/en/example/fixtures/test_fixtures_order_dependencies.py new file mode 100644 index 00000000000..b3512c2a64d --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_dependencies.py @@ -0,0 +1,45 @@ +import pytest + + +@pytest.fixture +def order(): + return [] + + +@pytest.fixture +def a(order): + order.append("a") + + +@pytest.fixture +def b(a, order): + order.append("b") + + +@pytest.fixture +def c(a, b, order): + order.append("c") + + +@pytest.fixture +def d(c, b, order): + order.append("d") + + +@pytest.fixture +def e(d, b, order): + order.append("e") + + +@pytest.fixture +def f(e, order): + order.append("f") + + +@pytest.fixture +def g(f, c, order): + order.append("g") + + +def test_order(g, order): + assert order == ["a", "b", "c", "d", "e", "f", "g"] diff --git a/doc/en/example/fixtures/test_fixtures_order_dependencies.svg b/doc/en/example/fixtures/test_fixtures_order_dependencies.svg new file mode 100644 index 00000000000..24418e63c9d --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_dependencies.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + order + + a + + b + + c + + d + + e + + f + + g + + test_order + diff --git a/doc/en/example/fixtures/test_fixtures_order_dependencies_flat.svg b/doc/en/example/fixtures/test_fixtures_order_dependencies_flat.svg new file mode 100644 index 00000000000..bbe7ad28339 --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_dependencies_flat.svg @@ -0,0 +1,51 @@ + + + + + order + + a + + b + + c + + d + + e + + f + + g + + test_order + diff --git a/doc/en/example/fixtures/test_fixtures_order_dependencies_unclear.svg b/doc/en/example/fixtures/test_fixtures_order_dependencies_unclear.svg new file mode 100644 index 00000000000..150724f80a3 --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_dependencies_unclear.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + order + + a + + b + + c + + d + + e + + f + + g + + test_order + diff --git a/doc/en/example/fixtures/test_fixtures_order_scope.py b/doc/en/example/fixtures/test_fixtures_order_scope.py new file mode 100644 index 00000000000..5d9487cab34 --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_scope.py @@ -0,0 +1,36 @@ +import pytest + + +@pytest.fixture(scope="session") +def order(): + return [] + + +@pytest.fixture +def func(order): + order.append("function") + + +@pytest.fixture(scope="class") +def cls(order): + order.append("class") + + +@pytest.fixture(scope="module") +def mod(order): + order.append("module") + + +@pytest.fixture(scope="package") +def pack(order): + order.append("package") + + +@pytest.fixture(scope="session") +def sess(order): + order.append("session") + + +class TestClass: + def test_order(self, func, cls, mod, pack, sess, order): + assert order == ["session", "package", "module", "class", "function"] diff --git a/doc/en/example/fixtures/test_fixtures_order_scope.svg b/doc/en/example/fixtures/test_fixtures_order_scope.svg new file mode 100644 index 00000000000..ebaf7e4e245 --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_order_scope.svg @@ -0,0 +1,55 @@ + + + + + + + order + + sess + + pack + + mod + + cls + + func + + test_order + + + + + + TestClass + + diff --git a/doc/en/example/fixtures/test_fixtures_request_different_scope.py b/doc/en/example/fixtures/test_fixtures_request_different_scope.py new file mode 100644 index 00000000000..00e2e46d845 --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_request_different_scope.py @@ -0,0 +1,29 @@ +import pytest + + +@pytest.fixture +def order(): + return [] + + +@pytest.fixture +def outer(order, inner): + order.append("outer") + + +class TestOne: + @pytest.fixture + def inner(self, order): + order.append("one") + + def test_order(self, order, outer): + assert order == ["one", "outer"] + + +class TestTwo: + @pytest.fixture + def inner(self, order): + order.append("two") + + def test_order(self, order, outer): + assert order == ["two", "outer"] diff --git a/doc/en/example/fixtures/test_fixtures_request_different_scope.svg b/doc/en/example/fixtures/test_fixtures_request_different_scope.svg new file mode 100644 index 00000000000..ad98469ced0 --- /dev/null +++ b/doc/en/example/fixtures/test_fixtures_request_different_scope.svg @@ -0,0 +1,115 @@ + + + + + + + + + + test_fixtures_request_different_scope.py + + + + + + + + + + + + inner + + test_order + + + + + + TestOne + + + + + + + + 1 + + + + + + + 2 + + + + + + + + + + + inner + + test_order + + + + + + TestTwo + + + + + + + + 1 + + + 2 + + + outer + + order + diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 963fc32e6b0..c74984563ab 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -12,6 +12,8 @@ pytest fixtures: explicit, modular, scalable .. _`xUnit`: https://en.wikipedia.org/wiki/XUnit .. _`Software test fixtures`: https://en.wikipedia.org/wiki/Test_fixture#Software .. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection +.. _`Transaction`: https://en.wikipedia.org/wiki/Transaction_processing +.. _`linearizable`: https://en.wikipedia.org/wiki/Linearizability `Software test fixtures`_ initialize test functions. They provide a fixed baseline so that tests execute reliably and produce consistent, @@ -35,6 +37,10 @@ style of setup/teardown functions: to configuration and component options, or to re-use fixtures across function, class, module or whole test session scopes. +* teardown logic can be easily, and safely managed, no matter how many fixtures + are used, without the need to carefully handle errors by hand or micromanage + the order that cleanup steps are added. + In addition, pytest continues to support :ref:`xunitsetup`. You can mix both styles, moving incrementally from classic to new style, as you prefer. You can also start out from existing :ref:`unittest.TestCase @@ -115,32 +121,529 @@ for reference: .. _`@pytest.fixture`: .. _`pytest.fixture`: -Fixtures as Function arguments ------------------------------------------ +What fixtures are +----------------- + +Before we dive into what fixtures are, let's first look at what a test is. + +In the simplest terms, a test is meant to look at the result of a particular +behavior, and make sure that result aligns with what you would expect. +Behavior is not something that can be empirically measured, which is why writing +tests can be challenging. + +"Behavior" is the way in which some system **acts in response** to a particular +situation and/or stimuli. But exactly *how* or *why* something is done is not +quite as important as *what* was done. + +You can think of a test as being broken down into four steps: + +1. **Arrange** +2. **Act** +3. **Assert** +4. **Cleanup** + +**Arrange** is where we prepare everything for our test. This means pretty +much everything except for the "**act**". It's lining up the dominoes so that +the **act** can do its thing in one, state-changing step. This can mean +preparing objects, starting/killing services, entering records into a database, +or even things like defining a URL to query, generating some credentials for a +user that doesn't exist yet, or just waiting for some process to finish. + +**Act** is the singular, state-changing action that kicks off the **behavior** +we want to test. This behavior is what carries out the changing of the state of +the system under test (SUT), and it's the resulting changed state that we can +look at to make a judgement about the behavior. This typically takes the form of +a function/method call. + +**Assert** is where we look at that resulting state and check if it looks how +we'd expect after the dust has settled. It's where we gather evidence to say the +behavior does or does not aligns with what we expect. The ``assert`` in our test +is where we take that measurement/observation and apply our judgement to it. If +something should be green, we'd say ``assert thing == "green"``. + +**Cleanup** is where the test picks up after itself, so other tests aren't being +accidentally influenced by it. + +At it's core, the test is ultimately the **act** and **assert** steps, with the +**arrange** step only providing the context. **Behavior** exists between **act** +and **assert**. + +Back to fixtures +^^^^^^^^^^^^^^^^ + +"Fixtures", in the literal sense, are each of the **arrange** steps and data. They're +everything that test needs to do its thing. + +At a basic level, test functions request fixtures by declaring them as +arguments, as in the ``test_ehlo(smtp_connection):`` in the previous example. -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 `. Let's look at a simple -self-contained test module containing a fixture and a test function -using it: +In pytest, "fixtures" are functions you define that serve this purpose. But they +don't have to be limited to just the **arrange** steps. They can provide the +**act** step, as well, and this can be a powerful technique for designing more +complex tests, especially given how pytest's fixture system works. But we'll get +into that further down. + +We can tell pytest that a particular function is a fixture by decorating it with +:py:func:`@pytest.fixture `. Here's a simple example of +what a fixture in pytest might look like: .. code-block:: python - # content of ./test_smtpsimple.py import pytest + class Fruit: + def __init__(self, name): + self.name = name + + def __eq__(self, other): + return self.name == other.name + + @pytest.fixture - def smtp_connection(): - import smtplib + def my_fruit(): + return Fruit("apple") + + + @pytest.fixture + def fruit_basket(my_fruit): + return [Fruit("banana"), my_fruit] + + + def test_my_fruit_in_basket(my_fruit, fruit_basket): + assert my_fruit in fruit_basket + + + +Tests don't have to be limited to a single fixture, either. They can depend on +as many fixtures as you want, and fixtures can use other fixtures, as well. This +is where pytest's fixture system really shines. + +Don't be afraid to break things up if it makes things cleaner. + +"Requesting" fixtures +--------------------- + +So fixtures are how we *prepare* for a test, but how do we tell pytest what +tests and fixtures need which fixtures? + +At a basic level, test functions request fixtures by declaring them as +arguments, as in the ``test_my_fruit_in_basket(my_fruit, fruit_basket):`` in the +previous example. + +At a basic level, pytest depends on a test to tell it what fixtures it needs, so +we have to build that information into the test itself. We have to make the test +"**request**" the fixtures it depends on, and to do this, we have to +list those fixtures as parameters in the test function's "signature" (which is +the ``def test_something(blah, stuff, more):`` line). + +When pytest goes to run a test, it looks at the parameters in that test +function's signature, and then searches for fixtures that have the same names as +those parameters. Once pytest finds them, it runs those fixtures, captures what +they returned (if anything), and passes those objects into the test function as +arguments. + +Quick example +^^^^^^^^^^^^^ + +.. code-block:: python + + import pytest + + + class Fruit: + def __init__(self, name): + self.name = name + self.cubed = False + + def cube(self): + self.cubed = True + + + class FruitSalad: + def __init__(self, *fruit_bowl): + self.fruit = fruit_bowl + self._cube_fruit() + + def _cube_fruit(self): + for fruit in self.fruit: + fruit.cube() + + + # Arrange + @pytest.fixture + def fruit_bowl(): + return [Fruit("apple"), Fruit("banana")] + + + def test_fruit_salad(fruit_bowl): + # Act + fruit_salad = FruitSalad(*fruit_bowl) + + # Assert + assert all(fruit.cubed for fruit in fruit_salad.fruit) + +In this example, ``test_fruit_salad`` "**requests**" ``fruit_bowl`` (i.e. +``def test_fruit_salad(fruit_bowl):``), and when pytest sees this, it will +execute the ``fruit_bowl`` fixture function and pass the object it returns into +``test_fruit_salad`` as the ``fruit_bowl`` argument. + +Here's roughly +what's happening if we were to do it by hand: + +.. code-block:: python + + def fruit_bowl(): + return [Fruit("apple"), Fruit("banana")] + + + def test_fruit_salad(fruit_bowl): + # Act + fruit_salad = FruitSalad(*fruit_bowl) + + # Assert + assert all(fruit.cubed for fruit in fruit_salad.fruit) + + + # Arrange + bowl = fruit_bowl() + test_fruit_salad(fruit_bowl=bowl) + +Fixtures can **request** other fixtures +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +One of pytest's greatest strengths is its extremely flexible fixture system. It +allows us to boil down complex requirements for tests into more simple and +organized functions, where we only need to have each one describe the things +they are dependent on. We'll get more into this further down, but for now, +here's a quick example to demonstrate how fixtures can use other fixtures: + +.. code-block:: python + + # contents of test_append.py + import pytest + + + # Arrange + @pytest.fixture + def first_entry(): + return "a" + + + # Arrange + @pytest.fixture + def order(first_entry): + return [first_entry] + + + def test_string(order): + # Act + order.append("b") + + # Assert + assert order == ["a", "b"] + + +Notice that this is the same example from above, but very little changed. The +fixtures in pytest **request** fixtures just like tests. All the same +**requesting** rules apply to fixtures that do for tests. Here's how this +example would work if we did it by hand: + +.. code-block:: python + + def first_entry(): + return "a" + + + def order(first_entry): + return [first_entry] + + + def test_string(order): + # Act + order.append("b") + + # Assert + assert order == ["a", "b"] + + + entry = first_entry() + the_list = order(first_entry=entry) + test_string(order=the_list) + +Fixtures are reusable +^^^^^^^^^^^^^^^^^^^^^ + +One of the things that makes pytest's fixture system so powerful, is that it +gives us the abilty to define a generic setup step that can reused over and +over, just like a normal function would be used. Two different tests can request +the same fixture and have pytest give each test their own result from that +fixture. + +This is extremely useful for making sure tests aren't affected by each other. We +can use this system to make sure each test gets its own fresh batch of data and +is starting from a clean state so it can provide consistent, repeatable results. + +Here's an example of how this can come in handy: + +.. code-block:: python + + # contents of test_append.py + import pytest + + + # Arrange + @pytest.fixture + def first_entry(): + return "a" + + + # Arrange + @pytest.fixture + def order(first_entry): + return [first_entry] + + + def test_string(order): + # Act + order.append("b") + + # Assert + assert order == ["a", "b"] + + + def test_int(order): + # Act + order.append(2) + + # Assert + assert order == ["a", 2] + + +Each test here is being given its own copy of that ``list`` object, +which means the ``order`` fixture is getting executed twice (the same +is true for the ``first_entry`` fixture). If we were to do this by hand as +well, it would look something like this: + +.. code-block:: python + + def first_entry(): + return "a" + + + def order(first_entry): + return [first_entry] + + def test_string(order): + # Act + order.append("b") + + # Assert + assert order == ["a", "b"] + + + def test_int(order): + # Act + order.append(2) + + # Assert + assert order == ["a", 2] + + + entry = first_entry() + the_list = order(first_entry=entry) + test_string(order=the_list) + + entry = first_entry() + the_list = order(first_entry=entry) + test_int(order=the_list) + +A test/fixture can **request** more than one fixture at a time +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Tests and fixtures aren't limited to **requesting** a single fixture at a time. +They can request as many as they like. Here's another quick example to +demonstrate: + +.. code-block:: python + + # contents of test_append.py + import pytest + + + # Arrange + @pytest.fixture + def first_entry(): + return "a" + + + # Arrange + @pytest.fixture + def second_entry(): + return 2 + + + # Arrange + @pytest.fixture + def order(first_entry, second_entry): + return [first_entry, second_entry] + + + # Arrange + @pytest.fixture + def expected_list(): + return ["a", 2, 3.0] + + + def test_string(order, expected_list): + # Act + order.append(3.0) + + # Assert + assert order == expected_list + +Fixtures can be **requested** more than once per test (return values are cached) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Fixtures can also be **requested** more than once during the same test, and +pytest won't execute them again for that test. This means we can **request** +fixtures in multiple fixtures that are dependent on them (and even again in the +test itself) without those fixtures being executed more than once. + +.. code-block:: python + + # contents of test_append.py + import pytest + + + # Arrange + @pytest.fixture + def first_entry(): + return "a" + + + # Arrange + @pytest.fixture + def order(): + return [] + + + # Act + @pytest.fixture + def append_first(order, first_entry): + return order.append(first_entry) + + + def test_string_only(append_first, order, first_entry): + # Assert + assert order == [first_entry] + +If a **requested** fixture was executed once for every time it was **requested** +during a test, then this test would fail because both ``append_first`` and +``test_string_only`` would see ``order`` as an empty list (i.e. ``[]``), but +since the return value of ``order`` was cached (along with any side effects +executing it may have had) after the first time it was called, both the test and +``append_first`` were referencing the same object, and the test saw the effect +``append_first`` had on that object. + +.. _`autouse`: +.. _`autouse fixtures`: + +Autouse fixtures (fixtures you don't have to request) +----------------------------------------------------- + +Sometimes you may want to have a fixture (or even several) that you know all +your tests will depend on. "Autouse" fixtures are a convenient way to make all +tests automatically **request** them. This can cut out a +lot of redundant **requests**, and can even provide more advanced fixture usage +(more on that further down). + +We can make a fixture an autouse fixture by passing in ``autouse=True`` to the +fixture's decorator. Here's a simple example for how they can be used: + +.. code-block:: python + + # contents of test_append.py + import pytest + + + @pytest.fixture + def first_entry(): + return "a" + + + @pytest.fixture + def order(first_entry): + return [] + + + @pytest.fixture(autouse=True) + def append_first(order, first_entry): + return order.append(first_entry) + + + def test_string_only(order, first_entry): + assert order == [first_entry] + + + def test_string_and_int(order, first_entry): + order.append(2) + assert order == [first_entry, 2] + +In this example, the ``append_first`` fixture is an autouse fixture. Because it +happens automatically, both tests are affected by it, even though neither test +**requested** it. That doesn't mean they *can't* be **requested** though; just +that it isn't *necessary*. + +.. _smtpshared: + +Scope: sharing fixtures across classes, modules, packages or session +-------------------------------------------------------------------- + +.. regendoc:wipe + +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 ` invocation +to cause a ``smtp_connection`` fixture function, responsible to create a connection to a preexisting SMTP server, 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 +each receive the same ``smtp_connection`` fixture instance, thus saving time. +Possible values for ``scope`` are: ``function``, ``class``, ``module``, ``package`` or ``session``. + +The next example puts the fixture function into a separate ``conftest.py`` file +so that tests from multiple test modules in the directory can +access the fixture function: + +.. code-block:: python + + # content of conftest.py + import pytest + import smtplib + + + @pytest.fixture(scope="module") + def smtp_connection(): return smtplib.SMTP("smtp.gmail.com", 587, timeout=5) +.. code-block:: python + + # content of test_module.py + + def test_ehlo(smtp_connection): response, msg = smtp_connection.ehlo() assert response == 250 + assert b"smtp.gmail.com" in msg + assert 0 # for demo purposes + + + def test_noop(smtp_connection): + response, msg = smtp_connection.noop() + assert response == 250 assert 0 # for demo purposes Here, the ``test_ehlo`` needs the ``smtp_connection`` fixture value. pytest @@ -149,442 +652,967 @@ marked ``smtp_connection`` fixture function. Running the test looks like this: .. code-block:: pytest - $ pytest test_smtpsimple.py + $ pytest test_module.py =========================== test session starts ============================ 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 + collected 2 items + + test_module.py FF [100%] + + ================================= FAILURES ================================= + ________________________________ test_ehlo _________________________________ + + smtp_connection = + + def test_ehlo(smtp_connection): + response, msg = smtp_connection.ehlo() + assert response == 250 + assert b"smtp.gmail.com" in msg + > assert 0 # for demo purposes + E assert 0 + + test_module.py:7: AssertionError + ________________________________ test_noop _________________________________ + + smtp_connection = + + def test_noop(smtp_connection): + response, msg = smtp_connection.noop() + assert response == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:13: AssertionError + ========================= short test summary info ========================== + FAILED test_module.py::test_ehlo - assert 0 + FAILED test_module.py::test_noop - assert 0 + ============================ 2 failed in 0.12s ============================= + +You see the two ``assert 0`` failing and more importantly you can also see +that the **exactly same** ``smtp_connection`` object was passed into the +two test functions because pytest shows the incoming argument values in the +traceback. As a result, the two test functions using ``smtp_connection`` run +as quick as a single one because they reuse the same instance. + +If you decide that you rather want to have a session-scoped ``smtp_connection`` +instance, you can simply declare it: + +.. code-block:: python + + @pytest.fixture(scope="session") + def smtp_connection(): + # the returned fixture value will be shared for + # all tests requesting it + ... + + +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. + +.. 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: + +Dynamic scope +^^^^^^^^^^^^^ + +.. versionadded:: 5.2 + +In some cases, you might want to change the scope of the fixture without changing the code. +To do that, pass a callable to ``scope``. The callable must return a string with a valid scope +and will be executed only once - during the fixture definition. It will be called with two +keyword arguments - ``fixture_name`` as a string and ``config`` with a configuration object. + +This can be especially useful when dealing with fixtures that need time for setup, like spawning +a docker container. You can use the command-line argument to control the scope of the spawned +containers for different environments. See the example below. + +.. code-block:: python + + def determine_scope(fixture_name, config): + if config.getoption("--keep-containers", None): + return "session" + return "function" + + + @pytest.fixture(scope=determine_scope) + def docker_container(): + yield spawn_container() + +Fixture errors +-------------- + +pytest does its best to put all the fixtures for a given test in a linear order +so that it can see which fixture happens first, second, third, and so on. If an +earlier fixture has a problem, though, and raises an exception, pytest will stop +executing fixtures for that test and mark the test as having an error. + +When a test is marked as having an error, it doesn't mean the test failed, +though. It just means the test couldn't even be attempted because one of the +things it depends on had a problem. + +This is one reason why it's a good idea to cut out as many unnecessary +dependencies as possible for a given test. That way a problem in something +unrelated isn't causing us to have an incomplete picture of what may or may not +have issues. + +Here's a quick example to help explain: + +.. code-block:: python + + import pytest + + + @pytest.fixture + def order(): + return [] + + + @pytest.fixture + def append_first(order): + order.append(1) + + + @pytest.fixture + def append_second(order, append_first): + order.extend([2]) + + + @pytest.fixture(autouse=True) + def append_third(order, append_second): + order += [3] + + + def test_order(order): + assert order == [1, 2, 3] + + +If, for whatever reason, ``order.append(1)`` had a bug and it raises an exception, +we wouldn't be able to know if ``order.extend([2])`` or ``order += [3]`` would +also have problems. After ``append_first`` throws an exception, pytest won't run +any more fixtures for ``test_order``, and it won't even try to run +``test_order`` itself. The only things that would've run would be ``order`` and +``append_first``. + + + + +.. _`finalization`: - test_smtpsimple.py F [100%] +Teardown/Cleanup (AKA Fixture finalization) +------------------------------------------- + +When we run our tests, we'll want to make sure they clean up after themselves so +they don't mess with any other tests (and also so that we don't leave behind a +mountain of test data to bloat the system). Fixtures in pytest offer a very +useful teardown system, which allows us to define the specific steps necessary +for each fixture to clean up after itself. + +This system can be leveraged in two ways. + +.. _`yield fixtures`: + +1. ``yield`` fixtures (recommended) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +"Yield" fixtures ``yield`` instead of ``return``. With these +fixtures, we can run some code and pass an object back to the requesting +fixture/test, just like with the other fixtures. The only differences are: + +1. ``return`` is swapped out for ``yield``. +2. Any teardown code for that fixture is placed *after* the ``yield``. + +Once pytest figures out a linear order for the fixtures, it will run each one up +until it returns or yields, and then move on to the next fixture in the list to +do the same thing. + +Once the test is finished, pytest will go back down the list of fixtures, but in +the *reverse order*, taking each one that yielded, and running the code inside +it that was *after* the ``yield`` statement. + +As a simple example, let's say we want to test sending email from one user to +another. We'll have to first make each user, then send the email from one user +to the other, and finally assert that the other user received that message in +their inbox. If we want to clean up after the test runs, we'll likely have to +make sure the other user's mailbox is emptied before deleting that user, +otherwise the system may complain. + +Here's what that might look like: + +.. code-block:: python + + import pytest + + from emaillib import Email, MailAdminClient + + + @pytest.fixture + def mail_admin(): + return MailAdminClient() + + + @pytest.fixture + def sending_user(mail_admin): + user = mail_admin.create_user() + yield user + admin_client.delete_user(user) + + + @pytest.fixture + def receiving_user(mail_admin): + user = mail_admin.create_user() + yield user + admin_client.delete_user(user) + + + def test_email_received(receiving_user, email): + email = Email(subject="Hey!", body="How's it going?") + sending_user.send_email(_email, receiving_user) + assert email in receiving_user.inbox + +Because ``receiving_user`` is the last fixture to run during setup, it's the first to run +during teardown. + +There is a risk that even having the order right on the teardown side of things +doesn't guarantee a safe cleanup. That's covered in a bit more detail in +:ref:`safe teardowns`. + +Handling errors for yield fixture +""""""""""""""""""""""""""""""""" + +If a yield fixture raises an exception before yielding, pytest won't try to run +the teardown code after that yield fixture's ``yield`` statement. But, for every +fixture that has already run successfully for that test, pytest will still +attempt to tear them down as it normally would. + +2. Adding finalizers directly +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +While yield fixtures are considered to be the cleaner and more straighforward +option, there is another choice, and that is to add "finalizer" functions +directly to the test's `request-context`_ object. It brings a similar result as +yield fixtures, but requires a bit more verbosity. + +In order to use this approach, we have to request the `request-context`_ object +(just like we would request another fixture) in the fixture we need to add +teardown code for, and then pass a callable, containing that teardown code, to +its ``addfinalizer`` method. + +We have to be careful though, because pytest will run that finalizer once it's +been added, even if that fixture raises an exception after adding the finalizer. +So to make sure we don't run the finalizer code when we wouldn't need to, we +would only add the finalizer once the fixture would have done something that +we'd need to teardown. + +Here's how the previous example would look using the ``addfinalizer`` method: + +.. code-block:: python + + import pytest + + from emaillib import Email, MailAdminClient + + + @pytest.fixture + def mail_admin(): + return MailAdminClient() + + + @pytest.fixture + def sending_user(mail_admin): + user = mail_admin.create_user() + yield user + admin_client.delete_user(user) + + + @pytest.fixture + def receiving_user(mail_admin, request): + user = mail_admin.create_user() + + def delete_user(): + admin_client.delete_user(user) + + request.addfinalizer(delete_user) + return user + + + @pytest.fixture + def email(sending_user, receiving_user, request): + _email = Email(subject="Hey!", body="How's it going?") + sending_user.send_email(_email, receiving_user) + + def empty_mailbox(): + receiving_user.delete_email(_email) + + request.addfinalizer(empty_mailbox) + return _email + + + def test_email_received(receiving_user, email): + assert email in receiving_user.inbox + + +It's a bit longer than yield fixtures and a bit more complex, but it +does offer some nuances for when you're in a pinch. + +.. _`safe teardowns`: + +Safe teardowns +-------------- + +The fixture system of pytest is *very* powerful, but it's still being run by a +computer, so it isn't able to figure out how to safely teardown everything we +throw at it. If we aren't careful, an error in the wrong spot might leave stuff +from our tests behind, and that can cause further issues pretty quickly. + +For example, consider the following tests (based off of the mail example from +above): + +.. code-block:: python + + import pytest + + from emaillib import Email, MailAdminClient + + + @pytest.fixture + def setup(): + mail_admin = MailAdminClient() + sending_user = mail_admin.create_user() + receiving_user = mail_admin.create_user() + email = Email(subject="Hey!", body="How's it going?") + sending_user.send_emai(email, receiving_user) + yield receiving_user, email + receiving_user.delete_email(email) + admin_client.delete_user(sending_user) + admin_client.delete_user(receiving_user) + + + def test_email_received(setup): + receiving_user, email = setup + assert email in receiving_user.inbox + +This version is a lot more compact, but it's also harder to read, doesn't have a +very descriptive fixture name, and none of the fixtures can be reused easily. + +There's also a more serious issue, which is that if any of those steps in the +setup raise an exception, none of the teardown code will run. + +One option might be to go with the ``addfinalizer`` method instead of yield +fixtures, but that might get pretty complex and difficult to maintain (and it +wouldn't be compact anymore). + +.. _`safe fixture structure`: + +Safe fixture structure +^^^^^^^^^^^^^^^^^^^^^^ + +The safest and simplest fixture structure requires limiting fixtures to only +making one state-changing action each, and then bundling them together with +their teardown code, as :ref:`the email examples above ` showed. + +The chance that a state-changing operation can fail but still modify state is +neglibible, as most of these operations tend to be `transaction`_-based (at +least at the level of testing where state could be left behind). So if we make +sure that any successful state-changing action gets torn down by moving it to a +separate fixture function and separating it from other, potentially failing +state-changing actions, then our tests will stand the best chance at leaving the +test environment the way they found it. + +For an example, let's say we have a website with a login page, and we have +access to an admin API where we can generate users. For our test, we want to: + +1. Create a user through that admin API +2. Launch a browser using Selenium +3. Go to the login page of our site +4. Log in as the user we created +5. Assert that their name is in the header of the landing page + +We wouldn't want to leave that user in the system, nor would we want to leave +that browser session running, so we'll want to make sure the fixtures that +create those things clean up after themselves. + +Here's what that might look like: + +.. note:: + + For this example, certain fixtures (i.e. ``base_url`` and + ``admin_credentials``) are implied to exist elsewhere. So for now, let's + assume they exist, and we're just not looking at them. + +.. code-block:: python + + from uuid import uuid4 + from urllib.parse import urljoin + + from selenium.webdriver import Chrome + import pytest + + from src.utils.pages import LoginPage, LandingPage + from src.utils import AdminApiClient + from src.utils.data_types import User + + + @pytest.fixture + def admin_client(base_url, admin_credentials): + return AdminApiClient(base_url, **admin_credentials) + + + @pytest.fixture + def user(admin_client): + _user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word") + admin_client.create_user(_user) + yield _user + admin_client.delete_user(_user) + + + @pytest.fixture + def driver(): + _driver = Chrome() + yield _driver + _driver.quit() + + + @pytest.fixture + def login(driver, base_url, user): + driver.get(urljoin(base_url, "/login")) + page = LoginPage(driver) + page.login(user) + + + @pytest.fixture + def landing_page(driver, login): + return LandingPage(driver) + + + def test_name_on_landing_page_after_login(landing_page, user): + assert landing_page.header == f"Welcome, {user.name}!" + +The way the dependencies are laid out means it's unclear if the ``user`` fixture +would execute before the ``driver`` fixture. But that's ok, because those are +atomic operations, and so it doesn't matter which one runs first because the +sequence of events for the test is still `linearizable`_. But what *does* matter +is that, no matter which one runs first, if the one raises an exception while +the other would not have, neither will have left anything behind. If ``driver`` +executes before ``user``, and ``user`` raises an exception, the driver will +still quit, and the user was never made. And if ``driver`` was the one to raise +the exception, then the driver would never have been started and the user would +never have been made. + +.. note: + + While the ``user`` fixture doesn't *actually* need to happen before the + ``driver`` fixture, if we made ``driver`` request ``user``, it might save + some time in the event that making the user raises an exception, since it + won't bother trying to start the driver, which is a fairly expensive + operation. + +.. _`conftest.py`: +.. _`conftest`: - ================================= FAILURES ================================= - ________________________________ test_ehlo _________________________________ +Fixture availabiility +--------------------- - smtp_connection = +Fixture availability is determined from the perspective of the test. A fixture +is only available for tests to request if they are in the scope that fixture is +defined in. If a fixture is defined inside a class, it can only be requested by +tests inside that class. But if a fixture is defined inside the global scope of +the module, than every test in that module, even if it's defined inside a class, +can request it. - def test_ehlo(smtp_connection): - response, msg = smtp_connection.ehlo() - assert response == 250 - > assert 0 # for demo purposes - E assert 0 +Similarly, a test can also only be affected by an autouse fixture if that test +is in the same scope that autouse fixture is defined in (see +:ref:`autouse order`). - test_smtpsimple.py:14: AssertionError - ========================= short test summary info ========================== - FAILED test_smtpsimple.py::test_ehlo - assert 0 - ============================ 1 failed in 0.12s ============================= +A fixture can also request any other fixture, no matter where it's defined, so +long as the test requesting them can see all fixtures involved. -In the failure traceback we see that the test function was called with a -``smtp_connection`` argument, the ``smtplib.SMTP()`` instance created by the fixture -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: +For example, here's a test file with a fixture (``outer``) that requests a +fixture (``inner``) from a scope it wasn't defined in: -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``. +.. literalinclude:: example/fixtures/test_fixtures_request_different_scope.py -2. ``smtp_connection()`` is called to create an instance. +From the tests' perspectives, they have no problem seeing each of the fixtures +they're dependent on: -3. ``test_ehlo()`` is called and fails in the last - line of the test function. +.. image:: example/fixtures/test_fixtures_request_different_scope.svg + :align: center -Note that if you misspell a function argument or want -to use one that isn't available, you'll see an error -with a list of available function arguments. +So when they run, ``outer`` will have no problem finding ``inner``, because +pytest searched from the tests' perspectives. .. note:: + The scope a fixture is defined in has no bearing on the order it will be + instantiated in: the order is mandated by the logic described + :ref:`here `. - You can always issue: +``conftest.py``: sharing fixtures across multiple files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - .. code-block:: bash +The ``conftest.py`` file serves as a means of providing fixtures for an entire +directory. Fixtures defined in a ``conftest.py`` can be used by any test +in that package without needing to import them (pytest will automatically +discover them). - pytest --fixtures test_simplefactory.py +You can have multiple nested directories/packages containing your tests, and +each directory can have its own ``conftest.py`` with its own fixtures, adding on +to the ones provided by the ``conftest.py`` files in parent directories. - to see available fixtures (fixtures with leading ``_`` are only shown if you add the ``-v`` option). +For example, given a test file structure like this: -Fixtures: a prime example of dependency injection ---------------------------------------------------- +:: -Fixtures allow test functions to easily receive and work -against specific pre-initialized application objects without having -to care about import/setup/cleanup details. -It's a prime example of `dependency injection`_ where fixture -functions take the role of the *injector* and test functions are the -*consumers* of fixture objects. + tests/ + __init__.py -.. _`conftest.py`: -.. _`conftest`: + conftest.py + # content of tests/conftest.py + import pytest -``conftest.py``: sharing fixture functions ------------------------------------------- + @pytest.fixture + def order(): + return [] -If during implementing your tests you realize that you -want to use a fixture function from multiple test files you can move it -to a ``conftest.py`` file. -You don't need to import the fixture you want to use in a test, it -automatically gets discovered by pytest. The discovery of -fixture functions starts at test classes, then test modules, then -``conftest.py`` files and finally builtin and third party plugins. + @pytest.fixture + def top(order, innermost): + order.append("top") -You can also use the ``conftest.py`` file to implement -:ref:`local per-directory plugins `. + test_top.py + # content of tests/test_top.py + import pytest -Sharing test data ------------------ + @pytest.fixture + def innermost(order): + order.append("innermost top") -If you want to make test data from files available to your tests, a good way -to do this is by loading these data in a fixture for use by your tests. -This makes use of the automatic caching mechanisms of pytest. + def test_order(order, top): + assert order == ["innermost top", "top"] -Another good approach is by adding the data files in the ``tests`` folder. -There are also community plugins available to help managing this aspect of -testing, e.g. `pytest-datadir `__ -and `pytest-datafiles `__. + subpackage/ + __init__.py -.. _smtpshared: + conftest.py + # content of tests/subpackage/conftest.py + import pytest -Scope: sharing fixtures across classes, modules, packages or session --------------------------------------------------------------------- + @pytest.fixture + def mid(order): + order.append("mid subpackage") -.. regendoc:wipe + test_subpackage.py + # content of tests/subpackage/test_subpackage.py + import pytest -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 ` 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 -each receive the same ``smtp_connection`` fixture instance, thus saving time. -Possible values for ``scope`` are: ``function``, ``class``, ``module``, ``package`` or ``session``. + @pytest.fixture + def innermost(order, mid): + order.append("innermost subpackage") -The next example puts the fixture function into a separate ``conftest.py`` file -so that tests from multiple test modules in the directory can -access the fixture function: + def test_order(order, top): + assert order == ["mid subpackage", "innermost subpackage", "top"] -.. code-block:: python +The boundaries of the scopes can be visualized like this: - # content of conftest.py - import pytest - import smtplib +.. image:: example/fixtures/fixture_availability.svg + :align: center +The directories become their own sort of scope where fixtures that are defined +in a ``conftest.py`` file in that directory become available for that whole +scope. - @pytest.fixture(scope="module") - def smtp_connection(): - return smtplib.SMTP("smtp.gmail.com", 587, timeout=5) +Tests are allowed to search upward (stepping outside a circle) for fixtures, but +can never go down (stepping inside a circle) to continue their search. So +``tests/subpackage/test_subpackage.py::test_order`` would be able to find the +``innermost`` fixture defined in ``tests/subpackage/test_subpackage.py``, but +the one defined in ``tests/test_top.py`` would be unavailable to it because it +would have to step down a level (step inside a circle) to find it. -The name of the fixture again is ``smtp_connection`` and you can access its -result by listing the name ``smtp_connection`` as an input parameter in any -test or fixture function (in or below the directory where ``conftest.py`` is -located): +The first fixture the test finds is the one that will be used, so +:ref:`fixtures can be overriden ` if you need to change or +extend what one does for a particular scope. -.. code-block:: python +You can also use the ``conftest.py`` file to implement +:ref:`local per-directory plugins `. - # content of test_module.py +Fixtures from third-party plugins +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Fixtures don't have to be defined in this structure to be available for tests, +though. They can also be provided by third-party plugins that are installed, and +this is how many pytest plugins operate. As long as those plugins are installed, +the fixtures they provide can be requested from anywhere in your test suite. - def test_ehlo(smtp_connection): - response, msg = smtp_connection.ehlo() - assert response == 250 - assert b"smtp.gmail.com" in msg - assert 0 # for demo purposes +Because they're provided from outside the structure of your test suite, +third-party plugins don't really provide a scope like `conftest.py` files and +the directories in your test suite do. As a result, pytest will search for +fixtures stepping out through scopes as explained previously, only reaching +fixtures defined in plugins *last*. +For example, given the following file structure: - def test_noop(smtp_connection): - response, msg = smtp_connection.noop() - assert response == 250 - assert 0 # for demo purposes +:: -We deliberately insert failing ``assert 0`` statements in order to -inspect what is going on and can now run the tests: + tests/ + __init__.py -.. code-block:: pytest + conftest.py + # content of tests/conftest.py + import pytest - $ pytest test_module.py - =========================== test session starts ============================ - 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 + @pytest.fixture + def order(): + return [] - test_module.py FF [100%] + subpackage/ + __init__.py - ================================= FAILURES ================================= - ________________________________ test_ehlo _________________________________ + conftest.py + # content of tests/subpackage/conftest.py + import pytest - smtp_connection = + @pytest.fixture(autouse=True) + def mid(order, b_fix): + order.append("mid subpackage") - def test_ehlo(smtp_connection): - response, msg = smtp_connection.ehlo() - assert response == 250 - assert b"smtp.gmail.com" in msg - > assert 0 # for demo purposes - E assert 0 + test_subpackage.py + # content of tests/subpackage/test_subpackage.py + import pytest - test_module.py:7: AssertionError - ________________________________ test_noop _________________________________ + @pytest.fixture + def inner(order, mid, a_fix): + order.append("inner subpackage") - smtp_connection = + def test_order(order, inner): + assert order == ["b_fix", "mid subpackage", "a_fix", "inner subpackage"] - def test_noop(smtp_connection): - response, msg = smtp_connection.noop() - assert response == 250 - > assert 0 # for demo purposes - E assert 0 +If ``plugin_a`` is installed and provides the fixture ``a_fix``, and +``plugin_b`` is installed and provides the fixture ``b_fix``, then this is what +the test's search for fixtures would look like: - test_module.py:13: AssertionError - ========================= short test summary info ========================== - FAILED test_module.py::test_ehlo - assert 0 - FAILED test_module.py::test_noop - assert 0 - ============================ 2 failed in 0.12s ============================= +.. image:: example/fixtures/fixture_availability_plugins.svg + :align: center -You see the two ``assert 0`` failing and more importantly you can also see -that the same (module-scoped) ``smtp_connection`` object was passed into the -two test functions because pytest shows the incoming argument values in the -traceback. As a result, the two test functions using ``smtp_connection`` run -as quick as a single one because they reuse the same instance. +pytest will only search for ``a_fix`` and ``b_fix`` in the plugins after +searching for them first in the scopes inside ``tests/``. -If you decide that you rather want to have a session-scoped ``smtp_connection`` -instance, you can simply declare it: +.. note: -.. code-block:: python + pytest can tell you what fixtures are available for a given test if you call + ``pytests`` along with the test's name (or the scope it's in), and provide + the ``--fixtures`` flag, e.g. ``pytest --fixtures test_something.py`` + (fixtures with names that start with ``_`` will only be shown if you also + provide the ``-v`` flag). - @pytest.fixture(scope="session") - def smtp_connection(): - # the returned fixture value will be shared for - # all tests needing it - ... +Sharing test data +----------------- +If you want to make test data from files available to your tests, a good way +to do this is by loading these data in a fixture for use by your tests. +This makes use of the automatic caching mechanisms of pytest. -Fixture scopes -^^^^^^^^^^^^^^ +Another good approach is by adding the data files in the ``tests`` folder. +There are also community plugins available to help managing this aspect of +testing, e.g. `pytest-datadir `__ +and `pytest-datafiles `__. -Fixtures are created when first requested by a test, and are destroyed based on their ``scope``: +.. _`fixture order`: -* ``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. +Fixture instantiation order +--------------------------- -.. note:: +When pytest wants to execute a test, once it knows what fixtures will be +executed, it has to figure out the order they'll be executed in. To do this, it +considers 3 factors: - 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. +1. scope +2. dependencies +3. autouse -.. _dynamic scope: +Names of fixtures or tests, where they're defined, the order they're defined in, +and the order fixtures are requested in have no bearing on execution order +beyond coincidence. While pytest will try to make sure coincidences like these +stay consistent from run to run, it's not something that should be depended on. +If you want to control the order, it's safest to rely on these 3 things and make +sure dependencies are clearly established. -Dynamic scope -^^^^^^^^^^^^^ +Higher-scoped fixtures are executed first +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. versionadded:: 5.2 +Within a function request for fixtures, those of higher-scopes (such as +``session``) are executed before lower-scoped fixtures (such as ``function`` or +``class``). -In some cases, you might want to change the scope of the fixture without changing the code. -To do that, pass a callable to ``scope``. The callable must return a string with a valid scope -and will be executed only once - during the fixture definition. It will be called with two -keyword arguments - ``fixture_name`` as a string and ``config`` with a configuration object. +Here's an example: -This can be especially useful when dealing with fixtures that need time for setup, like spawning -a docker container. You can use the command-line argument to control the scope of the spawned -containers for different environments. See the example below. +.. literalinclude:: example/fixtures/test_fixtures_order_scope.py -.. code-block:: python +The test will pass because the larger scoped fixtures are executing first. - def determine_scope(fixture_name, config): - if config.getoption("--keep-containers", None): - return "session" - return "function" +The order breaks down to this: +.. image:: example/fixtures/test_fixtures_order_scope.svg + :align: center - @pytest.fixture(scope=determine_scope) - def docker_container(): - yield spawn_container() +Fixtures of the same order execute based on dependencies +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +When a fixture requests another fixture, the other fixture is executed first. +So if fixture ``a`` requests fixture ``b``, fixture ``b`` will execute first, +because ``a`` depends on ``b`` and can't operate without it. Even if ``a`` +doesn't need the result of ``b``, it can still request ``b`` if it needs to make +sure it is executed after ``b``. +For example: -Order: Higher-scoped fixtures are instantiated first ----------------------------------------------------- +.. literalinclude:: example/fixtures/test_fixtures_order_dependencies.py +If we map out what depends on what, we get something that look like this: +.. image:: example/fixtures/test_fixtures_order_dependencies.svg + :align: center -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. +The rules provided by each fixture (as to what fixture(s) each one has to come +after) are comprehensive enough that it can be flattened to this: -Consider the code below: +.. image:: example/fixtures/test_fixtures_order_dependencies_flat.svg + :align: center -.. literalinclude:: example/fixtures/test_fixtures_order.py +Enough information has to be provided through these requests in order for pytest +to be able to figure out a clear, linear chain of dependencies, and as a result, +an order of operations for a given test. If there's any ambiguity, and the order +of operations can be interpreted more than one way, you should assume pytest +could go with any one of those interpretations at any point. -The fixtures requested by ``test_order`` will be instantiated in the following order: +For example, if ``d`` didn't request ``c``, i.e.the graph would look like this: -1. ``s1``: is the highest-scoped fixture (``session``). -2. ``m1``: is the second highest-scoped fixture (``module``). -3. ``a1``: is a ``function``-scoped ``autouse`` fixture: it will be instantiated before other fixtures - within the same scope. -4. ``f3``: is a ``function``-scoped fixture, required by ``f1``: it needs to be instantiated at this point -5. ``f1``: is the first ``function``-scoped fixture in ``test_order`` parameter list. -6. ``f2``: is the last ``function``-scoped fixture in ``test_order`` parameter list. +.. image:: example/fixtures/test_fixtures_order_dependencies_unclear.svg + :align: center +Because nothing requested ``c`` other than ``g``, and ``g`` also requests ``f``, +it's now unclear if ``c`` should go before/after ``f``, ``e``, or ``d``. The +only rules that were set for ``c`` is that it must execute after ``b`` and +before ``g``. -.. _`finalization`: +pytest doesn't know where ``c`` should go in the case, so it should be assumed +that it could go anywhere between ``g`` and ``b``. -Fixture finalization / executing teardown code -------------------------------------------------------------- +This isn't necessarily bad, but it's something to keep in mind. If the order +they execute in could affect the behavior a test is targetting, or could +otherwise influence the result of a test, then the order should be defined +explicitely in a way that allows pytest to linearize/"flatten" that order. -pytest supports execution of fixture specific finalization code -when the fixture goes out of scope. By using a ``yield`` statement instead of ``return``, all -the code after the *yield* statement serves as the teardown code: +.. _`autouse order`: -.. code-block:: python +Autouse fixtures are executed first within their scope +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - # content of conftest.py +Autouse fixtures are assumed to apply to every test that could reference them, +so they are executed before other fixtures in that scope. Fixtures that are +requested by autouse fixtures effectively become autouse fixtures themselves for +the tests that the real autouse fixture applies to. - import smtplib - import pytest +So if fixture ``a`` is autouse and fixture ``b`` is not, but fixture ``a`` +requests fixture ``b``, then fixture ``b`` will effectively be an autouse +fixture as well, but only for the tests that ``a`` applies to. +In the last example, the graph became unclear if ``d`` didn't request ``c``. But +if ``c`` was autouse, then ``b`` and ``a`` would effectively also be autouse +because ``c`` depends on them. As a result, they would all be shifted above +non-autouse fixtures within that scope. - @pytest.fixture(scope="module") - def smtp_connection(): - smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5) - yield smtp_connection # provide the fixture value - print("teardown smtp") - smtp_connection.close() +So if the test file looked like this: -The ``print`` and ``smtp.close()`` statements will execute when the last test in -the module has finished execution, regardless of the exception status of the -tests. +.. literalinclude:: example/fixtures/test_fixtures_order_autouse.py -Let's execute it: +the graph would look like this: -.. code-block:: pytest +.. image:: example/fixtures/test_fixtures_order_autouse.svg + :align: center - $ pytest -s -q --tb=no - FFteardown smtp +Because ``c`` can now be put above ``d`` in the graph, pytest can once again +linearize the graph to this: - ========================= short test summary info ========================== - FAILED test_module.py::test_ehlo - assert 0 - FAILED test_module.py::test_noop - assert 0 - 2 failed in 0.12s +In this example, ``c`` makes ``b`` and ``a`` effectively autouse fixtures as +well. -We see that the ``smtp_connection`` instance is finalized after the two -tests finished execution. Note that if we decorated our fixture -function with ``scope='function'`` then fixture setup and cleanup would -occur around each single test. In either case the test -module itself does not need to change or know about these details -of fixture setup. +Be careful with autouse, though, as an autouse fixture will automatically +execute for every test that can reach it, even if they don't request it. For +example, consider this file: -Note that we can also seamlessly use the ``yield`` syntax with ``with`` statements: +.. literalinclude:: example/fixtures/test_fixtures_order_autouse_multiple_scopes.py -.. code-block:: python +Even though nothing in ``TestClassWithC1Request`` is requesting ``c1``, it still +is executed for the tests inside it anyway: - # content of test_yield2.py +.. image:: example/fixtures/test_fixtures_order_autouse_multiple_scopes.svg + :align: center - import smtplib - import pytest +But just because one autouse fixture requested a non-autouse fixture, that +doesn't mean the non-autouse fixture becomes an autouse fixture for all contexts +that it can apply to. It only effectively becomes an auotuse fixture for the +contexts the real autouse fixture (the one that requested the non-autouse +fixture) can apply to. +For example, take a look at this test file: - @pytest.fixture(scope="module") - def smtp_connection(): - with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection: - yield smtp_connection # provide the fixture value +.. literalinclude:: example/fixtures/test_fixtures_order_autouse_temp_effects.py +It would break down to something like this: -The ``smtp_connection`` connection will be closed after the test finished -execution because the ``smtp_connection`` object automatically closes when -the ``with`` statement ends. +.. image:: example/fixtures/test_fixtures_order_autouse_temp_effects.svg + :align: center -Using the contextlib.ExitStack context manager finalizers will always be called -regardless if the fixture *setup* code raises an exception. This is handy to properly -close all resources created by a fixture even if one of them fails to be created/acquired: +For ``test_req`` and ``test_no_req`` inside ``TestClassWithAutouse``, ``c3`` +effectively makes ``c2`` an autouse fixture, which is why ``c2`` and ``c3`` are +executed for both tests, despite not being requested, and why ``c2`` and ``c3`` +are executed before ``c1`` for ``test_req``. -.. code-block:: python +If this made ``c2`` an *actual* autouse fixture, then ``c2`` would also execute +for the tests inside ``TestClassWithoutAutouse``, since they can reference +``c2`` if they wanted to. But it doesn't, because from the perspective of the +``TestClassWithoutAutouse`` tests, ``c2`` isn't an autouse fixture, since they +can't see ``c3``. - # content of test_yield3.py - import contextlib +.. note: - import pytest + pytest can tell you what order the fixtures will execute in for a given test + if you call ``pytests`` along with the test's name (or the scope it's in), + and provide the ``--setup-plan`` flag, e.g. + ``pytest --setup-plan test_something.py`` (fixtures with names that start + with ``_`` will only be shown if you also provide the ``-v`` flag). - @contextlib.contextmanager - def connect(port): - ... # create connection - yield - ... # close connection +Running multiple ``assert`` statements safely +--------------------------------------------- +Sometimes you may want to run multiple asserts after doing all that setup, which +makes sense as, in more complex systems, a single action can kick off multiple +behaviors. pytest has a convenient way of handling this and it combines a bunch +of what we've gone over so far. - @pytest.fixture - def equipments(): - with contextlib.ExitStack() as stack: - yield [stack.enter_context(connect(port)) for port in ("C1", "C3", "C28")] +All that's needed is stepping up to a larger scope, then having the **act** +step defined as an autouse fixture, and finally, making sure all the fixtures +are targetting that highler level scope. -In the example above, if ``"C28"`` fails with an exception, ``"C1"`` and ``"C3"`` will still -be properly closed. +Let's pull :ref:`an example from above `, and tweak it a +bit. Let's say that in addition to checking for a welcome message in the header, +we also want to check for a sign out button, and a link to the user's profile. -Note that if an exception happens during the *setup* code (before the ``yield`` keyword), the -*teardown* code (after the ``yield``) will not be called. +Let's take a look at how we can structure that so we can run multiple asserts +without having to repeat all those steps again. -An alternative option for executing *teardown* code is to -make use of the ``addfinalizer`` method of the `request-context`_ object to register -finalization functions. +.. note:: -Here's the ``smtp_connection`` fixture changed to use ``addfinalizer`` for cleanup: + For this example, certain fixtures (i.e. ``base_url`` and + ``admin_credentials``) are implied to exist elsewhere. So for now, let's + assume they exist, and we're just not looking at them. .. code-block:: python - # content of conftest.py - import smtplib + # contents of tests/end_to_end/test_login.py + from uuid import uuid4 + from urllib.parse import urljoin + + from selenium.webdriver import Chrome import pytest + from src.utils.pages import LoginPage, LandingPage + from src.utils import AdminApiClient + from src.utils.data_types import User - @pytest.fixture(scope="module") - def smtp_connection(request): - smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5) - def fin(): - print("teardown smtp_connection") - smtp_connection.close() + @pytest.fixture(scope="class") + def admin_client(base_url, admin_credentials): + return AdminApiClient(base_url, **admin_credentials) - request.addfinalizer(fin) - return smtp_connection # provide the fixture value + @pytest.fixture(scope="class") + def user(admin_client): + _user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word") + admin_client.create_user(_user) + yield _user + admin_client.delete_user(_user) -Here's the ``equipments`` fixture changed to use ``addfinalizer`` for cleanup: -.. code-block:: python + @pytest.fixture(scope="class") + def driver(): + _driver = Chrome() + yield _driver + _driver.quit() - # content of test_yield3.py - import contextlib - import functools + @pytest.fixture(scope="class") + def landing_page(driver, login): + return LandingPage(driver) - import pytest + class TestLandingPageSuccess: + @pytest.fixture(scope="class", autouse=True) + def login(self, driver, base_url, user): + driver.get(urljoin(base_url, "/login")) + page = LoginPage(driver) + page.login(user) - @contextlib.contextmanager - def connect(port): - ... # create connection - yield - ... # close connection + def test_name_in_header(self, landing_page, user): + assert landing_page.header == f"Welcome, {user.name}!" + def test_sign_out_button(self, landing_page): + assert landing_page.sign_out_button.is_displayed() - @pytest.fixture - def equipments(request): - r = [] - for port in ("C1", "C3", "C28"): - cm = connect(port) - equip = cm.__enter__() - request.addfinalizer(functools.partial(cm.__exit__, None, None, None)) - r.append(equip) - return r + def test_profile_link(self, landing_page, user): + profile_href = urljoin(base_url, f"/profile?id={user.profile_id}") + assert landing_page.profile_link.get_attribute("href") == profile_href +Notice that the methods are only referencing ``self`` in the signature as a +formality. No state is tied to the actual test class as it might be in the +``unittest.TestCase`` framework. Everything is managed by the pytest fixture +system. -Both ``yield`` and ``addfinalizer`` methods work similarly by calling their code after the test -ends. Of course, if an exception happens before the finalize function is registered then it -will not be executed. +Each method only has to request the fixtures that it actually needs without +worrying about order. This is because the **act** fixture is an autouse fixture, +and it made sure all the other fixtures executed before it. There's no more +changes of state that need to take place, so the tests are free to make as many +non-state-changing queries as they want without risking stepping on the toes of +the other tests. + +The ``login`` fixture is defined inside the class as well, because not every one +of the other tests in the module will be expecting a successful login, and the **act** may need to +be handled a little differently for another test class. For example, if we +wanted to write another test scenario around submitting bad credentials, we +could handle it by adding something like this to the test file: + +.. note: + + It's assumed that the page object for this (i.e. ``LoginPage``) raises a + custom exception, ``BadCredentialsException``, when it recognizes text + signifying that on the login form after attempting to log in. + +.. code-block:: python + + class TestLandingPageBadCredentials: + @pytest.fixture(scope="class") + def faux_user(self, user): + _user = deepcopy(user) + _user.password = "badpass" + return _user + + def test_raises_bad_credentials_exception(self, login_page, faux_user): + with pytest.raises(BadCredentialsException): + login_page.login(faux_user) .. _`request-context`: @@ -1239,116 +2267,7 @@ into an ini-file: Currently this will not generate any error or warning, but this is intended to be handled by `#3664 `_. - -.. _`autouse`: -.. _`autouse fixtures`: - -Autouse fixtures (xUnit setup on steroids) ----------------------------------------------------------------------- - -.. regendoc:wipe - -Occasionally, you may want to have fixtures get invoked automatically -without declaring a function argument explicitly or a `usefixtures`_ decorator. -As a practical example, suppose we have a database fixture which has a -begin/rollback/commit architecture and we want to automatically surround -each test method by a transaction and a rollback. Here is a dummy -self-contained implementation of this idea: - -.. code-block:: python - - # content of test_db_transact.py - - import pytest - - - class DB: - def __init__(self): - self.intransaction = [] - - def begin(self, name): - self.intransaction.append(name) - - def rollback(self): - self.intransaction.pop() - - - @pytest.fixture(scope="module") - def db(): - return DB() - - - class TestClass: - @pytest.fixture(autouse=True) - def transact(self, request, db): - db.begin(request.function.__name__) - yield - db.rollback() - - def test_method1(self, db): - assert db.intransaction == ["test_method1"] - - def test_method2(self, db): - assert db.intransaction == ["test_method2"] - -The class-level ``transact`` fixture is marked with *autouse=true* -which implies that all test methods in the class will use this fixture -without a need to state it in the test function signature or with a -class-level ``usefixtures`` decorator. - -If we run it, we get two passing tests: - -.. code-block:: pytest - - $ pytest -q - .. [100%] - 2 passed in 0.12s - -Here is how autouse fixtures work in other scopes: - -- autouse fixtures obey the ``scope=`` keyword-argument: if an autouse fixture - has ``scope='session'`` it will only be run once, no matter where it is - defined. ``scope='class'`` means it will be run once per class, etc. - -- if an autouse fixture is defined in a test module, all its test - functions automatically use it. - -- if an autouse fixture is defined in a conftest.py file then all tests in - all test modules below its directory will invoke the fixture. - -- lastly, and **please use that with care**: if you define an autouse - fixture in a plugin, it will be invoked for all tests in all projects - where the plugin is installed. This can be useful if a fixture only - anyway works in the presence of certain settings e. g. in the ini-file. Such - a global fixture should always quickly determine if it should do - any work and avoid otherwise expensive imports or computation. - -Note that the above ``transact`` fixture may very well be a fixture that -you want to make available in your project without having it generally -active. The canonical way to do that is to put the transact definition -into a conftest.py file **without** using ``autouse``: - -.. code-block:: python - - # content of conftest.py - @pytest.fixture - def transact(request, db): - db.begin() - yield - db.rollback() - -and then e.g. have a TestClass using it by declaring the need: - -.. code-block:: python - - @pytest.mark.usefixtures("transact") - class TestClass: - def test_method1(self): - ... - -All test methods in this TestClass will use the transaction fixture while -other test classes or functions in the module will not use it unless -they also add a ``transact`` reference. +.. _`override fixtures`: Overriding fixtures on various levels ------------------------------------- From 8255effc5b17594987f77c3165b022ee31e2e70a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 16 Dec 2020 15:43:58 -0300 Subject: [PATCH 0335/2846] Remove Travis badge from README Since we stopped testing Python 3.5, we no longer use Travis. --- README.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.rst b/README.rst index 0fb4e363b1c..46b07e59d15 100644 --- a/README.rst +++ b/README.rst @@ -20,9 +20,6 @@ :target: https://codecov.io/gh/pytest-dev/pytest :alt: Code coverage Status -.. image:: https://travis-ci.org/pytest-dev/pytest.svg?branch=master - :target: https://travis-ci.org/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 From 3eef150f2e2a2d2eee3c41a18e10e37a148d4e3b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 17 Dec 2020 08:19:50 -0300 Subject: [PATCH 0336/2846] Remove other references to Travis --- scripts/append_codecov_token.py | 4 ++-- scripts/publish-gh-release-notes.py | 9 +++------ tox.ini | 3 +-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/scripts/append_codecov_token.py b/scripts/append_codecov_token.py index 8eecb0fa51a..5c617aafb54 100644 --- a/scripts/append_codecov_token.py +++ b/scripts/append_codecov_token.py @@ -1,8 +1,8 @@ """ Appends the codecov token to the 'codecov.yml' file at the root of the repository. -This is done by CI during PRs and builds on the pytest-dev repository so we can upload coverage, at least -until codecov grows some native integration like it has with Travis and AppVeyor. +This is done by CI during PRs and builds on the pytest-dev repository so we can +upload coverage, at least until codecov grows some native integration with GitHub Actions. See discussion in https://github.com/pytest-dev/pytest/pull/6441 for more information. """ diff --git a/scripts/publish-gh-release-notes.py b/scripts/publish-gh-release-notes.py index 2531b0221b9..68cbd7adffd 100644 --- a/scripts/publish-gh-release-notes.py +++ b/scripts/publish-gh-release-notes.py @@ -1,7 +1,7 @@ """ Script used to publish GitHub release notes extracted from CHANGELOG.rst. -This script is meant to be executed after a successful deployment in Travis. +This script is meant to be executed after a successful deployment in GitHub actions. Uses the following environment variables: @@ -12,11 +12,8 @@ https://github.com/settings/tokens - It should be encrypted using: - - $travis encrypt GH_RELEASE_NOTES_TOKEN= -r pytest-dev/pytest - - And the contents pasted in the ``deploy.env.secure`` section in the ``travis.yml`` file. + This token should be set in a secret in the repository, which is exposed as an + environment variable in the main.yml workflow file. The script also requires ``pandoc`` to be previously installed in the system. diff --git a/tox.ini b/tox.ini index 43e151c07aa..908f56ea681 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,6 @@ isolated_build = True minversion = 3.20.0 distshare = {homedir}/.tox/distshare -# make sure to update environment list in travis.yml and appveyor.yml envlist = linting py36 @@ -23,7 +22,7 @@ commands = doctesting: {env:_PYTEST_TOX_COVERAGE_RUN:} pytest --doctest-modules --pyargs _pytest coverage: coverage combine coverage: coverage report -m -passenv = USER USERNAME COVERAGE_* TRAVIS PYTEST_ADDOPTS TERM +passenv = USER USERNAME COVERAGE_* PYTEST_ADDOPTS TERM setenv = _PYTEST_TOX_DEFAULT_POSARGS={env:_PYTEST_TOX_POSARGS_DOCTESTING:} {env:_PYTEST_TOX_POSARGS_LSOF:} {env:_PYTEST_TOX_POSARGS_XDIST:} From bd894e3065ba6fa13327ad5dfc94f2b6208cf0ff Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 17 Dec 2020 16:55:36 +0000 Subject: [PATCH 0337/2846] Add Changelog to setup.cfg (#8166) Co-authored-by: Thomas Grainger Co-authored-by: Bruno Oliveira --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index 09c07d5bb6c..14fdb6df5c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,8 @@ classifiers = Topic :: Utilities keywords = test, unittest project_urls = + Changelog=https://docs.pytest.org/en/stable/changelog.html + Twitter=https://twitter.com/pytestdotorg Source=https://github.com/pytest-dev/pytest Tracker=https://github.com/pytest-dev/pytest/issues From 1264404fe712de864aa365416cee648f3cd56128 Mon Sep 17 00:00:00 2001 From: antonblr Date: Thu, 17 Dec 2020 21:01:20 -0800 Subject: [PATCH 0338/2846] infra: Temporary pin setup-python GH action to v2.1.4 --- .github/workflows/main.yml | 9 ++++++--- .github/workflows/prepare-release-pr.yml | 3 ++- .github/workflows/release-on-comment.yml | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2b779279fdc..4ae366a7c40 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -123,7 +123,8 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + # TODO: Use "v2" tag once https://github.com/actions/setup-python/issues/171 is resolved. + uses: actions/setup-python@v2.1.4 with: python-version: ${{ matrix.python }} - name: Install dependencies @@ -158,7 +159,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + # TODO: Use "v2" tag once https://github.com/actions/setup-python/issues/171 is resolved. + - uses: actions/setup-python@v2.1.4 - 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@v2 @@ -184,7 +186,8 @@ jobs: with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + # TODO: Use "v2" tag once https://github.com/actions/setup-python/issues/171 is resolved. + uses: actions/setup-python@v2.1.4 with: python-version: "3.7" - name: Install dependencies diff --git a/.github/workflows/prepare-release-pr.yml b/.github/workflows/prepare-release-pr.yml index dec35236430..e4bf51d1184 100644 --- a/.github/workflows/prepare-release-pr.yml +++ b/.github/workflows/prepare-release-pr.yml @@ -22,7 +22,8 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + # TODO: Use "v2" tag once https://github.com/actions/setup-python/issues/171 is resolved. + uses: actions/setup-python@v2.1.4 with: python-version: "3.8" diff --git a/.github/workflows/release-on-comment.yml b/.github/workflows/release-on-comment.yml index 94863d896b9..f51cd86e9ee 100644 --- a/.github/workflows/release-on-comment.yml +++ b/.github/workflows/release-on-comment.yml @@ -19,7 +19,8 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + # TODO: Use "v2" tag once https://github.com/actions/setup-python/issues/171 is resolved. + uses: actions/setup-python@v2.1.4 with: python-version: "3.8" - name: Install dependencies From 4da445dc2e9212f724ae0dc0f7c6fdf465801a8e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 18 Dec 2020 10:44:20 -0800 Subject: [PATCH 0339/2846] Revert "infra: Temporary pin setup-python GH action to v2.1.4" --- .github/workflows/main.yml | 9 +++------ .github/workflows/prepare-release-pr.yml | 3 +-- .github/workflows/release-on-comment.yml | 3 +-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ae366a7c40..2b779279fdc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -123,8 +123,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python }} - # TODO: Use "v2" tag once https://github.com/actions/setup-python/issues/171 is resolved. - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install dependencies @@ -159,8 +158,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - # TODO: Use "v2" tag once https://github.com/actions/setup-python/issues/171 is resolved. - - uses: actions/setup-python@v2.1.4 + - 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@v2 @@ -186,8 +184,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python - # TODO: Use "v2" tag once https://github.com/actions/setup-python/issues/171 is resolved. - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2 with: python-version: "3.7" - name: Install dependencies diff --git a/.github/workflows/prepare-release-pr.yml b/.github/workflows/prepare-release-pr.yml index e4bf51d1184..dec35236430 100644 --- a/.github/workflows/prepare-release-pr.yml +++ b/.github/workflows/prepare-release-pr.yml @@ -22,8 +22,7 @@ jobs: fetch-depth: 0 - name: Set up Python - # TODO: Use "v2" tag once https://github.com/actions/setup-python/issues/171 is resolved. - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2 with: python-version: "3.8" diff --git a/.github/workflows/release-on-comment.yml b/.github/workflows/release-on-comment.yml index f51cd86e9ee..94863d896b9 100644 --- a/.github/workflows/release-on-comment.yml +++ b/.github/workflows/release-on-comment.yml @@ -19,8 +19,7 @@ jobs: fetch-depth: 0 - name: Set up Python - # TODO: Use "v2" tag once https://github.com/actions/setup-python/issues/171 is resolved. - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2 with: python-version: "3.8" - name: Install dependencies From 293a7c962da6aedbccf6ce4fc6fecf345e835432 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 18 Dec 2020 10:45:28 -0800 Subject: [PATCH 0340/2846] Use new pypy3 github actions syntax --- .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 2b779279fdc..beb50178528 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,7 +89,7 @@ jobs: os: ubuntu-latest tox_env: "py39-xdist" - name: "ubuntu-pypy3" - python: "pypy3" + python: "pypy-3.7" os: ubuntu-latest tox_env: "pypy3-xdist" From 15156e94c49da11c6fc5a57d576d655cc7794fdf Mon Sep 17 00:00:00 2001 From: antonblr Date: Tue, 15 Dec 2020 20:16:05 -0800 Subject: [PATCH 0341/2846] tests: Migrate to pytester - final update --- .github/workflows/main.yml | 3 +- src/_pytest/nodes.py | 2 +- src/_pytest/python.py | 2 +- testing/deprecated_test.py | 35 ++- testing/test_cacheprovider.py | 2 +- testing/test_collection.py | 25 +- testing/test_debugging.py | 5 +- testing/test_junitxml.py | 425 ++++++++++++++++++++-------------- testing/test_link_resolve.py | 29 ++- testing/test_main.py | 29 ++- testing/test_parseopt.py | 2 +- testing/test_pytester.py | 273 +++++++++++----------- testing/test_unittest.py | 387 ++++++++++++++++--------------- 13 files changed, 656 insertions(+), 563 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index beb50178528..1b6e85fd87e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -123,7 +123,8 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + # https://github.com/actions/setup-python/issues/171 + uses: actions/setup-python@v2.1.4 with: python-version: ${{ matrix.python }} - name: Install dependencies diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index fee0770eb2b..27c76a04302 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -528,7 +528,7 @@ def gethookproxy(self, fspath: "os.PathLike[str]"): warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.gethookproxy(fspath) - def isinitpath(self, path: py.path.local) -> bool: + def isinitpath(self, path: "os.PathLike[str]") -> bool: warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.isinitpath(path) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 18e449b9361..018e368f45e 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -660,7 +660,7 @@ def gethookproxy(self, fspath: "os.PathLike[str]"): warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.gethookproxy(fspath) - def isinitpath(self, path: py.path.local) -> bool: + def isinitpath(self, path: "os.PathLike[str]") -> bool: warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.isinitpath(path) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index d213414ee45..6d92d181f99 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -5,24 +5,23 @@ import pytest from _pytest import deprecated from _pytest.pytester import Pytester -from _pytest.pytester import Testdir @pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore # false positive due to dynamic attribute -def test_pytest_collect_module_deprecated(attribute): +def test_pytest_collect_module_deprecated(attribute) -> None: with pytest.warns(DeprecationWarning, match=attribute): getattr(pytest.collect, attribute) @pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS)) @pytest.mark.filterwarnings("default") -def test_external_plugins_integrated(testdir, plugin): - testdir.syspathinsert() - testdir.makepyfile(**{plugin: ""}) +def test_external_plugins_integrated(pytester: Pytester, plugin) -> None: + pytester.syspathinsert() + pytester.makepyfile(**{plugin: ""}) with pytest.warns(pytest.PytestConfigWarning): - testdir.parseconfig("-p", plugin) + pytester.parseconfig("-p", plugin) def test_fillfuncargs_is_deprecated() -> None: @@ -49,32 +48,32 @@ def test_fillfixtures_is_deprecated() -> None: _pytest.fixtures.fillfixtures(mock.Mock()) -def test_minus_k_dash_is_deprecated(testdir) -> None: - threepass = testdir.makepyfile( +def test_minus_k_dash_is_deprecated(pytester: Pytester) -> None: + threepass = pytester.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 = pytester.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( +def test_minus_k_colon_is_deprecated(pytester: Pytester) -> None: + threepass = pytester.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 = pytester.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_fscollector_gethookproxy_isinitpath(pytester: Pytester) -> None: + module = pytester.getmodulecol( """ def test_foo(): pass """, @@ -85,16 +84,16 @@ def test_foo(): pass assert isinstance(package, pytest.Package) with pytest.warns(pytest.PytestDeprecationWarning, match="gethookproxy"): - package.gethookproxy(testdir.tmpdir) + package.gethookproxy(pytester.path) with pytest.warns(pytest.PytestDeprecationWarning, match="isinitpath"): - package.isinitpath(testdir.tmpdir) + package.isinitpath(pytester.path) # 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) + session.gethookproxy(pytester.path) + session.isinitpath(pytester.path) assert len(rec) == 0 diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 7f0827bd488..ebd455593f3 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -1050,7 +1050,7 @@ def test_packages(self, pytester: Pytester) -> None: class TestNewFirst: - def test_newfirst_usecase(self, pytester: Pytester, testdir) -> None: + def test_newfirst_usecase(self, pytester: Pytester) -> None: pytester.makepyfile( **{ "test_1/test_1.py": """ diff --git a/testing/test_collection.py b/testing/test_collection.py index 862c1aba8d2..2d03fda39de 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -6,6 +6,8 @@ from pathlib import Path from typing import List +import py.path + import pytest from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest @@ -16,7 +18,6 @@ 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: @@ -206,15 +207,17 @@ def test_ignored_virtualenvs_norecursedirs_precedence( "Activate.ps1", ), ) - def test__in_venv(self, testdir: Testdir, fname: str) -> None: + def test__in_venv(self, pytester: Pytester, 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 - base_path = testdir.tmpdir.mkdir("venv") - assert _in_venv(base_path) is False + base_path = pytester.mkdir("venv") + assert _in_venv(py.path.local(base_path)) is False # with bin/activate, totally a virtualenv - base_path.ensure(bindir, fname) - assert _in_venv(base_path) is True + bin_path = base_path.joinpath(bindir) + bin_path.mkdir() + bin_path.joinpath(fname).touch() + assert _in_venv(py.path.local(base_path)) is True def test_custom_norecursedirs(self, pytester: Pytester) -> None: pytester.makeini( @@ -264,7 +267,7 @@ def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> No class TestCollectPluginHookRelay: - def test_pytest_collect_file(self, testdir: Testdir) -> None: + def test_pytest_collect_file(self, pytester: Pytester) -> None: wascalled = [] class Plugin: @@ -273,8 +276,8 @@ def pytest_collect_file(self, path): # Ignore hidden files, e.g. .testmondata. wascalled.append(path) - testdir.makefile(".abc", "xyz") - pytest.main(testdir.tmpdir, plugins=[Plugin()]) + pytester.makefile(".abc", "xyz") + pytest.main(py.path.local(pytester.path), plugins=[Plugin()]) assert len(wascalled) == 1 assert wascalled[0].ext == ".abc" @@ -1336,7 +1339,7 @@ def test_does_not_put_src_on_path(pytester: Pytester) -> None: assert result.ret == ExitCode.OK -def test_fscollector_from_parent(testdir: Testdir, request: FixtureRequest) -> None: +def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) -> None: """Ensure File.from_parent can forward custom arguments to the constructor. Context: https://github.com/pytest-dev/pytest-cpp/pull/47 @@ -1352,7 +1355,7 @@ 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=testdir.tmpdir / "foo", x=10 + parent=request.session, fspath=py.path.local(pytester.path) / "foo", x=10 ) assert collector.x == 10 diff --git a/testing/test_debugging.py b/testing/test_debugging.py index ed96f7ec781..8218b7a0ede 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -21,11 +21,10 @@ @pytest.fixture(autouse=True) -def pdb_env(request): +def pdb_env(request, monkeypatch: MonkeyPatch): if "pytester" in request.fixturenames: # Disable pdb++ with inner tests. - pytester = request.getfixturevalue("testdir") - pytester.monkeypatch.setenv("PDBPP_HIJACK_PDB", "0") + monkeypatch.setenv("PDBPP_HIJACK_PDB", "0") def runpdb_and_get_report(pytester: Pytester, source: str): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 006bea96280..3e445dcefc5 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -4,24 +4,31 @@ from pathlib import Path from typing import cast 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 from xml.dom import minidom -import py import xmlschema import pytest from _pytest.config import Config from _pytest.junitxml import bin_xml_escape from _pytest.junitxml import LogXML +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Pytester +from _pytest.pytester import RunResult from _pytest.reports import BaseReport from _pytest.reports import TestReport from _pytest.store import Store +T = TypeVar("T") + @pytest.fixture(scope="session") -def schema(): +def schema() -> xmlschema.XMLSchema: """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: @@ -29,7 +36,7 @@ def schema(): @pytest.fixture -def run_and_parse(testdir, schema): +def run_and_parse(pytester: Pytester, schema: xmlschema.XMLSchema) -> T: """Fixture that returns a function that can be used to execute pytest and return the parsed ``DomNode`` of the root xml node. @@ -37,18 +44,20 @@ def run_and_parse(testdir, schema): "xunit2" is also automatically validated against the schema. """ - def run(*args, family="xunit1"): + def run( + *args: Union[str, "os.PathLike[str]"], family: Optional[str] = "xunit1", + ) -> Tuple[RunResult, "DomNode"]: if family: args = ("-o", "junit_family=" + family) + args - xml_path = testdir.tmpdir.join("junit.xml") - result = testdir.runpytest("--junitxml=%s" % xml_path, *args) + xml_path = pytester.path.joinpath("junit.xml") + result = pytester.runpytest("--junitxml=%s" % xml_path, *args) if family == "xunit2": with xml_path.open() as f: schema.validate(f) xmldoc = minidom.parse(str(xml_path)) return result, DomNode(xmldoc) - return run + return cast(T, run) def assert_attr(node, **kwargs): @@ -130,8 +139,10 @@ def next_sibling(self): class TestPython: @parametrize_families - def test_summing_simple(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_summing_simple( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest def test_pass(): @@ -154,8 +165,10 @@ def test_xpass(): node.assert_attr(name="pytest", errors=0, failures=1, skipped=2, tests=5) @parametrize_families - def test_summing_simple_with_errors(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_summing_simple_with_errors( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture @@ -181,8 +194,10 @@ def test_xpass(): node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5) @parametrize_families - def test_hostname_in_xml(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_hostname_in_xml( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ def test_pass(): pass @@ -193,8 +208,10 @@ def test_pass(): node.assert_attr(hostname=platform.node()) @parametrize_families - def test_timestamp_in_xml(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_timestamp_in_xml( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ def test_pass(): pass @@ -206,8 +223,10 @@ 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, mock_timing): - testdir.makepyfile( + def test_timing_function( + self, pytester: Pytester, run_and_parse, mock_timing + ) -> None: + pytester.makepyfile( """ from _pytest import timing def setup_module(): @@ -226,8 +245,12 @@ def test_sleep(): @pytest.mark.parametrize("duration_report", ["call", "total"]) def test_junit_duration_report( - self, testdir, monkeypatch, duration_report, run_and_parse - ): + self, + pytester: Pytester, + monkeypatch: MonkeyPatch, + duration_report, + run_and_parse, + ) -> None: # mock LogXML.node_reporter so it always sets a known duration to each test report object original_node_reporter = LogXML.node_reporter @@ -239,7 +262,7 @@ def node_reporter_wrapper(s, report): monkeypatch.setattr(LogXML, "node_reporter", node_reporter_wrapper) - testdir.makepyfile( + pytester.makepyfile( """ def test_foo(): pass @@ -256,8 +279,8 @@ def test_foo(): assert val == 1.0 @parametrize_families - def test_setup_error(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_setup_error(self, pytester: Pytester, run_and_parse, xunit_family) -> None: + pytester.makepyfile( """ import pytest @@ -279,8 +302,10 @@ def test_function(arg): assert "ValueError" in fnode.toxml() @parametrize_families - def test_teardown_error(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_teardown_error( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest @@ -302,8 +327,10 @@ def test_function(arg): assert "ValueError" in fnode.toxml() @parametrize_families - def test_call_failure_teardown_error(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_call_failure_teardown_error( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest @@ -331,8 +358,10 @@ def test_function(arg): ) @parametrize_families - def test_skip_contains_name_reason(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_skip_contains_name_reason( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest def test_skip(): @@ -349,8 +378,10 @@ def test_skip(): snode.assert_attr(type="pytest.skip", message="hello23") @parametrize_families - def test_mark_skip_contains_name_reason(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_mark_skip_contains_name_reason( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skip(reason="hello24") @@ -371,9 +402,9 @@ def test_skip(): @parametrize_families def test_mark_skipif_contains_name_reason( - self, testdir, run_and_parse, xunit_family - ): - testdir.makepyfile( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest GLOBAL_CONDITION = True @@ -395,9 +426,9 @@ def test_skip(): @parametrize_families def test_mark_skip_doesnt_capture_output( - self, testdir, run_and_parse, xunit_family - ): - testdir.makepyfile( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skip(reason="foo") @@ -411,8 +442,10 @@ def test_skip(): assert "bar!" not in node_xml @parametrize_families - def test_classname_instance(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_classname_instance( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ class TestClass(object): def test_method(self): @@ -429,9 +462,11 @@ def test_method(self): ) @parametrize_families - def test_classname_nested_dir(self, testdir, run_and_parse, xunit_family): - p = testdir.tmpdir.ensure("sub", "test_hello.py") - p.write("def test_func(): 0/0") + def test_classname_nested_dir( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + p = pytester.mkdir("sub").joinpath("test_hello.py") + p.write_text("def test_func(): 0/0") result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") @@ -440,9 +475,11 @@ def test_classname_nested_dir(self, testdir, run_and_parse, xunit_family): tnode.assert_attr(classname="sub.test_hello", name="test_func") @parametrize_families - def test_internal_error(self, testdir, run_and_parse, xunit_family): - testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0") - testdir.makepyfile("def test_function(): pass") + def test_internal_error( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makeconftest("def pytest_runtest_protocol(): 0 / 0") + pytester.makepyfile("def test_function(): pass") result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") @@ -458,9 +495,9 @@ def test_internal_error(self, testdir, run_and_parse, xunit_family): ) @parametrize_families def test_failure_function( - self, testdir, junit_logging, run_and_parse, xunit_family - ): - testdir.makepyfile( + self, pytester: Pytester, junit_logging, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import logging import sys @@ -521,8 +558,10 @@ def test_fail(): ), "Found unexpected content: system-err" @parametrize_families - def test_failure_verbose_message(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_failure_verbose_message( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import sys def test_fail(): @@ -536,8 +575,10 @@ def test_fail(): fnode.assert_attr(message="AssertionError: An error\nassert 0") @parametrize_families - def test_failure_escape(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_failure_escape( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.parametrize('arg1', "<&'", ids="<&'") @@ -564,8 +605,10 @@ def test_func(arg1): assert "%s\n" % char in text @parametrize_families - def test_junit_prefixing(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_junit_prefixing( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ def test_func(): assert 0 @@ -586,8 +629,10 @@ def test_hello(self): ) @parametrize_families - def test_xfailure_function(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_xfailure_function( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest def test_xfail(): @@ -604,8 +649,10 @@ def test_xfail(): fnode.assert_attr(type="pytest.xfail", message="42") @parametrize_families - def test_xfailure_marker(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_xfailure_marker( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.xfail(reason="42") @@ -625,8 +672,10 @@ def test_xfail(): @pytest.mark.parametrize( "junit_logging", ["no", "log", "system-out", "system-err", "out-err", "all"] ) - def test_xfail_captures_output_once(self, testdir, junit_logging, run_and_parse): - testdir.makepyfile( + def test_xfail_captures_output_once( + self, pytester: Pytester, junit_logging, run_and_parse + ) -> None: + pytester.makepyfile( """ import sys import pytest @@ -652,8 +701,10 @@ def test_fail(): assert len(tnode.find_by_tag("system-out")) == 0 @parametrize_families - def test_xfailure_xpass(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_xfailure_xpass( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.xfail @@ -669,8 +720,10 @@ def test_xpass(): tnode.assert_attr(classname="test_xfailure_xpass", name="test_xpass") @parametrize_families - def test_xfailure_xpass_strict(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile( + def test_xfailure_xpass_strict( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.xfail(strict=True, reason="This needs to fail!") @@ -688,8 +741,10 @@ def test_xpass(): fnode.assert_attr(message="[XPASS(strict)] This needs to fail!") @parametrize_families - def test_collect_error(self, testdir, run_and_parse, xunit_family): - testdir.makepyfile("syntax error") + def test_collect_error( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makepyfile("syntax error") result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") @@ -699,9 +754,9 @@ def test_collect_error(self, testdir, run_and_parse, xunit_family): fnode.assert_attr(message="collection failure") assert "SyntaxError" in fnode.toxml() - def test_unicode(self, testdir, run_and_parse): + def test_unicode(self, pytester: Pytester, run_and_parse) -> None: value = "hx\xc4\x85\xc4\x87\n" - testdir.makepyfile( + pytester.makepyfile( """\ # coding: latin1 def test_hello(): @@ -716,9 +771,9 @@ def test_hello(): fnode = tnode.find_first_by_tag("failure") assert "hx" in fnode.toxml() - def test_assertion_binchars(self, testdir, run_and_parse): + def test_assertion_binchars(self, pytester: Pytester, run_and_parse) -> None: """This test did fail when the escaping wasn't strict.""" - testdir.makepyfile( + pytester.makepyfile( """ M1 = '\x01\x02\x03\x04' @@ -732,8 +787,10 @@ def test_str_compare(): print(dom.toxml()) @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) - def test_pass_captures_stdout(self, testdir, run_and_parse, junit_logging): - testdir.makepyfile( + def test_pass_captures_stdout( + self, pytester: Pytester, run_and_parse, junit_logging + ) -> None: + pytester.makepyfile( """ def test_pass(): print('hello-stdout') @@ -753,8 +810,10 @@ def test_pass(): ), "'hello-stdout' should be in system-out" @pytest.mark.parametrize("junit_logging", ["no", "system-err"]) - def test_pass_captures_stderr(self, testdir, run_and_parse, junit_logging): - testdir.makepyfile( + def test_pass_captures_stderr( + self, pytester: Pytester, run_and_parse, junit_logging + ) -> None: + pytester.makepyfile( """ import sys def test_pass(): @@ -775,8 +834,10 @@ def test_pass(): ), "'hello-stderr' should be in system-err" @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) - def test_setup_error_captures_stdout(self, testdir, run_and_parse, junit_logging): - testdir.makepyfile( + def test_setup_error_captures_stdout( + self, pytester: Pytester, run_and_parse, junit_logging + ) -> None: + pytester.makepyfile( """ import pytest @@ -802,8 +863,10 @@ def test_function(arg): ), "'hello-stdout' should be in system-out" @pytest.mark.parametrize("junit_logging", ["no", "system-err"]) - def test_setup_error_captures_stderr(self, testdir, run_and_parse, junit_logging): - testdir.makepyfile( + def test_setup_error_captures_stderr( + self, pytester: Pytester, run_and_parse, junit_logging + ) -> None: + pytester.makepyfile( """ import sys import pytest @@ -830,8 +893,10 @@ def test_function(arg): ), "'hello-stderr' should be in system-err" @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) - def test_avoid_double_stdout(self, testdir, run_and_parse, junit_logging): - testdir.makepyfile( + def test_avoid_double_stdout( + self, pytester: Pytester, run_and_parse, junit_logging + ) -> None: + pytester.makepyfile( """ import sys import pytest @@ -858,7 +923,7 @@ def test_function(arg): assert "hello-stdout teardown" in systemout.toxml() -def test_mangle_test_address(): +def test_mangle_test_address() -> None: from _pytest.junitxml import mangle_test_address address = "::".join(["a/my.py.thing.py", "Class", "()", "method", "[a-1-::]"]) @@ -866,7 +931,7 @@ def test_mangle_test_address(): assert newnames == ["a.my.py.thing", "Class", "method", "[a-1-::]"] -def test_dont_configure_on_workers(tmpdir) -> None: +def test_dont_configure_on_workers(tmp_path: Path) -> None: gotten: List[object] = [] class FakeConfig: @@ -882,8 +947,8 @@ def getini(self, name): return "pytest" junitprefix = None - # XXX: shouldn't need tmpdir ? - xmlpath = str(tmpdir.join("junix.xml")) + # XXX: shouldn't need tmp_path ? + xmlpath = str(tmp_path.joinpath("junix.xml")) register = gotten.append fake_config = cast(Config, FakeConfig()) @@ -898,8 +963,10 @@ def getini(self, name): class TestNonPython: @parametrize_families - def test_summing_simple(self, testdir, run_and_parse, xunit_family): - testdir.makeconftest( + def test_summing_simple( + self, pytester: Pytester, run_and_parse, xunit_family + ) -> None: + pytester.makeconftest( """ import pytest def pytest_collect_file(path, parent): @@ -912,7 +979,7 @@ def repr_failure(self, excinfo): return "custom item runtest failed" """ ) - testdir.tmpdir.join("myfile.xyz").write("hello") + pytester.path.joinpath("myfile.xyz").write_text("hello") result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") @@ -925,9 +992,9 @@ def repr_failure(self, excinfo): @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) -def test_nullbyte(testdir, junit_logging): +def test_nullbyte(pytester: Pytester, junit_logging) -> None: # A null byte can not occur in XML (see section 2.2 of the spec) - testdir.makepyfile( + pytester.makepyfile( """ import sys def test_print_nullbyte(): @@ -936,9 +1003,9 @@ def test_print_nullbyte(): assert False """ ) - xmlf = testdir.tmpdir.join("junit.xml") - testdir.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) - text = xmlf.read() + xmlf = pytester.path.joinpath("junit.xml") + pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) + text = xmlf.read_text() assert "\x00" not in text if junit_logging == "system-out": assert "#x00" in text @@ -947,9 +1014,9 @@ def test_print_nullbyte(): @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) -def test_nullbyte_replace(testdir, junit_logging): +def test_nullbyte_replace(pytester: Pytester, junit_logging) -> None: # Check if the null byte gets replaced - testdir.makepyfile( + pytester.makepyfile( """ import sys def test_print_nullbyte(): @@ -958,16 +1025,16 @@ def test_print_nullbyte(): assert False """ ) - xmlf = testdir.tmpdir.join("junit.xml") - testdir.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) - text = xmlf.read() + xmlf = pytester.path.joinpath("junit.xml") + pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) + text = xmlf.read_text() if junit_logging == "system-out": assert "#x0" in text if junit_logging == "no": assert "#x0" not in text -def test_invalid_xml_escape(): +def test_invalid_xml_escape() -> None: # Test some more invalid xml chars, the full range should be # tested really but let's just test the edges of the ranges # instead. @@ -1003,52 +1070,52 @@ def test_invalid_xml_escape(): assert chr(i) == bin_xml_escape(chr(i)) -def test_logxml_path_expansion(tmpdir, monkeypatch): - home_tilde = py.path.local(os.path.expanduser("~")).join("test.xml") - xml_tilde = LogXML("~%stest.xml" % tmpdir.sep, None) - assert xml_tilde.logfile == home_tilde +def test_logxml_path_expansion(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + home_tilde = Path(os.path.expanduser("~")).joinpath("test.xml") + xml_tilde = LogXML(Path("~", "test.xml"), None) + assert xml_tilde.logfile == str(home_tilde) - monkeypatch.setenv("HOME", str(tmpdir)) + monkeypatch.setenv("HOME", str(tmp_path)) home_var = os.path.normpath(os.path.expandvars("$HOME/test.xml")) - xml_var = LogXML("$HOME%stest.xml" % tmpdir.sep, None) - assert xml_var.logfile == home_var + xml_var = LogXML(Path("$HOME", "test.xml"), None) + assert xml_var.logfile == str(home_var) -def test_logxml_changingdir(testdir): - testdir.makepyfile( +def test_logxml_changingdir(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_func(): import os os.chdir("a") """ ) - testdir.tmpdir.mkdir("a") - result = testdir.runpytest("--junitxml=a/x.xml") + pytester.mkdir("a") + result = pytester.runpytest("--junitxml=a/x.xml") assert result.ret == 0 - assert testdir.tmpdir.join("a/x.xml").check() + assert pytester.path.joinpath("a/x.xml").exists() -def test_logxml_makedir(testdir): +def test_logxml_makedir(pytester: Pytester) -> None: """--junitxml should automatically create directories for the xml file""" - testdir.makepyfile( + pytester.makepyfile( """ def test_pass(): pass """ ) - result = testdir.runpytest("--junitxml=path/to/results.xml") + result = pytester.runpytest("--junitxml=path/to/results.xml") assert result.ret == 0 - assert testdir.tmpdir.join("path/to/results.xml").check() + assert pytester.path.joinpath("path/to/results.xml").exists() -def test_logxml_check_isdir(testdir): +def test_logxml_check_isdir(pytester: Pytester) -> None: """Give an error if --junit-xml is a directory (#2089)""" - result = testdir.runpytest("--junit-xml=.") + result = pytester.runpytest("--junit-xml=.") result.stderr.fnmatch_lines(["*--junitxml must be a filename*"]) -def test_escaped_parametrized_names_xml(testdir, run_and_parse): - testdir.makepyfile( +def test_escaped_parametrized_names_xml(pytester: Pytester, run_and_parse) -> None: + pytester.makepyfile( """\ import pytest @pytest.mark.parametrize('char', ["\\x00"]) @@ -1062,8 +1129,10 @@ def test_func(char): node.assert_attr(name="test_func[\\x00]") -def test_double_colon_split_function_issue469(testdir, run_and_parse): - testdir.makepyfile( +def test_double_colon_split_function_issue469( + pytester: Pytester, run_and_parse +) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.parametrize('param', ["double::colon"]) @@ -1078,8 +1147,8 @@ def test_func(param): node.assert_attr(name="test_func[double::colon]") -def test_double_colon_split_method_issue469(testdir, run_and_parse): - testdir.makepyfile( +def test_double_colon_split_method_issue469(pytester: Pytester, run_and_parse) -> None: + pytester.makepyfile( """ import pytest class TestClass(object): @@ -1095,8 +1164,8 @@ def test_func(self, param): node.assert_attr(name="test_func[double::colon]") -def test_unicode_issue368(testdir) -> None: - path = testdir.tmpdir.join("test.xml") +def test_unicode_issue368(pytester: Pytester) -> None: + path = pytester.path.joinpath("test.xml") log = LogXML(str(path), None) ustr = "ВНИ!" @@ -1125,8 +1194,8 @@ class Report(BaseReport): log.pytest_sessionfinish() -def test_record_property(testdir, run_and_parse): - testdir.makepyfile( +def test_record_property(pytester: Pytester, run_and_parse) -> None: + pytester.makepyfile( """ import pytest @@ -1147,8 +1216,8 @@ def test_record(record_property, other): result.stdout.fnmatch_lines(["*= 1 passed in *"]) -def test_record_property_same_name(testdir, run_and_parse): - testdir.makepyfile( +def test_record_property_same_name(pytester: Pytester, run_and_parse) -> None: + pytester.makepyfile( """ def test_record_with_same_name(record_property): record_property("foo", "bar") @@ -1165,8 +1234,8 @@ def test_record_with_same_name(record_property): @pytest.mark.parametrize("fixture_name", ["record_property", "record_xml_attribute"]) -def test_record_fixtures_without_junitxml(testdir, fixture_name): - testdir.makepyfile( +def test_record_fixtures_without_junitxml(pytester: Pytester, fixture_name) -> None: + pytester.makepyfile( """ def test_record({fixture_name}): {fixture_name}("foo", "bar") @@ -1174,19 +1243,19 @@ def test_record({fixture_name}): fixture_name=fixture_name ) ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 @pytest.mark.filterwarnings("default") -def test_record_attribute(testdir, run_and_parse): - testdir.makeini( +def test_record_attribute(pytester: Pytester, run_and_parse) -> None: + pytester.makeini( """ [pytest] junit_family = xunit1 """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1209,15 +1278,17 @@ 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): +def test_record_fixtures_xunit2( + pytester: Pytester, fixture_name, run_and_parse +) -> None: """Ensure record_xml_attribute and record_property drop values when outside of legacy family.""" - testdir.makeini( + pytester.makeini( """ [pytest] junit_family = xunit2 """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1246,13 +1317,15 @@ def test_record({fixture_name}, other): result.stdout.fnmatch_lines(expected_lines) -def test_random_report_log_xdist(testdir, monkeypatch, run_and_parse): +def test_random_report_log_xdist( + pytester: Pytester, monkeypatch: MonkeyPatch, run_and_parse +) -> None: """`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).""" pytest.importorskip("xdist") monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) - testdir.makepyfile( + pytester.makepyfile( """ import pytest, time @pytest.mark.parametrize('i', list(range(30))) @@ -1271,8 +1344,8 @@ def test_x(i): @parametrize_families -def test_root_testsuites_tag(testdir, run_and_parse, xunit_family): - testdir.makepyfile( +def test_root_testsuites_tag(pytester: Pytester, run_and_parse, xunit_family) -> None: + pytester.makepyfile( """ def test_x(): pass @@ -1285,8 +1358,8 @@ def test_x(): assert suite_node.tag == "testsuite" -def test_runs_twice(testdir, run_and_parse): - f = testdir.makepyfile( +def test_runs_twice(pytester: Pytester, run_and_parse) -> None: + f = pytester.makepyfile( """ def test_pass(): pass @@ -1299,10 +1372,12 @@ def test_pass(): assert first == second -def test_runs_twice_xdist(testdir, run_and_parse): +def test_runs_twice_xdist( + pytester: Pytester, monkeypatch: MonkeyPatch, run_and_parse +) -> None: pytest.importorskip("xdist") - testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") - f = testdir.makepyfile( + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + f = pytester.makepyfile( """ def test_pass(): pass @@ -1315,9 +1390,9 @@ def test_pass(): assert first == second -def test_fancy_items_regression(testdir, run_and_parse): +def test_fancy_items_regression(pytester: Pytester, run_and_parse) -> None: # issue 1259 - testdir.makeconftest( + pytester.makeconftest( """ import pytest class FunItem(pytest.Item): @@ -1341,7 +1416,7 @@ def pytest_collect_file(path, parent): """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_pass(): pass @@ -1368,8 +1443,8 @@ def test_pass(): @parametrize_families -def test_global_properties(testdir, xunit_family) -> None: - path = testdir.tmpdir.join("test_global_properties.xml") +def test_global_properties(pytester: Pytester, xunit_family) -> None: + path = pytester.path.joinpath("test_global_properties.xml") log = LogXML(str(path), None, family=xunit_family) class Report(BaseReport): @@ -1402,9 +1477,9 @@ class Report(BaseReport): assert actual == expected -def test_url_property(testdir) -> None: +def test_url_property(pytester: Pytester) -> None: test_url = "http://www.github.com/pytest-dev" - path = testdir.tmpdir.join("test_url_property.xml") + path = pytester.path.joinpath("test_url_property.xml") log = LogXML(str(path), None) class Report(BaseReport): @@ -1429,8 +1504,10 @@ class Report(BaseReport): @parametrize_families -def test_record_testsuite_property(testdir, run_and_parse, xunit_family): - testdir.makepyfile( +def test_record_testsuite_property( + pytester: Pytester, run_and_parse, xunit_family +) -> None: + pytester.makepyfile( """ def test_func1(record_testsuite_property): record_testsuite_property("stats", "all good") @@ -1449,27 +1526,27 @@ def test_func2(record_testsuite_property): p2_node.assert_attr(name="stats", value="10") -def test_record_testsuite_property_junit_disabled(testdir): - testdir.makepyfile( +def test_record_testsuite_property_junit_disabled(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_func1(record_testsuite_property): record_testsuite_property("stats", "all good") """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 @pytest.mark.parametrize("junit", [True, False]) -def test_record_testsuite_property_type_checking(testdir, junit): - testdir.makepyfile( +def test_record_testsuite_property_type_checking(pytester: Pytester, junit) -> None: + pytester.makepyfile( """ def test_func1(record_testsuite_property): record_testsuite_property(1, 2) """ ) args = ("--junitxml=tests.xml",) if junit else () - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) assert result.ret == 1 result.stdout.fnmatch_lines( ["*TypeError: name parameter needs to be a string, but int given"] @@ -1478,9 +1555,11 @@ def test_func1(record_testsuite_property): @pytest.mark.parametrize("suite_name", ["my_suite", ""]) @parametrize_families -def test_set_suite_name(testdir, suite_name, run_and_parse, xunit_family): +def test_set_suite_name( + pytester: Pytester, suite_name, run_and_parse, xunit_family +) -> None: if suite_name: - testdir.makeini( + pytester.makeini( """ [pytest] junit_suite_name={suite_name} @@ -1492,7 +1571,7 @@ def test_set_suite_name(testdir, suite_name, run_and_parse, xunit_family): expected = suite_name else: expected = "pytest" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1506,8 +1585,8 @@ def test_func(): node.assert_attr(name=expected) -def test_escaped_skipreason_issue3533(testdir, run_and_parse): - testdir.makepyfile( +def test_escaped_skipreason_issue3533(pytester: Pytester, run_and_parse) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skip(reason='1 <> 2') @@ -1524,9 +1603,9 @@ def test_skip(): @parametrize_families def test_logging_passing_tests_disabled_does_not_log_test_output( - testdir, run_and_parse, xunit_family -): - testdir.makeini( + pytester: Pytester, run_and_parse, xunit_family +) -> None: + pytester.makeini( """ [pytest] junit_log_passing_tests=False @@ -1536,7 +1615,7 @@ def test_logging_passing_tests_disabled_does_not_log_test_output( family=xunit_family ) ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging @@ -1558,9 +1637,9 @@ def test_func(): @parametrize_families @pytest.mark.parametrize("junit_logging", ["no", "system-out", "system-err"]) def test_logging_passing_tests_disabled_logs_output_for_failing_test_issue5430( - testdir, junit_logging, run_and_parse, xunit_family -): - testdir.makeini( + pytester: Pytester, junit_logging, run_and_parse, xunit_family +) -> None: + pytester.makeini( """ [pytest] junit_log_passing_tests=False @@ -1569,7 +1648,7 @@ def test_logging_passing_tests_disabled_logs_output_for_failing_test_issue5430( family=xunit_family ) ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest import logging diff --git a/testing/test_link_resolve.py b/testing/test_link_resolve.py index 7eaf4124796..60a86ada36e 100644 --- a/testing/test_link_resolve.py +++ b/testing/test_link_resolve.py @@ -3,15 +3,14 @@ import sys import textwrap from contextlib import contextmanager +from pathlib import Path from string import ascii_lowercase -import py.path - -from _pytest import pytester +from _pytest.pytester import Pytester @contextmanager -def subst_path_windows(filename): +def subst_path_windows(filepath: Path): for c in ascii_lowercase[7:]: # Create a subst drive from H-Z. c += ":" if not os.path.exists(c): @@ -20,14 +19,14 @@ def subst_path_windows(filename): else: raise AssertionError("Unable to find suitable drive letter for subst.") - directory = filename.dirpath() - basename = filename.basename + directory = filepath.parent + basename = filepath.name args = ["subst", drive, str(directory)] subprocess.check_call(args) assert os.path.exists(drive) try: - filename = py.path.local(drive) / basename + filename = Path(drive, os.sep, basename) yield filename finally: args = ["subst", "/D", drive] @@ -35,9 +34,9 @@ def subst_path_windows(filename): @contextmanager -def subst_path_linux(filename): - directory = filename.dirpath() - basename = filename.basename +def subst_path_linux(filepath: Path): + directory = filepath.parent + basename = filepath.name target = directory / ".." / "sub2" os.symlink(str(directory), str(target), target_is_directory=True) @@ -49,11 +48,11 @@ def subst_path_linux(filename): pass -def test_link_resolve(testdir: pytester.Testdir) -> None: +def test_link_resolve(pytester: Pytester) -> None: """See: https://github.com/pytest-dev/pytest/issues/5965.""" - sub1 = testdir.mkpydir("sub1") - p = sub1.join("test_foo.py") - p.write( + sub1 = pytester.mkpydir("sub1") + p = sub1.joinpath("test_foo.py") + p.write_text( textwrap.dedent( """ import pytest @@ -68,7 +67,7 @@ def test_foo(): subst = subst_path_windows with subst(p) as subst_p: - result = testdir.runpytest(str(subst_p), "-v") + result = pytester.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 diff --git a/testing/test_main.py b/testing/test_main.py index f45607abc30..2ed111895cd 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -10,7 +10,6 @@ from _pytest.main import resolve_collection_argument from _pytest.main import validate_basetemp from _pytest.pytester import Pytester -from _pytest.pytester import Testdir @pytest.mark.parametrize( @@ -21,9 +20,9 @@ pytest.param((False, SystemExit)), ), ) -def test_wrap_session_notify_exception(ret_exc, testdir): +def test_wrap_session_notify_exception(ret_exc, pytester: Pytester) -> None: returncode, exc = ret_exc - c1 = testdir.makeconftest( + c1 = pytester.makeconftest( """ import pytest @@ -38,7 +37,7 @@ def pytest_internalerror(excrepr, excinfo): returncode=returncode, exc=exc.__name__ ) ) - result = testdir.runpytest() + result = pytester.runpytest() if returncode: assert result.ret == returncode else: @@ -65,9 +64,9 @@ def pytest_internalerror(excrepr, excinfo): @pytest.mark.parametrize("returncode", (None, 42)) def test_wrap_session_exit_sessionfinish( - returncode: Optional[int], testdir: Testdir + returncode: Optional[int], pytester: Pytester ) -> None: - testdir.makeconftest( + pytester.makeconftest( """ import pytest def pytest_sessionfinish(): @@ -76,7 +75,7 @@ def pytest_sessionfinish(): returncode=returncode ) ) - result = testdir.runpytest() + result = pytester.runpytest() if returncode: assert result.ret == returncode else: @@ -101,8 +100,8 @@ def test_validate_basetemp_fails(tmp_path, basetemp, monkeypatch): validate_basetemp(basetemp) -def test_validate_basetemp_integration(testdir): - result = testdir.runpytest("--basetemp=.") +def test_validate_basetemp_integration(pytester: Pytester) -> None: + result = pytester.runpytest("--basetemp=.") result.stderr.fnmatch_lines("*basetemp must not be*") @@ -203,14 +202,14 @@ def test_absolute_paths_are_resolved_correctly(self, invocation_path: Path) -> N ) == (Path(os.path.abspath("src")), []) -def test_module_full_path_without_drive(testdir): +def test_module_full_path_without_drive(pytester: Pytester) -> None: """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( + pytester.makepyfile( **{ "project/conftest.py": """ import pytest @@ -220,7 +219,7 @@ def fix(): return 1 } ) - testdir.makepyfile( + pytester.makepyfile( **{ "project/tests/dummy_test.py": """ def test(fix): @@ -228,12 +227,12 @@ def test(fix): """ } ) - fn = testdir.tmpdir.join("project/tests/dummy_test.py") - assert fn.isfile() + fn = pytester.path.joinpath("project/tests/dummy_test.py") + assert fn.is_file() drive, path = os.path.splitdrive(str(fn)) - result = testdir.runpytest(path, "-v") + result = pytester.runpytest(path, "-v") result.stdout.fnmatch_lines( [ os.path.join("project", "tests", "dummy_test.py") + "::test PASSED *", diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index a124009c401..c33337b67b3 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -315,7 +315,7 @@ def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: shlex.quote(sys.executable) ) ) - # alternative would be extended Testdir.{run(),_run(),popen()} to be able + # alternative would be extended Pytester.{run(),_run(),popen()} to be able # to handle a keyword argument env that replaces os.environ in popen or # extends the copy, advantage: could not forget to restore monkeypatch.setenv("_ARGCOMPLETE", "1") diff --git a/testing/test_pytester.py b/testing/test_pytester.py index f2e8dd5a36a..a9ba1a046f1 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -2,26 +2,26 @@ import subprocess import sys import time +from pathlib import Path +from types import ModuleType from typing import List -import py.path - -import _pytest.pytester as pytester +import _pytest.pytester as pytester_mod import pytest from _pytest.config import ExitCode from _pytest.config import PytestPluginManager +from _pytest.monkeypatch import MonkeyPatch 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 -def test_make_hook_recorder(testdir) -> None: - item = testdir.getitem("def test_func(): pass") - recorder = testdir.make_hook_recorder(item.config.pluginmanager) +def test_make_hook_recorder(pytester: Pytester) -> None: + item = pytester.getitem("def test_func(): pass") + recorder = pytester.make_hook_recorder(item.config.pluginmanager) assert not recorder.getfailures() # (The silly condition is to fool mypy that the code below this is reachable) @@ -35,11 +35,11 @@ class rep: skipped = False when = "call" - recorder.hook.pytest_runtest_logreport(report=rep) + recorder.hook.pytest_runtest_logreport(report=rep) # type: ignore[attr-defined] failures = recorder.getfailures() - assert failures == [rep] + assert failures == [rep] # type: ignore[comparison-overlap] failures = recorder.getfailures() - assert failures == [rep] + assert failures == [rep] # type: ignore[comparison-overlap] class rep2: excinfo = None @@ -50,14 +50,14 @@ class rep2: rep2.passed = False rep2.skipped = True - recorder.hook.pytest_runtest_logreport(report=rep2) + recorder.hook.pytest_runtest_logreport(report=rep2) # type: ignore[attr-defined] - modcol = testdir.getmodulecol("") + modcol = pytester.getmodulecol("") rep3 = modcol.config.hook.pytest_make_collect_report(collector=modcol) rep3.passed = False rep3.failed = True rep3.skipped = False - recorder.hook.pytest_collectreport(report=rep3) + recorder.hook.pytest_collectreport(report=rep3) # type: ignore[attr-defined] passed, skipped, failed = recorder.listoutcomes() assert not passed and skipped and failed @@ -68,55 +68,55 @@ class rep2: assert numfailed == 1 assert len(recorder.getfailedcollections()) == 1 - recorder.unregister() + recorder.unregister() # type: ignore[attr-defined] recorder.clear() - recorder.hook.pytest_runtest_logreport(report=rep3) + recorder.hook.pytest_runtest_logreport(report=rep3) # type: ignore[attr-defined] pytest.raises(ValueError, recorder.getfailures) -def test_parseconfig(testdir) -> None: - config1 = testdir.parseconfig() - config2 = testdir.parseconfig() +def test_parseconfig(pytester: Pytester) -> None: + config1 = pytester.parseconfig() + config2 = pytester.parseconfig() assert config2 is not config1 -def test_testdir_runs_with_plugin(testdir) -> None: - testdir.makepyfile( +def test_pytester_runs_with_plugin(pytester: Pytester) -> None: + pytester.makepyfile( """ pytest_plugins = "pytester" - def test_hello(testdir): + def test_hello(pytester): assert 1 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=1) -def test_testdir_with_doctest(testdir): - """Check that testdir can be used within doctests. +def test_pytester_with_doctest(pytester: Pytester): + """Check that pytester can be used within doctests. It used to use `request.function`, which is `None` with doctests.""" - testdir.makepyfile( + pytester.makepyfile( **{ "sub/t-doctest.py": """ ''' >>> import os - >>> testdir = getfixture("testdir") - >>> str(testdir.makepyfile("content")).replace(os.sep, '/') + >>> pytester = getfixture("pytester") + >>> str(pytester.makepyfile("content")).replace(os.sep, '/') '.../basetemp/sub.t-doctest0/sub.py' ''' """, "sub/__init__.py": "", } ) - result = testdir.runpytest( + result = pytester.runpytest( "-p", "pytester", "--doctest-modules", "sub/t-doctest.py" ) assert result.ret == 0 -def test_runresult_assertion_on_xfail(testdir) -> None: - testdir.makepyfile( +def test_runresult_assertion_on_xfail(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -127,13 +127,13 @@ def test_potato(): assert False """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(xfailed=1) assert result.ret == 0 -def test_runresult_assertion_on_xpassed(testdir) -> None: - testdir.makepyfile( +def test_runresult_assertion_on_xpassed(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -144,13 +144,13 @@ def test_potato(): assert True """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(xpassed=1) assert result.ret == 0 -def test_xpassed_with_strict_is_considered_a_failure(testdir) -> None: - testdir.makepyfile( +def test_xpassed_with_strict_is_considered_a_failure(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -161,7 +161,7 @@ def test_potato(): assert True """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(failed=1) assert result.ret != 0 @@ -202,28 +202,28 @@ def test_hookrecorder_basic(holder) -> None: assert call._name == "pytest_xyz_noarg" -def test_makepyfile_unicode(testdir) -> None: - testdir.makepyfile(chr(0xFFFD)) +def test_makepyfile_unicode(pytester: Pytester) -> None: + pytester.makepyfile(chr(0xFFFD)) -def test_makepyfile_utf8(testdir) -> None: +def test_makepyfile_utf8(pytester: Pytester) -> None: """Ensure makepyfile accepts utf-8 bytes as input (#2738)""" utf8_contents = """ def setup_function(function): mixed_encoding = 'São Paulo' """.encode() - p = testdir.makepyfile(utf8_contents) - assert "mixed_encoding = 'São Paulo'".encode() in p.read("rb") + p = pytester.makepyfile(utf8_contents) + assert "mixed_encoding = 'São Paulo'".encode() in p.read_bytes() class TestInlineRunModulesCleanup: - def test_inline_run_test_module_not_cleaned_up(self, testdir) -> None: - test_mod = testdir.makepyfile("def test_foo(): assert True") - result = testdir.inline_run(str(test_mod)) + def test_inline_run_test_module_not_cleaned_up(self, pytester: Pytester) -> None: + test_mod = pytester.makepyfile("def test_foo(): assert True") + result = pytester.inline_run(str(test_mod)) assert result.ret == ExitCode.OK # rewrite module, now test should fail if module was re-imported - test_mod.write("def test_foo(): assert False") - result2 = testdir.inline_run(str(test_mod)) + test_mod.write_text("def test_foo(): assert False") + result2 = pytester.inline_run(str(test_mod)) assert result2.ret == ExitCode.TESTS_FAILED def spy_factory(self): @@ -243,20 +243,20 @@ def restore(self): return SysModulesSnapshotSpy def test_inline_run_taking_and_restoring_a_sys_modules_snapshot( - self, testdir, monkeypatch + self, pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: spy_factory = self.spy_factory() - monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory) - testdir.syspathinsert() + monkeypatch.setattr(pytester_mod, "SysModulesSnapshot", spy_factory) + pytester.syspathinsert() original = dict(sys.modules) - testdir.makepyfile(import1="# you son of a silly person") - testdir.makepyfile(import2="# my hovercraft is full of eels") - test_mod = testdir.makepyfile( + pytester.makepyfile(import1="# you son of a silly person") + pytester.makepyfile(import2="# my hovercraft is full of eels") + test_mod = pytester.makepyfile( """ import import1 def test_foo(): import import2""" ) - testdir.inline_run(str(test_mod)) + pytester.inline_run(str(test_mod)) assert len(spy_factory.instances) == 1 spy = spy_factory.instances[0] assert spy._spy_restore_count == 1 @@ -264,55 +264,57 @@ def test_foo(): import import2""" assert all(sys.modules[x] is original[x] for x in sys.modules) def test_inline_run_sys_modules_snapshot_restore_preserving_modules( - self, testdir, monkeypatch + self, pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: spy_factory = self.spy_factory() - monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory) - test_mod = testdir.makepyfile("def test_foo(): pass") - testdir.inline_run(str(test_mod)) + monkeypatch.setattr(pytester_mod, "SysModulesSnapshot", spy_factory) + test_mod = pytester.makepyfile("def test_foo(): pass") + pytester.inline_run(str(test_mod)) spy = spy_factory.instances[0] assert not spy._spy_preserve("black_knight") assert spy._spy_preserve("zope") assert spy._spy_preserve("zope.interface") assert spy._spy_preserve("zopelicious") - def test_external_test_module_imports_not_cleaned_up(self, testdir) -> None: - testdir.syspathinsert() - testdir.makepyfile(imported="data = 'you son of a silly person'") + def test_external_test_module_imports_not_cleaned_up( + self, pytester: Pytester + ) -> None: + pytester.syspathinsert() + pytester.makepyfile(imported="data = 'you son of a silly person'") import imported - test_mod = testdir.makepyfile( + test_mod = pytester.makepyfile( """ def test_foo(): import imported imported.data = 42""" ) - testdir.inline_run(str(test_mod)) + pytester.inline_run(str(test_mod)) assert imported.data == 42 -def test_assert_outcomes_after_pytest_error(testdir) -> None: - testdir.makepyfile("def test_foo(): assert True") +def test_assert_outcomes_after_pytest_error(pytester: Pytester) -> None: + pytester.makepyfile("def test_foo(): assert True") - result = testdir.runpytest("--unexpected-argument") + result = pytester.runpytest("--unexpected-argument") with pytest.raises(ValueError, match="Pytest terminal summary report not found"): result.assert_outcomes(passed=0) -def test_cwd_snapshot(testdir: Testdir) -> None: - tmpdir = testdir.tmpdir - foo = tmpdir.ensure("foo", dir=1) - bar = tmpdir.ensure("bar", dir=1) - foo.chdir() +def test_cwd_snapshot(pytester: Pytester) -> None: + foo = pytester.mkdir("foo") + bar = pytester.mkdir("bar") + os.chdir(foo) snapshot = CwdSnapshot() - bar.chdir() - assert py.path.local() == bar + os.chdir(bar) + assert Path().absolute() == bar snapshot.restore() - assert py.path.local() == foo + assert Path().absolute() == foo class TestSysModulesSnapshot: key = "my-test-module" + mod = ModuleType("something") def test_remove_added(self) -> None: original = dict(sys.modules) @@ -323,9 +325,9 @@ def test_remove_added(self) -> None: snapshot.restore() assert sys.modules == original - def test_add_removed(self, monkeypatch) -> None: + def test_add_removed(self, monkeypatch: MonkeyPatch) -> None: assert self.key not in sys.modules - monkeypatch.setitem(sys.modules, self.key, "something") + monkeypatch.setitem(sys.modules, self.key, self.mod) assert self.key in sys.modules original = dict(sys.modules) snapshot = SysModulesSnapshot() @@ -334,9 +336,9 @@ def test_add_removed(self, monkeypatch) -> None: snapshot.restore() assert sys.modules == original - def test_restore_reloaded(self, monkeypatch) -> None: + def test_restore_reloaded(self, monkeypatch: MonkeyPatch) -> None: assert self.key not in sys.modules - monkeypatch.setitem(sys.modules, self.key, "something") + monkeypatch.setitem(sys.modules, self.key, self.mod) assert self.key in sys.modules original = dict(sys.modules) snapshot = SysModulesSnapshot() @@ -344,11 +346,12 @@ def test_restore_reloaded(self, monkeypatch) -> None: snapshot.restore() assert sys.modules == original - def test_preserve_modules(self, monkeypatch) -> None: + def test_preserve_modules(self, monkeypatch: MonkeyPatch) -> None: key = [self.key + str(i) for i in range(3)] assert not any(k in sys.modules for k in key) for i, k in enumerate(key): - monkeypatch.setitem(sys.modules, k, "something" + str(i)) + mod = ModuleType("something" + str(i)) + monkeypatch.setitem(sys.modules, k, mod) original = dict(sys.modules) def preserve(name): @@ -361,7 +364,7 @@ def preserve(name): snapshot.restore() assert sys.modules == original - def test_preserve_container(self, monkeypatch) -> None: + def test_preserve_container(self, monkeypatch: MonkeyPatch) -> None: original = dict(sys.modules) assert self.key not in original replacement = dict(sys.modules) @@ -381,7 +384,7 @@ class TestSysPathsSnapshot: def path(n: int) -> str: return "my-dirty-little-secret-" + str(n) - def test_restore(self, monkeypatch, path_type) -> None: + def test_restore(self, monkeypatch: MonkeyPatch, path_type) -> None: other_path_type = self.other_path[path_type] for i in range(10): assert self.path(i) not in getattr(sys, path_type) @@ -404,7 +407,7 @@ def test_restore(self, monkeypatch, path_type) -> None: assert getattr(sys, path_type) == original assert getattr(sys, other_path_type) == original_other - def test_preserve_container(self, monkeypatch, path_type) -> None: + def test_preserve_container(self, monkeypatch: MonkeyPatch, path_type) -> None: other_path_type = self.other_path[path_type] original_data = list(getattr(sys, path_type)) original_other = getattr(sys, other_path_type) @@ -419,49 +422,47 @@ def test_preserve_container(self, monkeypatch, path_type) -> None: assert getattr(sys, other_path_type) == original_other_data -def test_testdir_subprocess(testdir) -> None: - testfile = testdir.makepyfile("def test_one(): pass") - assert testdir.runpytest_subprocess(testfile).ret == 0 +def test_pytester_subprocess(pytester: Pytester) -> None: + testfile = pytester.makepyfile("def test_one(): pass") + assert pytester.runpytest_subprocess(testfile).ret == 0 -def test_testdir_subprocess_via_runpytest_arg(testdir) -> None: - testfile = testdir.makepyfile( +def test_pytester_subprocess_via_runpytest_arg(pytester: Pytester) -> None: + testfile = pytester.makepyfile( """ - def test_testdir_subprocess(testdir): + def test_pytester_subprocess(pytester): import os - testfile = testdir.makepyfile( + testfile = pytester.makepyfile( \""" import os def test_one(): assert {} != os.getpid() \""".format(os.getpid()) ) - assert testdir.runpytest(testfile).ret == 0 + assert pytester.runpytest(testfile).ret == 0 """ ) - result = testdir.runpytest_subprocess( - "-p", "pytester", "--runpytest", "subprocess", testfile - ) + result = pytester.runpytest("-p", "pytester", "--runpytest", "subprocess", testfile) assert result.ret == 0 -def test_unicode_args(testdir) -> None: - result = testdir.runpytest("-k", "אבג") +def test_unicode_args(pytester: Pytester) -> None: + result = pytester.runpytest("-k", "אבג") assert result.ret == ExitCode.NO_TESTS_COLLECTED -def test_testdir_run_no_timeout(testdir) -> None: - testfile = testdir.makepyfile("def test_no_timeout(): pass") - assert testdir.runpytest_subprocess(testfile).ret == ExitCode.OK +def test_pytester_run_no_timeout(pytester: Pytester) -> None: + testfile = pytester.makepyfile("def test_no_timeout(): pass") + assert pytester.runpytest_subprocess(testfile).ret == ExitCode.OK -def test_testdir_run_with_timeout(testdir) -> None: - testfile = testdir.makepyfile("def test_no_timeout(): pass") +def test_pytester_run_with_timeout(pytester: Pytester) -> None: + testfile = pytester.makepyfile("def test_no_timeout(): pass") timeout = 120 start = time.time() - result = testdir.runpytest_subprocess(testfile, timeout=timeout) + result = pytester.runpytest_subprocess(testfile, timeout=timeout) end = time.time() duration = end - start @@ -469,16 +470,16 @@ def test_testdir_run_with_timeout(testdir) -> None: assert duration < timeout -def test_testdir_run_timeout_expires(testdir) -> None: - testfile = testdir.makepyfile( +def test_pytester_run_timeout_expires(pytester: Pytester) -> None: + testfile = pytester.makepyfile( """ import time def test_timeout(): time.sleep(10)""" ) - with pytest.raises(testdir.TimeoutExpired): - testdir.runpytest_subprocess(testfile, timeout=1) + with pytest.raises(pytester.TimeoutExpired): + pytester.runpytest_subprocess(testfile, timeout=1) def test_linematcher_with_nonlist() -> None: @@ -533,7 +534,7 @@ def test_linematcher_match_failure() -> None: ] -def test_linematcher_consecutive(): +def test_linematcher_consecutive() -> None: lm = LineMatcher(["1", "", "2"]) with pytest.raises(pytest.fail.Exception) as excinfo: lm.fnmatch_lines(["1", "2"], consecutive=True) @@ -554,7 +555,7 @@ def test_linematcher_consecutive(): @pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"]) -def test_linematcher_no_matching(function) -> None: +def test_linematcher_no_matching(function: str) -> None: if function == "no_fnmatch_line": good_pattern = "*.py OK*" bad_pattern = "*X.py OK*" @@ -615,7 +616,7 @@ def test_linematcher_string_api() -> None: assert str(lm) == "foo\nbar" -def test_pytester_addopts_before_testdir(request, monkeypatch) -> None: +def test_pytester_addopts_before_testdir(request, monkeypatch: MonkeyPatch) -> None: orig = os.environ.get("PYTEST_ADDOPTS", None) monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused") testdir = request.getfixturevalue("testdir") @@ -626,9 +627,9 @@ def test_pytester_addopts_before_testdir(request, monkeypatch) -> None: assert os.environ.get("PYTEST_ADDOPTS") == orig -def test_run_stdin(testdir) -> None: - with pytest.raises(testdir.TimeoutExpired): - testdir.run( +def test_run_stdin(pytester: Pytester) -> None: + with pytest.raises(pytester.TimeoutExpired): + pytester.run( sys.executable, "-c", "import sys, time; time.sleep(1); print(sys.stdin.read())", @@ -636,8 +637,8 @@ def test_run_stdin(testdir) -> None: timeout=0.1, ) - with pytest.raises(testdir.TimeoutExpired): - result = testdir.run( + with pytest.raises(pytester.TimeoutExpired): + result = pytester.run( sys.executable, "-c", "import sys, time; time.sleep(1); print(sys.stdin.read())", @@ -645,7 +646,7 @@ def test_run_stdin(testdir) -> None: timeout=0.1, ) - result = testdir.run( + result = pytester.run( sys.executable, "-c", "import sys; print(sys.stdin.read())", @@ -656,8 +657,8 @@ def test_run_stdin(testdir) -> None: assert result.ret == 0 -def test_popen_stdin_pipe(testdir) -> None: - proc = testdir.popen( +def test_popen_stdin_pipe(pytester: Pytester) -> None: + proc = pytester.popen( [sys.executable, "-c", "import sys; print(sys.stdin.read())"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -670,8 +671,8 @@ def test_popen_stdin_pipe(testdir) -> None: assert proc.returncode == 0 -def test_popen_stdin_bytes(testdir) -> None: - proc = testdir.popen( +def test_popen_stdin_bytes(pytester: Pytester) -> None: + proc = pytester.popen( [sys.executable, "-c", "import sys; print(sys.stdin.read())"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -683,18 +684,18 @@ def test_popen_stdin_bytes(testdir) -> None: assert proc.returncode == 0 -def test_popen_default_stdin_stderr_and_stdin_None(testdir) -> None: +def test_popen_default_stdin_stderr_and_stdin_None(pytester: Pytester) -> 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 make it not hang when run with "-s". - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( ''' import sys - def test_inner(testdir): - p1 = testdir.makepyfile( + def test_inner(pytester): + p1 = pytester.makepyfile( """ import sys print(sys.stdin.read()) # empty @@ -702,14 +703,14 @@ def test_inner(testdir): sys.stderr.write('stderr') """ ) - proc = testdir.popen([sys.executable, str(p1)], stdin=None) + proc = pytester.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)) + result = pytester.runpytest("-p", "pytester", str(p1)) assert result.ret == 0 @@ -740,22 +741,22 @@ def test_run_result_repr() -> None: errlines = ["some", "nasty", "errors", "happened"] # known exit code - r = pytester.RunResult(1, outlines, errlines, duration=0.5) + r = pytester_mod.RunResult(1, outlines, errlines, duration=0.5) assert ( repr(r) == "" ) # unknown exit code: just the number - r = pytester.RunResult(99, outlines, errlines, duration=0.5) + r = pytester_mod.RunResult(99, outlines, errlines, duration=0.5) assert ( repr(r) == "" ) -def test_testdir_outcomes_with_multiple_errors(testdir): - p1 = testdir.makepyfile( +def test_pytester_outcomes_with_multiple_errors(pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest @@ -770,7 +771,7 @@ def test_error2(bad_fixture): pass """ ) - result = testdir.runpytest(str(p1)) + result = pytester.runpytest(str(p1)) result.assert_outcomes(errors=2) assert result.parseoutcomes() == {"errors": 2} @@ -784,7 +785,7 @@ def test_parse_summary_line_always_plural(): "======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====", "done.", ] - assert pytester.RunResult.parse_summary_nouns(lines) == { + assert pytester_mod.RunResult.parse_summary_nouns(lines) == { "errors": 1, "failed": 1, "passed": 1, @@ -797,7 +798,7 @@ def test_parse_summary_line_always_plural(): "======= 1 failed, 1 passed, 2 warnings, 2 errors in 0.13s ====", "done.", ] - assert pytester.RunResult.parse_summary_nouns(lines) == { + assert pytester_mod.RunResult.parse_summary_nouns(lines) == { "errors": 2, "failed": 1, "passed": 1, @@ -805,10 +806,10 @@ def test_parse_summary_line_always_plural(): } -def test_makefile_joins_absolute_path(testdir: Testdir) -> None: - absfile = testdir.tmpdir / "absfile" - p1 = testdir.makepyfile(**{str(absfile): ""}) - assert str(p1) == str(testdir.tmpdir / "absfile.py") +def test_makefile_joins_absolute_path(pytester: Pytester) -> None: + absfile = pytester.path / "absfile" + p1 = pytester.makepyfile(**{str(absfile): ""}) + assert str(p1) == str(pytester.path / "absfile.py") def test_testtmproot(testdir): diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 8b00cb826ac..feee09286c2 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -4,11 +4,12 @@ import pytest from _pytest.config import ExitCode -from _pytest.pytester import Testdir +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Pytester -def test_simple_unittest(testdir): - testpath = testdir.makepyfile( +def test_simple_unittest(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -18,13 +19,13 @@ def test_failing(self): self.assertEqual('foo', 'bar') """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) assert reprec.matchreport("testpassing").passed assert reprec.matchreport("test_failing").failed -def test_runTest_method(testdir): - testdir.makepyfile( +def test_runTest_method(pytester: Pytester) -> None: + pytester.makepyfile( """ import unittest class MyTestCaseWithRunTest(unittest.TestCase): @@ -37,7 +38,7 @@ def test_something(self): pass """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( """ *MyTestCaseWithRunTest::runTest* @@ -47,8 +48,8 @@ def test_something(self): ) -def test_isclasscheck_issue53(testdir): - testpath = testdir.makepyfile( +def test_isclasscheck_issue53(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class _E(object): @@ -57,12 +58,12 @@ def __getattr__(self, tag): E = _E() """ ) - result = testdir.runpytest(testpath) + result = pytester.runpytest(testpath) assert result.ret == ExitCode.NO_TESTS_COLLECTED -def test_setup(testdir): - testpath = testdir.makepyfile( +def test_setup(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -78,14 +79,14 @@ def teardown_method(self, method): """ ) - reprec = testdir.inline_run("-s", testpath) + reprec = pytester.inline_run("-s", testpath) assert reprec.matchreport("test_both", when="call").passed rep = reprec.matchreport("test_both", when="teardown") assert rep.failed and "42" in str(rep.longrepr) -def test_setUpModule(testdir): - testpath = testdir.makepyfile( +def test_setUpModule(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ values = [] @@ -102,12 +103,12 @@ def test_world(): assert values == [1] """ ) - result = testdir.runpytest(testpath) + result = pytester.runpytest(testpath) result.stdout.fnmatch_lines(["*2 passed*"]) -def test_setUpModule_failing_no_teardown(testdir): - testpath = testdir.makepyfile( +def test_setUpModule_failing_no_teardown(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ values = [] @@ -121,14 +122,14 @@ def test_hello(): pass """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) reprec.assertoutcome(passed=0, failed=1) call = reprec.getcalls("pytest_runtest_setup")[0] assert not call.item.module.values -def test_new_instances(testdir): - testpath = testdir.makepyfile( +def test_new_instances(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -138,13 +139,13 @@ def test_func2(self): assert not hasattr(self, 'x') """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) reprec.assertoutcome(passed=2) -def test_function_item_obj_is_instance(testdir): +def test_function_item_obj_is_instance(pytester: Pytester) -> None: """item.obj should be a bound method on unittest.TestCase function items (#5390).""" - testdir.makeconftest( + pytester.makeconftest( """ def pytest_runtest_makereport(item, call): if call.when == 'call': @@ -152,7 +153,7 @@ def pytest_runtest_makereport(item, call): assert isinstance(item.obj.__self__, class_) """ ) - testdir.makepyfile( + pytester.makepyfile( """ import unittest @@ -161,12 +162,12 @@ def test_foo(self): pass """ ) - result = testdir.runpytest_inprocess() + result = pytester.runpytest_inprocess() result.stdout.fnmatch_lines(["* 1 passed in*"]) -def test_teardown(testdir): - testpath = testdir.makepyfile( +def test_teardown(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -180,14 +181,14 @@ def test_check(self): self.assertEqual(MyTestCase.values, [None]) """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) passed, skipped, failed = reprec.countoutcomes() assert failed == 0, failed assert passed == 2 assert passed + skipped + failed == 2 -def test_teardown_issue1649(testdir): +def test_teardown_issue1649(pytester: Pytester) -> None: """ Are TestCase objects cleaned up? Often unittest TestCase objects set attributes that are large and expensive during setUp. @@ -195,7 +196,7 @@ def test_teardown_issue1649(testdir): The TestCase will not be cleaned up if the test fails, because it would then exist in the stackframe. """ - testpath = testdir.makepyfile( + testpath = pytester.makepyfile( """ import unittest class TestCaseObjectsShouldBeCleanedUp(unittest.TestCase): @@ -206,14 +207,14 @@ def test_demo(self): """ ) - testdir.inline_run("-s", testpath) + pytester.inline_run("-s", testpath) gc.collect() for obj in gc.get_objects(): assert type(obj).__name__ != "TestCaseObjectsShouldBeCleanedUp" -def test_unittest_skip_issue148(testdir): - testpath = testdir.makepyfile( +def test_unittest_skip_issue148(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest @@ -229,12 +230,12 @@ def tearDownClass(self): xxx """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) reprec.assertoutcome(skipped=1) -def test_method_and_teardown_failing_reporting(testdir): - testdir.makepyfile( +def test_method_and_teardown_failing_reporting(pytester: Pytester) -> None: + pytester.makepyfile( """ import unittest class TC(unittest.TestCase): @@ -244,7 +245,7 @@ def test_method(self): assert False, "down2" """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") assert result.ret == 1 result.stdout.fnmatch_lines( [ @@ -257,8 +258,8 @@ def test_method(self): ) -def test_setup_failure_is_shown(testdir): - testdir.makepyfile( +def test_setup_failure_is_shown(pytester: Pytester) -> None: + pytester.makepyfile( """ import unittest import pytest @@ -270,14 +271,14 @@ def test_method(self): xyz """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") assert result.ret == 1 result.stdout.fnmatch_lines(["*setUp*", "*assert 0*down1*", "*1 failed*"]) result.stdout.no_fnmatch_line("*never42*") -def test_setup_setUpClass(testdir): - testpath = testdir.makepyfile( +def test_setup_setUpClass(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest import pytest @@ -297,12 +298,12 @@ def test_teareddown(): assert MyTestCase.x == 0 """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) reprec.assertoutcome(passed=3) -def test_setup_class(testdir): - testpath = testdir.makepyfile( +def test_setup_class(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest import pytest @@ -320,13 +321,13 @@ def test_teareddown(): assert MyTestCase.x == 0 """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) reprec.assertoutcome(passed=3) @pytest.mark.parametrize("type", ["Error", "Failure"]) -def test_testcase_adderrorandfailure_defers(testdir, type): - testdir.makepyfile( +def test_testcase_adderrorandfailure_defers(pytester: Pytester, type: str) -> None: + pytester.makepyfile( """ from unittest import TestCase import pytest @@ -344,13 +345,13 @@ def test_hello(self): """ % (type, type) ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.no_fnmatch_line("*should not raise*") @pytest.mark.parametrize("type", ["Error", "Failure"]) -def test_testcase_custom_exception_info(testdir, type): - testdir.makepyfile( +def test_testcase_custom_exception_info(pytester: Pytester, type: str) -> None: + pytester.makepyfile( """ from unittest import TestCase import py, pytest @@ -375,7 +376,7 @@ def test_hello(self): """ % locals() ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "NOTE: Incompatible Exception Representation*", @@ -385,8 +386,10 @@ def test_hello(self): ) -def test_testcase_totally_incompatible_exception_info(testdir): - (item,) = testdir.getitems( +def test_testcase_totally_incompatible_exception_info(pytester: Pytester) -> None: + import _pytest.unittest + + (item,) = pytester.getitems( """ from unittest import TestCase class MyTestCase(TestCase): @@ -394,13 +397,15 @@ def test_hello(self): pass """ ) - item.addError(None, 42) - excinfo = item._excinfo.pop(0) - assert "ERROR: Unknown Incompatible" in str(excinfo.getrepr()) + assert isinstance(item, _pytest.unittest.TestCaseFunction) + item.addError(None, 42) # type: ignore[arg-type] + excinfo = item._excinfo + assert excinfo is not None + assert "ERROR: Unknown Incompatible" in str(excinfo.pop(0).getrepr()) -def test_module_level_pytestmark(testdir): - testpath = testdir.makepyfile( +def test_module_level_pytestmark(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest import pytest @@ -410,7 +415,7 @@ def test_func1(self): assert 0 """ ) - reprec = testdir.inline_run(testpath, "-s") + reprec = pytester.inline_run(testpath, "-s") reprec.assertoutcome(skipped=1) @@ -421,8 +426,8 @@ def setup_class(cls): # https://twistedmatrix.com/trac/ticket/9227 cls.ignore_unclosed_socket_warning = ("-W", "always") - def test_trial_testcase_runtest_not_collected(self, testdir): - testdir.makepyfile( + def test_trial_testcase_runtest_not_collected(self, pytester: Pytester) -> None: + pytester.makepyfile( """ from twisted.trial.unittest import TestCase @@ -431,9 +436,9 @@ def test_hello(self): pass """ ) - reprec = testdir.inline_run(*self.ignore_unclosed_socket_warning) + reprec = pytester.inline_run(*self.ignore_unclosed_socket_warning) reprec.assertoutcome(passed=1) - testdir.makepyfile( + pytester.makepyfile( """ from twisted.trial.unittest import TestCase @@ -442,11 +447,11 @@ def runTest(self): pass """ ) - reprec = testdir.inline_run(*self.ignore_unclosed_socket_warning) + reprec = pytester.inline_run(*self.ignore_unclosed_socket_warning) reprec.assertoutcome(passed=1) - def test_trial_exceptions_with_skips(self, testdir): - testdir.makepyfile( + def test_trial_exceptions_with_skips(self, pytester: Pytester) -> None: + pytester.makepyfile( """ from twisted.trial import unittest import pytest @@ -480,7 +485,7 @@ def test_method(self): pass """ ) - result = testdir.runpytest("-rxs", *self.ignore_unclosed_socket_warning) + result = pytester.runpytest("-rxs", *self.ignore_unclosed_socket_warning) result.stdout.fnmatch_lines_random( [ "*XFAIL*test_trial_todo*", @@ -495,8 +500,8 @@ def test_method(self): ) assert result.ret == 1 - def test_trial_error(self, testdir): - testdir.makepyfile( + def test_trial_error(self, pytester: Pytester) -> None: + pytester.makepyfile( """ from twisted.trial.unittest import TestCase from twisted.internet.defer import Deferred @@ -533,7 +538,7 @@ def f(_): # will crash both at test time and at teardown """ ) - result = testdir.runpytest("-vv", "-oconsole_output_style=classic") + result = pytester.runpytest("-vv", "-oconsole_output_style=classic") result.stdout.fnmatch_lines( [ "test_trial_error.py::TC::test_four FAILED", @@ -557,8 +562,8 @@ def f(_): ] ) - def test_trial_pdb(self, testdir): - p = testdir.makepyfile( + def test_trial_pdb(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ from twisted.trial import unittest import pytest @@ -567,12 +572,12 @@ def test_hello(self): assert 0, "hellopdb" """ ) - child = testdir.spawn_pytest(p) + child = pytester.spawn_pytest(str(p)) child.expect("hellopdb") child.sendeof() - def test_trial_testcase_skip_property(self, testdir): - testpath = testdir.makepyfile( + def test_trial_testcase_skip_property(self, pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ from twisted.trial import unittest class MyTestCase(unittest.TestCase): @@ -581,11 +586,11 @@ def test_func(self): pass """ ) - reprec = testdir.inline_run(testpath, "-s") + reprec = pytester.inline_run(testpath, "-s") reprec.assertoutcome(skipped=1) - def test_trial_testfunction_skip_property(self, testdir): - testpath = testdir.makepyfile( + def test_trial_testfunction_skip_property(self, pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ from twisted.trial import unittest class MyTestCase(unittest.TestCase): @@ -594,11 +599,11 @@ def test_func(self): test_func.skip = 'dont run' """ ) - reprec = testdir.inline_run(testpath, "-s") + reprec = pytester.inline_run(testpath, "-s") reprec.assertoutcome(skipped=1) - def test_trial_testcase_todo_property(self, testdir): - testpath = testdir.makepyfile( + def test_trial_testcase_todo_property(self, pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ from twisted.trial import unittest class MyTestCase(unittest.TestCase): @@ -607,11 +612,11 @@ def test_func(self): assert 0 """ ) - reprec = testdir.inline_run(testpath, "-s") + reprec = pytester.inline_run(testpath, "-s") reprec.assertoutcome(skipped=1) - def test_trial_testfunction_todo_property(self, testdir): - testpath = testdir.makepyfile( + def test_trial_testfunction_todo_property(self, pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ from twisted.trial import unittest class MyTestCase(unittest.TestCase): @@ -620,15 +625,15 @@ def test_func(self): test_func.todo = 'dont run' """ ) - reprec = testdir.inline_run( + reprec = pytester.inline_run( testpath, "-s", *self.ignore_unclosed_socket_warning ) reprec.assertoutcome(skipped=1) -def test_djangolike_testcase(testdir): +def test_djangolike_testcase(pytester: Pytester) -> None: # contributed from Morten Breekevold - testdir.makepyfile( + pytester.makepyfile( """ from unittest import TestCase, main @@ -671,7 +676,7 @@ def _post_teardown(self): print("_post_teardown()") """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") assert result.ret == 0 result.stdout.fnmatch_lines( [ @@ -684,8 +689,8 @@ def _post_teardown(self): ) -def test_unittest_not_shown_in_traceback(testdir): - testdir.makepyfile( +def test_unittest_not_shown_in_traceback(pytester: Pytester) -> None: + pytester.makepyfile( """ import unittest class t(unittest.TestCase): @@ -694,12 +699,12 @@ def test_hello(self): self.assertEqual(x, 4) """ ) - res = testdir.runpytest() + res = pytester.runpytest() res.stdout.no_fnmatch_line("*failUnlessEqual*") -def test_unorderable_types(testdir): - testdir.makepyfile( +def test_unorderable_types(pytester: Pytester) -> None: + pytester.makepyfile( """ import unittest class TestJoinEmpty(unittest.TestCase): @@ -713,13 +718,13 @@ class Test(unittest.TestCase): TestFoo = make_test() """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.no_fnmatch_line("*TypeError*") assert result.ret == ExitCode.NO_TESTS_COLLECTED -def test_unittest_typerror_traceback(testdir): - testdir.makepyfile( +def test_unittest_typerror_traceback(pytester: Pytester) -> None: + pytester.makepyfile( """ import unittest class TestJoinEmpty(unittest.TestCase): @@ -727,14 +732,16 @@ def test_hello(self, arg1): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert "TypeError" in result.stdout.str() assert result.ret == 1 @pytest.mark.parametrize("runner", ["pytest", "unittest"]) -def test_unittest_expected_failure_for_failing_test_is_xfail(testdir, runner): - script = testdir.makepyfile( +def test_unittest_expected_failure_for_failing_test_is_xfail( + pytester: Pytester, runner +) -> None: + script = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -746,19 +753,21 @@ def test_failing_test_is_xfail(self): """ ) if runner == "pytest": - result = testdir.runpytest("-rxX") + result = pytester.runpytest("-rxX") result.stdout.fnmatch_lines( ["*XFAIL*MyTestCase*test_failing_test_is_xfail*", "*1 xfailed*"] ) else: - result = testdir.runpython(script) + result = pytester.runpython(script) result.stderr.fnmatch_lines(["*1 test in*", "*OK*(expected failures=1)*"]) assert result.ret == 0 @pytest.mark.parametrize("runner", ["pytest", "unittest"]) -def test_unittest_expected_failure_for_passing_test_is_fail(testdir, runner): - script = testdir.makepyfile( +def test_unittest_expected_failure_for_passing_test_is_fail( + pytester: Pytester, runner +) -> None: + script = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -771,20 +780,20 @@ def test_passing_test_is_fail(self): ) if runner == "pytest": - result = testdir.runpytest("-rxX") + result = pytester.runpytest("-rxX") result.stdout.fnmatch_lines( ["*MyTestCase*test_passing_test_is_fail*", "*1 failed*"] ) else: - result = testdir.runpython(script) + result = pytester.runpython(script) result.stderr.fnmatch_lines(["*1 test in*", "*(unexpected successes=1)*"]) assert result.ret == 1 @pytest.mark.parametrize("stmt", ["return", "yield"]) -def test_unittest_setup_interaction(testdir: Testdir, stmt: str) -> None: - testdir.makepyfile( +def test_unittest_setup_interaction(pytester: Pytester, stmt: str) -> None: + pytester.makepyfile( """ import unittest import pytest @@ -811,12 +820,12 @@ def test_classattr(self): stmt=stmt ) ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) -def test_non_unittest_no_setupclass_support(testdir): - testpath = testdir.makepyfile( +def test_non_unittest_no_setupclass_support(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ class TestFoo(object): x = 0 @@ -837,12 +846,12 @@ def test_not_teareddown(): """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) reprec.assertoutcome(passed=2) -def test_no_teardown_if_setupclass_failed(testdir): - testpath = testdir.makepyfile( +def test_no_teardown_if_setupclass_failed(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest @@ -865,13 +874,13 @@ def test_notTornDown(): assert MyTestCase.x == 1 """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) reprec.assertoutcome(passed=1, failed=1) -def test_cleanup_functions(testdir): +def test_cleanup_functions(pytester: Pytester) -> None: """Ensure functions added with addCleanup are always called after each test ends (#6947)""" - testdir.makepyfile( + pytester.makepyfile( """ import unittest @@ -890,7 +899,7 @@ def test_func_3_check_cleanups(self): assert cleanups == ["test_func_1", "test_func_2"] """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( [ "*::test_func_1 PASSED *", @@ -900,8 +909,8 @@ def test_func_3_check_cleanups(self): ) -def test_issue333_result_clearing(testdir): - testdir.makeconftest( +def test_issue333_result_clearing(pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @pytest.hookimpl(hookwrapper=True) @@ -910,7 +919,7 @@ def pytest_runtest_call(item): assert 0 """ ) - testdir.makepyfile( + pytester.makepyfile( """ import unittest class TestIt(unittest.TestCase): @@ -919,12 +928,12 @@ def test_func(self): """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(failed=1) -def test_unittest_raise_skip_issue748(testdir): - testdir.makepyfile( +def test_unittest_raise_skip_issue748(pytester: Pytester) -> None: + pytester.makepyfile( test_foo=""" import unittest @@ -933,7 +942,7 @@ def test_one(self): raise unittest.SkipTest('skipping due to reasons') """ ) - result = testdir.runpytest("-v", "-rs") + result = pytester.runpytest("-v", "-rs") result.stdout.fnmatch_lines( """ *SKIP*[1]*test_foo.py*skipping due to reasons* @@ -942,8 +951,8 @@ def test_one(self): ) -def test_unittest_skip_issue1169(testdir): - testdir.makepyfile( +def test_unittest_skip_issue1169(pytester: Pytester) -> None: + pytester.makepyfile( test_foo=""" import unittest @@ -953,7 +962,7 @@ def test_skip(self): self.fail() """ ) - result = testdir.runpytest("-v", "-rs") + result = pytester.runpytest("-v", "-rs") result.stdout.fnmatch_lines( """ *SKIP*[1]*skipping due to reasons* @@ -962,8 +971,8 @@ def test_skip(self): ) -def test_class_method_containing_test_issue1558(testdir): - testdir.makepyfile( +def test_class_method_containing_test_issue1558(pytester: Pytester) -> None: + pytester.makepyfile( test_foo=""" import unittest @@ -975,16 +984,16 @@ def test_should_not_run(self): test_should_not_run.__test__ = False """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @pytest.mark.parametrize("base", ["builtins.object", "unittest.TestCase"]) -def test_usefixtures_marker_on_unittest(base, testdir): +def test_usefixtures_marker_on_unittest(base, pytester: Pytester) -> None: """#3498""" module = base.rsplit(".", 1)[0] pytest.importorskip(module) - testdir.makepyfile( + pytester.makepyfile( conftest=""" import pytest @@ -1013,7 +1022,7 @@ def pytest_collection_modifyitems(items): """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest import {module} @@ -1038,16 +1047,16 @@ def test_two(self): ) ) - result = testdir.runpytest("-s") + result = pytester.runpytest("-s") result.assert_outcomes(passed=2) -def test_testcase_handles_init_exceptions(testdir): +def test_testcase_handles_init_exceptions(pytester: Pytester) -> None: """ Regression test to make sure exceptions in the __init__ method are bubbled up correctly. See https://github.com/pytest-dev/pytest/issues/3788 """ - testdir.makepyfile( + pytester.makepyfile( """ from unittest import TestCase import pytest @@ -1058,14 +1067,14 @@ def test_hello(self): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert "should raise this exception" in result.stdout.str() result.stdout.no_fnmatch_line("*ERROR at teardown of MyTestCase.test_hello*") -def test_error_message_with_parametrized_fixtures(testdir): - testdir.copy_example("unittest/test_parametrized_fixture_error_message.py") - result = testdir.runpytest() +def test_error_message_with_parametrized_fixtures(pytester: Pytester) -> None: + pytester.copy_example("unittest/test_parametrized_fixture_error_message.py") + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*test_two does not support fixtures*", @@ -1083,15 +1092,17 @@ def test_error_message_with_parametrized_fixtures(testdir): ("test_setup_skip_module.py", "1 error"), ], ) -def test_setup_inheritance_skipping(testdir, test_name, expected_outcome): +def test_setup_inheritance_skipping( + pytester: Pytester, test_name, expected_outcome +) -> None: """Issue #4700""" - testdir.copy_example(f"unittest/{test_name}") - result = testdir.runpytest() + pytester.copy_example(f"unittest/{test_name}") + result = pytester.runpytest() result.stdout.fnmatch_lines([f"* {expected_outcome} in *"]) -def test_BdbQuit(testdir): - testdir.makepyfile( +def test_BdbQuit(pytester: Pytester) -> None: + pytester.makepyfile( test_foo=""" import unittest @@ -1104,12 +1115,12 @@ def test_should_not_run(self): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(failed=1, passed=1) -def test_exit_outcome(testdir): - testdir.makepyfile( +def test_exit_outcome(pytester: Pytester) -> None: + pytester.makepyfile( test_foo=""" import pytest import unittest @@ -1122,11 +1133,11 @@ def test_should_not_run(self): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*Exit: pytest_exit called*", "*= no tests ran in *"]) -def test_trace(testdir, monkeypatch): +def test_trace(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: calls = [] def check_call(*args, **kwargs): @@ -1141,7 +1152,7 @@ def runcall(*args, **kwargs): monkeypatch.setattr("_pytest.debugging.pytestPDB._init_pdb", check_call) - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import unittest @@ -1150,12 +1161,12 @@ def test(self): self.assertEqual('foo', 'foo') """ ) - result = testdir.runpytest("--trace", str(p1)) + result = pytester.runpytest("--trace", str(p1)) assert len(calls) == 2 assert result.ret == 0 -def test_pdb_teardown_called(testdir, monkeypatch) -> None: +def test_pdb_teardown_called(pytester: Pytester, monkeypatch: 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 @@ -1166,7 +1177,7 @@ def test_pdb_teardown_called(testdir, monkeypatch) -> None: pytest, "test_pdb_teardown_called_teardowns", teardowns, raising=False ) - testdir.makepyfile( + pytester.makepyfile( """ import unittest import pytest @@ -1182,7 +1193,7 @@ def test_2(self): pass """ ) - result = testdir.runpytest_inprocess("--pdb") + result = pytester.runpytest_inprocess("--pdb") result.stdout.fnmatch_lines("* 2 passed in *") assert teardowns == [ "test_pdb_teardown_called.MyTestCase.test_1", @@ -1191,12 +1202,14 @@ def test_2(self): @pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"]) -def test_pdb_teardown_skipped(testdir, monkeypatch, mark: str) -> None: +def test_pdb_teardown_skipped( + pytester: Pytester, monkeypatch: MonkeyPatch, mark: str +) -> None: """With --pdb, setUp and tearDown should not be called for skipped tests.""" tracked: List[str] = [] monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False) - testdir.makepyfile( + pytester.makepyfile( """ import unittest import pytest @@ -1217,29 +1230,29 @@ def test_1(self): mark=mark ) ) - result = testdir.runpytest_inprocess("--pdb") + result = pytester.runpytest_inprocess("--pdb") result.stdout.fnmatch_lines("* 1 skipped in *") assert tracked == [] -def test_async_support(testdir): +def test_async_support(pytester: Pytester) -> None: pytest.importorskip("unittest.async_case") - testdir.copy_example("unittest/test_unittest_asyncio.py") - reprec = testdir.inline_run() + pytester.copy_example("unittest/test_unittest_asyncio.py") + reprec = pytester.inline_run() reprec.assertoutcome(failed=1, passed=2) -def test_asynctest_support(testdir): +def test_asynctest_support(pytester: Pytester) -> None: """Check asynctest support (#7110)""" pytest.importorskip("asynctest") - testdir.copy_example("unittest/test_unittest_asynctest.py") - reprec = testdir.inline_run() + pytester.copy_example("unittest/test_unittest_asynctest.py") + reprec = pytester.inline_run() reprec.assertoutcome(failed=1, passed=2) -def test_plain_unittest_does_not_support_async(testdir): +def test_plain_unittest_does_not_support_async(pytester: Pytester) -> None: """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 @@ -1247,8 +1260,8 @@ def test_plain_unittest_does_not_support_async(testdir): 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() + pytester.copy_example("unittest/test_unittest_plain_async.py") + result = pytester.runpytest_subprocess() 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 @@ -1265,8 +1278,8 @@ def test_plain_unittest_does_not_support_async(testdir): @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( +def test_do_class_cleanups_on_success(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -1284,7 +1297,7 @@ def test_cleanup_called_exactly_once(): assert MyTestCase.values == [1] """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) passed, skipped, failed = reprec.countoutcomes() assert failed == 0 assert passed == 3 @@ -1293,8 +1306,8 @@ def test_cleanup_called_exactly_once(): @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( +def test_do_class_cleanups_on_setupclass_failure(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -1311,7 +1324,7 @@ def test_cleanup_called_exactly_once(): assert MyTestCase.values == [1] """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) passed, skipped, failed = reprec.countoutcomes() assert failed == 1 assert passed == 1 @@ -1320,8 +1333,8 @@ def test_cleanup_called_exactly_once(): @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( +def test_do_class_cleanups_on_teardownclass_failure(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -1342,13 +1355,13 @@ def test_cleanup_called_exactly_once(): assert MyTestCase.values == [1] """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) passed, skipped, failed = reprec.countoutcomes() assert passed == 3 -def test_do_cleanups_on_success(testdir): - testpath = testdir.makepyfile( +def test_do_cleanups_on_success(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -1365,14 +1378,14 @@ def test_cleanup_called_the_right_number_of_times(): assert MyTestCase.values == [1, 1] """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.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( +def test_do_cleanups_on_setup_failure(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -1390,14 +1403,14 @@ def test_cleanup_called_the_right_number_of_times(): assert MyTestCase.values == [1, 1] """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.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( +def test_do_cleanups_on_teardown_failure(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class MyTestCase(unittest.TestCase): @@ -1416,7 +1429,7 @@ def test_cleanup_called_the_right_number_of_times(): assert MyTestCase.values == [1, 1] """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) passed, skipped, failed = reprec.countoutcomes() assert failed == 2 assert passed == 1 From 196b173c8a86833b96f90128a6cc9928e17b6c23 Mon Sep 17 00:00:00 2001 From: antonblr Date: Fri, 18 Dec 2020 12:36:20 -0800 Subject: [PATCH 0342/2846] address comments --- .github/workflows/main.yml | 3 +- src/_pytest/nodes.py | 2 +- src/_pytest/python.py | 2 +- testing/test_debugging.py | 5 +- testing/test_junitxml.py | 175 +++++++++++++++++++++---------------- testing/test_pytester.py | 27 +++--- 6 files changed, 121 insertions(+), 93 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1b6e85fd87e..beb50178528 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -123,8 +123,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python }} - # https://github.com/actions/setup-python/issues/171 - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install dependencies diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 27c76a04302..1b3ec5571b1 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -528,7 +528,7 @@ def gethookproxy(self, fspath: "os.PathLike[str]"): warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.gethookproxy(fspath) - def isinitpath(self, path: "os.PathLike[str]") -> bool: + def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool: warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.isinitpath(path) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 018e368f45e..3ff04455fbf 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -660,7 +660,7 @@ def gethookproxy(self, fspath: "os.PathLike[str]"): warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.gethookproxy(fspath) - def isinitpath(self, path: "os.PathLike[str]") -> bool: + def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool: warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.isinitpath(path) diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 8218b7a0ede..e1b57299d25 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -21,10 +21,11 @@ @pytest.fixture(autouse=True) -def pdb_env(request, monkeypatch: MonkeyPatch): +def pdb_env(request): if "pytester" in request.fixturenames: # Disable pdb++ with inner tests. - monkeypatch.setenv("PDBPP_HIJACK_PDB", "0") + pytester = request.getfixturevalue("pytester") + pytester._monkeypatch.setenv("PDBPP_HIJACK_PDB", "0") def runpdb_and_get_report(pytester: Pytester, source: str): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 3e445dcefc5..1c76351eafc 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -7,7 +7,6 @@ from typing import Optional from typing import Tuple from typing import TYPE_CHECKING -from typing import TypeVar from typing import Union from xml.dom import minidom @@ -24,8 +23,6 @@ from _pytest.reports import TestReport from _pytest.store import Store -T = TypeVar("T") - @pytest.fixture(scope="session") def schema() -> xmlschema.XMLSchema: @@ -35,29 +32,34 @@ def schema() -> xmlschema.XMLSchema: return xmlschema.XMLSchema(f) -@pytest.fixture -def run_and_parse(pytester: Pytester, schema: xmlschema.XMLSchema) -> T: - """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. - """ +class RunAndParse: + def __init__(self, pytester: Pytester, schema: xmlschema.XMLSchema) -> None: + self.pytester = pytester + self.schema = schema - def run( - *args: Union[str, "os.PathLike[str]"], family: Optional[str] = "xunit1", + def __call__( + self, *args: Union[str, "os.PathLike[str]"], family: Optional[str] = "xunit1" ) -> Tuple[RunResult, "DomNode"]: if family: args = ("-o", "junit_family=" + family) + args - xml_path = pytester.path.joinpath("junit.xml") - result = pytester.runpytest("--junitxml=%s" % xml_path, *args) + xml_path = self.pytester.path.joinpath("junit.xml") + result = self.pytester.runpytest("--junitxml=%s" % xml_path, *args) if family == "xunit2": with xml_path.open() as f: - schema.validate(f) + self.schema.validate(f) xmldoc = minidom.parse(str(xml_path)) return result, DomNode(xmldoc) - return cast(T, run) + +@pytest.fixture +def run_and_parse(pytester: Pytester, schema: xmlschema.XMLSchema) -> RunAndParse: + """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. + """ + return RunAndParse(pytester, schema) def assert_attr(node, **kwargs): @@ -140,7 +142,7 @@ def next_sibling(self): class TestPython: @parametrize_families def test_summing_simple( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -166,7 +168,7 @@ def test_xpass(): @parametrize_families def test_summing_simple_with_errors( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -195,7 +197,7 @@ def test_xpass(): @parametrize_families def test_hostname_in_xml( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -209,7 +211,7 @@ def test_pass(): @parametrize_families def test_timestamp_in_xml( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -224,7 +226,7 @@ def test_pass(): assert start_time <= timestamp < datetime.now() def test_timing_function( - self, pytester: Pytester, run_and_parse, mock_timing + self, pytester: Pytester, run_and_parse: RunAndParse, mock_timing ) -> None: pytester.makepyfile( """ @@ -248,8 +250,8 @@ def test_junit_duration_report( self, pytester: Pytester, monkeypatch: MonkeyPatch, - duration_report, - run_and_parse, + duration_report: str, + run_and_parse: RunAndParse, ) -> None: # mock LogXML.node_reporter so it always sets a known duration to each test report object @@ -279,7 +281,9 @@ def test_foo(): assert val == 1.0 @parametrize_families - def test_setup_error(self, pytester: Pytester, run_and_parse, xunit_family) -> None: + def test_setup_error( + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str + ) -> None: pytester.makepyfile( """ import pytest @@ -303,7 +307,7 @@ def test_function(arg): @parametrize_families def test_teardown_error( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -328,7 +332,7 @@ def test_function(arg): @parametrize_families def test_call_failure_teardown_error( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -359,7 +363,7 @@ def test_function(arg): @parametrize_families def test_skip_contains_name_reason( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -379,7 +383,7 @@ def test_skip(): @parametrize_families def test_mark_skip_contains_name_reason( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -402,7 +406,7 @@ def test_skip(): @parametrize_families def test_mark_skipif_contains_name_reason( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -426,7 +430,7 @@ def test_skip(): @parametrize_families def test_mark_skip_doesnt_capture_output( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -443,7 +447,7 @@ def test_skip(): @parametrize_families def test_classname_instance( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -463,7 +467,7 @@ def test_method(self): @parametrize_families def test_classname_nested_dir( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: p = pytester.mkdir("sub").joinpath("test_hello.py") p.write_text("def test_func(): 0/0") @@ -476,7 +480,7 @@ def test_classname_nested_dir( @parametrize_families def test_internal_error( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makeconftest("def pytest_runtest_protocol(): 0 / 0") pytester.makepyfile("def test_function(): pass") @@ -495,7 +499,11 @@ def test_internal_error( ) @parametrize_families def test_failure_function( - self, pytester: Pytester, junit_logging, run_and_parse, xunit_family + self, + pytester: Pytester, + junit_logging, + run_and_parse: RunAndParse, + xunit_family, ) -> None: pytester.makepyfile( """ @@ -559,7 +567,7 @@ def test_fail(): @parametrize_families def test_failure_verbose_message( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -576,7 +584,7 @@ def test_fail(): @parametrize_families def test_failure_escape( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -606,7 +614,7 @@ def test_func(arg1): @parametrize_families def test_junit_prefixing( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -630,7 +638,7 @@ def test_hello(self): @parametrize_families def test_xfailure_function( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -650,7 +658,7 @@ def test_xfail(): @parametrize_families def test_xfailure_marker( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -673,7 +681,7 @@ def test_xfail(): "junit_logging", ["no", "log", "system-out", "system-err", "out-err", "all"] ) def test_xfail_captures_output_once( - self, pytester: Pytester, junit_logging, run_and_parse + self, pytester: Pytester, junit_logging: str, run_and_parse: RunAndParse ) -> None: pytester.makepyfile( """ @@ -702,7 +710,7 @@ def test_fail(): @parametrize_families def test_xfailure_xpass( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -721,7 +729,7 @@ def test_xpass(): @parametrize_families def test_xfailure_xpass_strict( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -742,7 +750,7 @@ def test_xpass(): @parametrize_families def test_collect_error( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile("syntax error") result, dom = run_and_parse(family=xunit_family) @@ -754,7 +762,7 @@ def test_collect_error( fnode.assert_attr(message="collection failure") assert "SyntaxError" in fnode.toxml() - def test_unicode(self, pytester: Pytester, run_and_parse) -> None: + def test_unicode(self, pytester: Pytester, run_and_parse: RunAndParse) -> None: value = "hx\xc4\x85\xc4\x87\n" pytester.makepyfile( """\ @@ -771,7 +779,9 @@ def test_hello(): fnode = tnode.find_first_by_tag("failure") assert "hx" in fnode.toxml() - def test_assertion_binchars(self, pytester: Pytester, run_and_parse) -> None: + def test_assertion_binchars( + self, pytester: Pytester, run_and_parse: RunAndParse + ) -> None: """This test did fail when the escaping wasn't strict.""" pytester.makepyfile( """ @@ -788,7 +798,7 @@ def test_str_compare(): @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) def test_pass_captures_stdout( - self, pytester: Pytester, run_and_parse, junit_logging + self, pytester: Pytester, run_and_parse: RunAndParse, junit_logging: str ) -> None: pytester.makepyfile( """ @@ -811,7 +821,7 @@ def test_pass(): @pytest.mark.parametrize("junit_logging", ["no", "system-err"]) def test_pass_captures_stderr( - self, pytester: Pytester, run_and_parse, junit_logging + self, pytester: Pytester, run_and_parse: RunAndParse, junit_logging: str ) -> None: pytester.makepyfile( """ @@ -835,7 +845,7 @@ def test_pass(): @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) def test_setup_error_captures_stdout( - self, pytester: Pytester, run_and_parse, junit_logging + self, pytester: Pytester, run_and_parse: RunAndParse, junit_logging: str ) -> None: pytester.makepyfile( """ @@ -864,7 +874,7 @@ def test_function(arg): @pytest.mark.parametrize("junit_logging", ["no", "system-err"]) def test_setup_error_captures_stderr( - self, pytester: Pytester, run_and_parse, junit_logging + self, pytester: Pytester, run_and_parse: RunAndParse, junit_logging: str ) -> None: pytester.makepyfile( """ @@ -894,7 +904,7 @@ def test_function(arg): @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) def test_avoid_double_stdout( - self, pytester: Pytester, run_and_parse, junit_logging + self, pytester: Pytester, run_and_parse: RunAndParse, junit_logging: str ) -> None: pytester.makepyfile( """ @@ -964,7 +974,7 @@ def getini(self, name): class TestNonPython: @parametrize_families def test_summing_simple( - self, pytester: Pytester, run_and_parse, xunit_family + self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makeconftest( """ @@ -992,7 +1002,7 @@ def repr_failure(self, excinfo): @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) -def test_nullbyte(pytester: Pytester, junit_logging) -> None: +def test_nullbyte(pytester: Pytester, junit_logging: str) -> None: # A null byte can not occur in XML (see section 2.2 of the spec) pytester.makepyfile( """ @@ -1014,7 +1024,7 @@ def test_print_nullbyte(): @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) -def test_nullbyte_replace(pytester: Pytester, junit_logging) -> None: +def test_nullbyte_replace(pytester: Pytester, junit_logging: str) -> None: # Check if the null byte gets replaced pytester.makepyfile( """ @@ -1114,7 +1124,9 @@ def test_logxml_check_isdir(pytester: Pytester) -> None: result.stderr.fnmatch_lines(["*--junitxml must be a filename*"]) -def test_escaped_parametrized_names_xml(pytester: Pytester, run_and_parse) -> None: +def test_escaped_parametrized_names_xml( + pytester: Pytester, run_and_parse: RunAndParse +) -> None: pytester.makepyfile( """\ import pytest @@ -1130,7 +1142,7 @@ def test_func(char): def test_double_colon_split_function_issue469( - pytester: Pytester, run_and_parse + pytester: Pytester, run_and_parse: RunAndParse ) -> None: pytester.makepyfile( """ @@ -1147,7 +1159,9 @@ def test_func(param): node.assert_attr(name="test_func[double::colon]") -def test_double_colon_split_method_issue469(pytester: Pytester, run_and_parse) -> None: +def test_double_colon_split_method_issue469( + pytester: Pytester, run_and_parse: RunAndParse +) -> None: pytester.makepyfile( """ import pytest @@ -1194,7 +1208,7 @@ class Report(BaseReport): log.pytest_sessionfinish() -def test_record_property(pytester: Pytester, run_and_parse) -> None: +def test_record_property(pytester: Pytester, run_and_parse: RunAndParse) -> None: pytester.makepyfile( """ import pytest @@ -1216,7 +1230,9 @@ def test_record(record_property, other): result.stdout.fnmatch_lines(["*= 1 passed in *"]) -def test_record_property_same_name(pytester: Pytester, run_and_parse) -> None: +def test_record_property_same_name( + pytester: Pytester, run_and_parse: RunAndParse +) -> None: pytester.makepyfile( """ def test_record_with_same_name(record_property): @@ -1234,7 +1250,9 @@ def test_record_with_same_name(record_property): @pytest.mark.parametrize("fixture_name", ["record_property", "record_xml_attribute"]) -def test_record_fixtures_without_junitxml(pytester: Pytester, fixture_name) -> None: +def test_record_fixtures_without_junitxml( + pytester: Pytester, fixture_name: str +) -> None: pytester.makepyfile( """ def test_record({fixture_name}): @@ -1248,7 +1266,7 @@ def test_record({fixture_name}): @pytest.mark.filterwarnings("default") -def test_record_attribute(pytester: Pytester, run_and_parse) -> None: +def test_record_attribute(pytester: Pytester, run_and_parse: RunAndParse) -> None: pytester.makeini( """ [pytest] @@ -1279,7 +1297,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( - pytester: Pytester, fixture_name, run_and_parse + pytester: Pytester, fixture_name: str, run_and_parse: RunAndParse ) -> None: """Ensure record_xml_attribute and record_property drop values when outside of legacy family.""" pytester.makeini( @@ -1318,7 +1336,7 @@ def test_record({fixture_name}, other): def test_random_report_log_xdist( - pytester: Pytester, monkeypatch: MonkeyPatch, run_and_parse + pytester: Pytester, monkeypatch: MonkeyPatch, run_and_parse: RunAndParse ) -> None: """`xdist` calls pytest_runtest_logreport as they are executed by the workers, with nodes from several nodes overlapping, so junitxml must cope with that @@ -1344,7 +1362,9 @@ def test_x(i): @parametrize_families -def test_root_testsuites_tag(pytester: Pytester, run_and_parse, xunit_family) -> None: +def test_root_testsuites_tag( + pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str +) -> None: pytester.makepyfile( """ def test_x(): @@ -1358,7 +1378,7 @@ def test_x(): assert suite_node.tag == "testsuite" -def test_runs_twice(pytester: Pytester, run_and_parse) -> None: +def test_runs_twice(pytester: Pytester, run_and_parse: RunAndParse) -> None: f = pytester.makepyfile( """ def test_pass(): @@ -1373,7 +1393,7 @@ def test_pass(): def test_runs_twice_xdist( - pytester: Pytester, monkeypatch: MonkeyPatch, run_and_parse + pytester: Pytester, monkeypatch: MonkeyPatch, run_and_parse: RunAndParse ) -> None: pytest.importorskip("xdist") monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") @@ -1390,7 +1410,7 @@ def test_pass(): assert first == second -def test_fancy_items_regression(pytester: Pytester, run_and_parse) -> None: +def test_fancy_items_regression(pytester: Pytester, run_and_parse: RunAndParse) -> None: # issue 1259 pytester.makeconftest( """ @@ -1443,7 +1463,7 @@ def test_pass(): @parametrize_families -def test_global_properties(pytester: Pytester, xunit_family) -> None: +def test_global_properties(pytester: Pytester, xunit_family: str) -> None: path = pytester.path.joinpath("test_global_properties.xml") log = LogXML(str(path), None, family=xunit_family) @@ -1505,7 +1525,7 @@ class Report(BaseReport): @parametrize_families def test_record_testsuite_property( - pytester: Pytester, run_and_parse, xunit_family + pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makepyfile( """ @@ -1538,7 +1558,9 @@ def test_func1(record_testsuite_property): @pytest.mark.parametrize("junit", [True, False]) -def test_record_testsuite_property_type_checking(pytester: Pytester, junit) -> None: +def test_record_testsuite_property_type_checking( + pytester: Pytester, junit: bool +) -> None: pytester.makepyfile( """ def test_func1(record_testsuite_property): @@ -1556,7 +1578,7 @@ def test_func1(record_testsuite_property): @pytest.mark.parametrize("suite_name", ["my_suite", ""]) @parametrize_families def test_set_suite_name( - pytester: Pytester, suite_name, run_and_parse, xunit_family + pytester: Pytester, suite_name: str, run_and_parse: RunAndParse, xunit_family: str ) -> None: if suite_name: pytester.makeini( @@ -1585,7 +1607,9 @@ def test_func(): node.assert_attr(name=expected) -def test_escaped_skipreason_issue3533(pytester: Pytester, run_and_parse) -> None: +def test_escaped_skipreason_issue3533( + pytester: Pytester, run_and_parse: RunAndParse +) -> None: pytester.makepyfile( """ import pytest @@ -1603,7 +1627,7 @@ def test_skip(): @parametrize_families def test_logging_passing_tests_disabled_does_not_log_test_output( - pytester: Pytester, run_and_parse, xunit_family + pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makeini( """ @@ -1637,7 +1661,10 @@ def test_func(): @parametrize_families @pytest.mark.parametrize("junit_logging", ["no", "system-out", "system-err"]) def test_logging_passing_tests_disabled_logs_output_for_failing_test_issue5430( - pytester: Pytester, junit_logging, run_and_parse, xunit_family + pytester: Pytester, + junit_logging: str, + run_and_parse: RunAndParse, + xunit_family: str, ) -> None: pytester.makeini( """ diff --git a/testing/test_pytester.py b/testing/test_pytester.py index a9ba1a046f1..57d6f4fd9eb 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -92,7 +92,7 @@ def test_hello(pytester): result.assert_outcomes(passed=1) -def test_pytester_with_doctest(pytester: Pytester): +def test_pytester_with_doctest(pytester: Pytester) -> None: """Check that pytester can be used within doctests. It used to use `request.function`, which is `None` with doctests.""" @@ -314,20 +314,19 @@ def test_cwd_snapshot(pytester: Pytester) -> None: class TestSysModulesSnapshot: key = "my-test-module" - mod = ModuleType("something") def test_remove_added(self) -> None: original = dict(sys.modules) assert self.key not in sys.modules snapshot = SysModulesSnapshot() - sys.modules[self.key] = "something" # type: ignore + sys.modules[self.key] = ModuleType("something") assert self.key in sys.modules snapshot.restore() assert sys.modules == original def test_add_removed(self, monkeypatch: MonkeyPatch) -> None: assert self.key not in sys.modules - monkeypatch.setitem(sys.modules, self.key, self.mod) + monkeypatch.setitem(sys.modules, self.key, ModuleType("something")) assert self.key in sys.modules original = dict(sys.modules) snapshot = SysModulesSnapshot() @@ -338,11 +337,11 @@ def test_add_removed(self, monkeypatch: MonkeyPatch) -> None: def test_restore_reloaded(self, monkeypatch: MonkeyPatch) -> None: assert self.key not in sys.modules - monkeypatch.setitem(sys.modules, self.key, self.mod) + monkeypatch.setitem(sys.modules, self.key, ModuleType("something")) assert self.key in sys.modules original = dict(sys.modules) snapshot = SysModulesSnapshot() - sys.modules[self.key] = "something else" # type: ignore + sys.modules[self.key] = ModuleType("something else") snapshot.restore() assert sys.modules == original @@ -358,9 +357,9 @@ def preserve(name): return name in (key[0], key[1], "some-other-key") snapshot = SysModulesSnapshot(preserve=preserve) - sys.modules[key[0]] = original[key[0]] = "something else0" # type: ignore - sys.modules[key[1]] = original[key[1]] = "something else1" # type: ignore - sys.modules[key[2]] = "something else2" # type: ignore + sys.modules[key[0]] = original[key[0]] = ModuleType("something else0") + sys.modules[key[1]] = original[key[1]] = ModuleType("something else1") + sys.modules[key[2]] = ModuleType("something else2") snapshot.restore() assert sys.modules == original @@ -368,7 +367,7 @@ def test_preserve_container(self, monkeypatch: MonkeyPatch) -> None: original = dict(sys.modules) assert self.key not in original replacement = dict(sys.modules) - replacement[self.key] = "life of brian" # type: ignore + replacement[self.key] = ModuleType("life of brian") snapshot = SysModulesSnapshot() monkeypatch.setattr(sys, "modules", replacement) snapshot.restore() @@ -442,7 +441,9 @@ def test_one(): assert pytester.runpytest(testfile).ret == 0 """ ) - result = pytester.runpytest("-p", "pytester", "--runpytest", "subprocess", testfile) + result = pytester.runpytest_inprocess( + "-p", "pytester", "--runpytest", "subprocess", testfile + ) assert result.ret == 0 @@ -777,7 +778,7 @@ def test_error2(bad_fixture): assert result.parseoutcomes() == {"errors": 2} -def test_parse_summary_line_always_plural(): +def test_parse_summary_line_always_plural() -> None: """Parsing summaries always returns plural nouns (#6505)""" lines = [ "some output 1", @@ -812,6 +813,6 @@ def test_makefile_joins_absolute_path(pytester: Pytester) -> None: assert str(p1) == str(pytester.path / "absfile.py") -def test_testtmproot(testdir): +def test_testtmproot(testdir) -> None: """Check test_tmproot is a py.path attribute for backward compatibility.""" assert testdir.test_tmproot.check(dir=1) From 73586be08fe33c483ae3c3509a05459969ba2ab9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 18 Dec 2020 20:33:39 +0200 Subject: [PATCH 0343/2846] terminal: remove unused union arm in WarningReport.fslocation --- src/_pytest/terminal.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index f5d4e1f8ddc..d3d1a4b666e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -285,15 +285,13 @@ class WarningReport: User friendly message about the warning. :ivar str|None nodeid: nodeid that generated the warning (see ``get_location``). - :ivar tuple|py.path.local fslocation: + :ivar tuple fslocation: File system location of the source of the warning (see ``get_location``). """ message = attr.ib(type=str) nodeid = attr.ib(type=Optional[str], default=None) - fslocation = attr.ib( - type=Optional[Union[Tuple[str, int], py.path.local]], default=None - ) + fslocation = attr.ib(type=Optional[Tuple[str, int]], default=None) count_towards_summary = True def get_location(self, config: Config) -> Optional[str]: @@ -301,14 +299,9 @@ def get_location(self, config: Config) -> Optional[str]: if self.nodeid: return self.nodeid if self.fslocation: - if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2: - filename, linenum = self.fslocation[:2] - relpath = bestrelpath( - config.invocation_params.dir, absolutepath(filename) - ) - return f"{relpath}:{linenum}" - else: - return str(self.fslocation) + filename, linenum = self.fslocation + relpath = bestrelpath(config.invocation_params.dir, absolutepath(filename)) + return f"{relpath}:{linenum}" return None From 2c05a7babb4fa31775730e596a908f7c1f861765 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 18 Dec 2020 20:39:33 +0200 Subject: [PATCH 0344/2846] config: let main() accept any os.PathLike instead of just py.path.local Really it ought to only take the List[str], but for backward compatibility, at least get rid of the explicit py.path.local check. --- src/_pytest/config/__init__.py | 10 ++++++---- testing/test_collection.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 0df4ffa01c1..c9a0e78bfcf 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -128,7 +128,7 @@ def filter_traceback_for_conftest_import_failure( def main( - args: Optional[Union[List[str], py.path.local]] = None, + args: Optional[Union[List[str], "os.PathLike[str]"]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, ) -> Union[int, ExitCode]: """Perform an in-process test run. @@ -295,13 +295,15 @@ def get_plugin_manager() -> "PytestPluginManager": def _prepareconfig( - args: Optional[Union[py.path.local, List[str]]] = None, + args: Optional[Union[List[str], "os.PathLike[str]"]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, ) -> "Config": if args is None: args = sys.argv[1:] - elif isinstance(args, py.path.local): - args = [str(args)] + # TODO: Remove type-ignore after next mypy release. + # https://github.com/python/typeshed/commit/076983eec45e739c68551cb6119fd7d85fd4afa9 + elif isinstance(args, os.PathLike): # type: ignore[misc] + args = [os.fspath(args)] elif not isinstance(args, list): msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})" raise TypeError(msg.format(args, type(args))) diff --git a/testing/test_collection.py b/testing/test_collection.py index 2d03fda39de..9733b4fbd47 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -277,7 +277,7 @@ def pytest_collect_file(self, path): wascalled.append(path) pytester.makefile(".abc", "xyz") - pytest.main(py.path.local(pytester.path), plugins=[Plugin()]) + pytest.main(pytester.path, plugins=[Plugin()]) assert len(wascalled) == 1 assert wascalled[0].ext == ".abc" From 042d12fae6e03f97ac25311504f6697154eff08e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:02:24 +0200 Subject: [PATCH 0345/2846] doctest: use Path instead of py.path where possible --- src/_pytest/doctest.py | 21 +++++++++++---------- testing/test_doctest.py | 17 ++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index d0b6b4c4185..24f8882579b 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -36,6 +36,7 @@ from _pytest.fixtures import FixtureRequest from _pytest.nodes import Collector from _pytest.outcomes import OutcomeException +from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import import_path from _pytest.python_api import approx from _pytest.warning_types import PytestWarning @@ -120,32 +121,32 @@ def pytest_unconfigure() -> None: def pytest_collect_file( - path: py.path.local, parent: Collector, + fspath: Path, path: py.path.local, parent: Collector, ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: config = parent.config - if path.ext == ".py": - if config.option.doctestmodules and not _is_setup_py(path): + if fspath.suffix == ".py": + if config.option.doctestmodules and not _is_setup_py(fspath): mod: DoctestModule = DoctestModule.from_parent(parent, fspath=path) return mod - elif _is_doctest(config, path, parent): + elif _is_doctest(config, fspath, parent): txt: DoctestTextfile = DoctestTextfile.from_parent(parent, fspath=path) return txt return None -def _is_setup_py(path: py.path.local) -> bool: - if path.basename != "setup.py": +def _is_setup_py(path: Path) -> bool: + if path.name != "setup.py": return False - contents = path.read_binary() + contents = path.read_bytes() return b"setuptools" in contents or b"distutils" in contents -def _is_doctest(config: Config, path: py.path.local, parent) -> bool: - if path.ext in (".txt", ".rst") and parent.session.isinitpath(path): +def _is_doctest(config: Config, path: Path, parent: Collector) -> bool: + if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path): return True globs = config.getoption("doctestglob") or ["test*.txt"] for glob in globs: - if path.check(fnmatch=glob): + if fnmatch_ex(glob, path): return True return False diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 6e3880330a9..08d0aacf68c 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,10 +1,9 @@ import inspect import textwrap +from pathlib import Path from typing import Callable from typing import Optional -import py - import pytest from _pytest.doctest import _get_checker from _pytest.doctest import _is_mocked @@ -1496,25 +1495,25 @@ def test_warning_on_unwrap_of_broken_object( assert inspect.unwrap.__module__ == "inspect" -def test_is_setup_py_not_named_setup_py(tmp_path): +def test_is_setup_py_not_named_setup_py(tmp_path: Path) -> None: 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))) + 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(f'from {mod} import setup; setup(name="foo")') +def test_is_setup_py_is_a_setup_py(tmp_path: Path, mod: str) -> None: + setup_py = tmp_path.joinpath("setup.py") + setup_py.write_text(f'from {mod} import setup; setup(name="foo")', "utf-8") assert _is_setup_py(setup_py) @pytest.mark.parametrize("mod", ("setuptools", "distutils.core")) -def test_is_setup_py_different_encoding(tmp_path, mod): +def test_is_setup_py_different_encoding(tmp_path: Path, mod: str) -> None: setup_py = tmp_path.joinpath("setup.py") contents = ( "# -*- coding: cp1252 -*-\n" 'from {} import setup; setup(name="foo", description="€")\n'.format(mod) ) setup_py.write_bytes(contents.encode("cp1252")) - assert _is_setup_py(py.path.local(str(setup_py))) + assert _is_setup_py(setup_py) From 7aa224083205adb650a7b1132e6b9e861361426e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:06:17 +0200 Subject: [PATCH 0346/2846] testing/test_nodes: fix fake session to be more accurate The type of _initialpaths is `FrozenSet[Path]`. --- testing/test_nodes.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/testing/test_nodes.py b/testing/test_nodes.py index bae31f0a39c..59d9f409eac 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import cast from typing import List from typing import Type @@ -69,23 +70,23 @@ def test(): def test__check_initialpaths_for_relpath() -> None: """Ensure that it handles dirs, and does not always use dirname.""" - cwd = py.path.local() + cwd = Path.cwd() class FakeSession1: - _initialpaths = [cwd] + _initialpaths = frozenset({cwd}) session = cast(pytest.Session, FakeSession1) - assert nodes._check_initialpaths_for_relpath(session, cwd) == "" + assert nodes._check_initialpaths_for_relpath(session, py.path.local(cwd)) == "" - sub = cwd.join("file") + sub = cwd / "file" class FakeSession2: - _initialpaths = [cwd] + _initialpaths = frozenset({cwd}) session = cast(pytest.Session, FakeSession2) - assert nodes._check_initialpaths_for_relpath(session, sub) == "file" + assert nodes._check_initialpaths_for_relpath(session, py.path.local(sub)) == "file" outside = py.path.local("/outside") assert nodes._check_initialpaths_for_relpath(session, outside) is None From 2ec372df8b987207efc4ad0f33c2f82df5c9e2e5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 22:19:51 +0200 Subject: [PATCH 0347/2846] mark: export pytest.Mark for typing purposes The type cannot be constructed directly, but is exported for use in type annotations, since it is reachable through existing public API. --- changelog/7469.deprecation.rst | 5 +++++ changelog/7469.feature.rst | 10 +++++++++ doc/en/reference.rst | 4 ++-- src/_pytest/mark/structures.py | 39 +++++++++++++++++++++++++--------- src/pytest/__init__.py | 2 ++ 5 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 changelog/7469.deprecation.rst create mode 100644 changelog/7469.feature.rst diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst new file mode 100644 index 00000000000..79419dacef2 --- /dev/null +++ b/changelog/7469.deprecation.rst @@ -0,0 +1,5 @@ +Directly constructing the following classes is now deprecated: + +- ``_pytest.mark.structures.Mark`` + +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.feature.rst b/changelog/7469.feature.rst new file mode 100644 index 00000000000..f4bc52414bc --- /dev/null +++ b/changelog/7469.feature.rst @@ -0,0 +1,10 @@ +The types of objects used in pytest's API are now exported so they may be used in type annotations. + +The newly-exported types are: + +- ``pytest.Mark`` for :class:`marks `. + +Constructing them directly is not supported; they are only meant for use in type annotations. +Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0. + +Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 8aa95ca6448..3fc62ee7279 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -239,7 +239,7 @@ For example: def test_function(): ... -Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to the collected +Will create and attach a :class:`Mark ` object to the collected :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: @@ -863,7 +863,7 @@ MarkGenerator Mark ~~~~ -.. autoclass:: _pytest.mark.structures.Mark +.. autoclass:: pytest.Mark() :members: diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 6c126cf4a29..29b9586871f 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -28,6 +28,7 @@ from ..compat import NOTSET from ..compat import NotSetType from _pytest.config import Config +from _pytest.deprecated import check_ispytest from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning @@ -200,21 +201,38 @@ def _for_parametrize( @final -@attr.s(frozen=True) +@attr.s(frozen=True, init=False, auto_attribs=True) class Mark: #: Name of the mark. - name = attr.ib(type=str) + name: str #: Positional arguments of the mark decorator. - args = attr.ib(type=Tuple[Any, ...]) + args: Tuple[Any, ...] #: Keyword arguments of the mark decorator. - kwargs = attr.ib(type=Mapping[str, Any]) + kwargs: Mapping[str, Any] #: Source Mark for ids with parametrize Marks. - _param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False) + _param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False) #: Resolved/generated ids with parametrize Marks. - _param_ids_generated = attr.ib( - type=Optional[Sequence[str]], default=None, repr=False - ) + _param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False) + + def __init__( + self, + name: str, + args: Tuple[Any, ...], + kwargs: Mapping[str, Any], + param_ids_from: Optional["Mark"] = None, + param_ids_generated: Optional[Sequence[str]] = None, + *, + _ispytest: bool = False, + ) -> None: + """:meta private:""" + check_ispytest(_ispytest) + # Weirdness to bypass frozen=True. + object.__setattr__(self, "name", name) + object.__setattr__(self, "args", args) + object.__setattr__(self, "kwargs", kwargs) + object.__setattr__(self, "_param_ids_from", param_ids_from) + object.__setattr__(self, "_param_ids_generated", param_ids_generated) def _has_param_ids(self) -> bool: return "ids" in self.kwargs or len(self.args) >= 4 @@ -243,6 +261,7 @@ def combined_with(self, other: "Mark") -> "Mark": self.args + other.args, dict(self.kwargs, **other.kwargs), param_ids_from=param_ids_from, + _ispytest=True, ) @@ -320,7 +339,7 @@ def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": :rtype: MarkDecorator """ - mark = Mark(self.name, args, kwargs) + mark = Mark(self.name, args, kwargs, _ispytest=True) return self.__class__(self.mark.combined_with(mark)) # Type ignored because the overloads overlap with an incompatible @@ -515,7 +534,7 @@ def __getattr__(self, name: str) -> MarkDecorator: 2, ) - return MarkDecorator(Mark(name, (), {})) + return MarkDecorator(Mark(name, (), {}, _ispytest=True)) MARK_GEN = MarkGenerator() diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 70177f95040..4b194e0c8e0 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -21,6 +21,7 @@ from _pytest.freeze_support import freeze_includes from _pytest.logging import LogCaptureFixture from _pytest.main import Session +from _pytest.mark import Mark from _pytest.mark import MARK_GEN as mark from _pytest.mark import param from _pytest.monkeypatch import MonkeyPatch @@ -89,6 +90,7 @@ "LogCaptureFixture", "main", "mark", + "Mark", "Module", "MonkeyPatch", "Package", From 69c302479e3f76450f29e7d2de24254d5eda6492 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 20 Dec 2020 15:11:01 +0200 Subject: [PATCH 0348/2846] mark: export pytest.MarkDecorator for typing purposes The type cannot be constructed directly, but is exported for use in type annotations, since it is reachable through existing public API. --- changelog/7469.deprecation.rst | 1 + changelog/7469.feature.rst | 1 + doc/en/reference.rst | 2 +- src/_pytest/fixtures.py | 2 +- src/_pytest/mark/structures.py | 40 +++++++++++++++++++--------------- src/pytest/__init__.py | 2 ++ 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst index 79419dacef2..6922b3bbb17 100644 --- a/changelog/7469.deprecation.rst +++ b/changelog/7469.deprecation.rst @@ -1,5 +1,6 @@ Directly constructing the following classes is now deprecated: - ``_pytest.mark.structures.Mark`` +- ``_pytest.mark.structures.MarkDecorator`` 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.feature.rst b/changelog/7469.feature.rst index f4bc52414bc..66113aa580f 100644 --- a/changelog/7469.feature.rst +++ b/changelog/7469.feature.rst @@ -3,6 +3,7 @@ The types of objects used in pytest's API are now exported so they may be used i The newly-exported types are: - ``pytest.Mark`` for :class:`marks `. +- ``pytest.MarkDecorator`` for :class:`mark decorators `. Constructing them directly is not supported; they are only meant for use in type annotations. Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 3fc62ee7279..8bd4111a1c0 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -849,7 +849,7 @@ Item MarkDecorator ~~~~~~~~~~~~~ -.. autoclass:: _pytest.mark.MarkDecorator +.. autoclass:: pytest.MarkDecorator() :members: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index c24ab7069cb..dbb039bf2a9 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -551,7 +551,7 @@ def applymarker(self, marker: Union[str, MarkDecorator]) -> None: on all function invocations. :param marker: - A :py:class:`_pytest.mark.MarkDecorator` object created by a call + A :class:`pytest.MarkDecorator` object created by a call to ``pytest.mark.NAME(...)``. """ self.node.add_marker(marker) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 29b9586871f..8bce33e685a 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -268,14 +268,14 @@ 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]) +Markable = TypeVar("Markable", bound=Union[Callable[..., object], type]) -@attr.s +@attr.s(init=False, auto_attribs=True) class MarkDecorator: """A decorator for applying a mark on test functions and classes. - MarkDecorators are created with ``pytest.mark``:: + ``MarkDecorators`` are created with ``pytest.mark``:: mark1 = pytest.mark.NAME # Simple MarkDecorator mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator @@ -286,7 +286,7 @@ class MarkDecorator: def test_function(): pass - When a MarkDecorator is called it does the following: + When a ``MarkDecorator`` is called, it does the following: 1. If called with a single class as its only positional argument and no additional keyword arguments, it attaches the mark to the class so it @@ -295,19 +295,24 @@ def test_function(): 2. If called with a single function as its only positional argument and no additional keyword arguments, it attaches the mark to the function, containing all the arguments already stored internally in the - MarkDecorator. + ``MarkDecorator``. - 3. When called in any other case, it returns a new MarkDecorator instance - with the original MarkDecorator's content updated with the arguments - passed to this call. + 3. When called in any other case, it returns a new ``MarkDecorator`` + instance with the original ``MarkDecorator``'s content updated with + the arguments passed to this call. - Note: The rules above prevent MarkDecorators from storing only a single - function or class reference as their positional argument with no + Note: The rules above prevent a ``MarkDecorator`` from storing only a + single function or class reference as its positional argument with no additional keyword or positional arguments. You can work around this by using `with_args()`. """ - mark = attr.ib(type=Mark, validator=attr.validators.instance_of(Mark)) + mark: Mark + + def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None: + """:meta private:""" + check_ispytest(_ispytest) + self.mark = mark @property def name(self) -> str: @@ -326,6 +331,7 @@ def kwargs(self) -> Mapping[str, Any]: @property def markname(self) -> str: + """:meta private:""" return self.name # for backward-compat (2.4.1 had this attr) def __repr__(self) -> str: @@ -336,17 +342,15 @@ def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": Unlike calling the MarkDecorator, with_args() can be used even if the sole argument is a callable/class. - - :rtype: MarkDecorator """ mark = Mark(self.name, args, kwargs, _ispytest=True) - return self.__class__(self.mark.combined_with(mark)) + return MarkDecorator(self.mark.combined_with(mark), _ispytest=True) # Type ignored because the overloads overlap with an incompatible # return type. Not much we can do about that. Thankfully mypy picks # the first match so it works out even if we break the rules. @overload - def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc] + def __call__(self, arg: Markable) -> Markable: # type: ignore[misc] pass @overload @@ -405,7 +409,7 @@ def store_mark(obj, mark: Mark) -> None: class _SkipMarkDecorator(MarkDecorator): @overload # type: ignore[override,misc] - def __call__(self, arg: _Markable) -> _Markable: + def __call__(self, arg: Markable) -> Markable: ... @overload @@ -423,7 +427,7 @@ def __call__( # type: ignore[override] class _XfailMarkDecorator(MarkDecorator): @overload # type: ignore[override,misc] - def __call__(self, arg: _Markable) -> _Markable: + def __call__(self, arg: Markable) -> Markable: ... @overload @@ -534,7 +538,7 @@ def __getattr__(self, name: str) -> MarkDecorator: 2, ) - return MarkDecorator(Mark(name, (), {}, _ispytest=True)) + return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True) MARK_GEN = MarkGenerator() diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 4b194e0c8e0..1d5b38ee091 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -23,6 +23,7 @@ from _pytest.main import Session from _pytest.mark import Mark from _pytest.mark import MARK_GEN as mark +from _pytest.mark import MarkDecorator from _pytest.mark import param from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Collector @@ -91,6 +92,7 @@ "main", "mark", "Mark", + "MarkDecorator", "Module", "MonkeyPatch", "Package", From 6aa4d1c7ab968aecf44ad89e568a4515bd7e5343 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 20 Dec 2020 15:36:24 +0200 Subject: [PATCH 0349/2846] mark: export pytest.MarkGenerator for typing purposes The type cannot be constructed directly, but is exported for use in type annotations, since it is reachable through existing public API. --- changelog/7469.deprecation.rst | 1 + changelog/7469.feature.rst | 1 + doc/en/reference.rst | 2 +- src/_pytest/mark/structures.py | 11 +++++++---- src/pytest/__init__.py | 2 ++ testing/test_mark.py | 4 ++-- 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst index 6922b3bbb17..6bbc8075522 100644 --- a/changelog/7469.deprecation.rst +++ b/changelog/7469.deprecation.rst @@ -2,5 +2,6 @@ Directly constructing the following classes is now deprecated: - ``_pytest.mark.structures.Mark`` - ``_pytest.mark.structures.MarkDecorator`` +- ``_pytest.mark.structures.MarkGenerator`` These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0. diff --git a/changelog/7469.feature.rst b/changelog/7469.feature.rst index 66113aa580f..81f93d1f7af 100644 --- a/changelog/7469.feature.rst +++ b/changelog/7469.feature.rst @@ -4,6 +4,7 @@ The newly-exported types are: - ``pytest.Mark`` for :class:`marks `. - ``pytest.MarkDecorator`` for :class:`mark decorators `. +- ``pytest.MarkGenerator`` for the :class:`pytest.mark ` singleton. Constructing them directly is not supported; they are only meant for use in type annotations. Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 8bd4111a1c0..c8e8dca7561 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -856,7 +856,7 @@ MarkDecorator MarkGenerator ~~~~~~~~~~~~~ -.. autoclass:: _pytest.mark.MarkGenerator +.. autoclass:: pytest.MarkGenerator() :members: diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 8bce33e685a..ae6920735a2 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -488,9 +488,6 @@ def test_function(): applies a 'slowtest' :class:`Mark` on ``test_function``. """ - _config: Optional[Config] = None - _markers: Set[str] = set() - # See TYPE_CHECKING above. if TYPE_CHECKING: skip: _SkipMarkDecorator @@ -500,7 +497,13 @@ def test_function(): usefixtures: _UsefixturesMarkDecorator filterwarnings: _FilterwarningsMarkDecorator + def __init__(self, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) + self._config: Optional[Config] = None + self._markers: Set[str] = set() + def __getattr__(self, name: str) -> MarkDecorator: + """Generate a new :class:`MarkDecorator` with the given name.""" if name[0] == "_": raise AttributeError("Marker name must NOT start with underscore") @@ -541,7 +544,7 @@ def __getattr__(self, name: str) -> MarkDecorator: return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True) -MARK_GEN = MarkGenerator() +MARK_GEN = MarkGenerator(_ispytest=True) @final diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 1d5b38ee091..74cf00ee2c4 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -24,6 +24,7 @@ from _pytest.mark import Mark from _pytest.mark import MARK_GEN as mark from _pytest.mark import MarkDecorator +from _pytest.mark import MarkGenerator from _pytest.mark import param from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Collector @@ -93,6 +94,7 @@ "mark", "Mark", "MarkDecorator", + "MarkGenerator", "Module", "MonkeyPatch", "Package", diff --git a/testing/test_mark.py b/testing/test_mark.py index e0b91f0cef4..5f4b3e063e4 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -21,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 = MarkGenerator() + mark = MarkGenerator(_ispytest=True) with pytest.raises(TypeError): mark() # type: ignore[operator] @@ -40,7 +40,7 @@ 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) -> None: - mark = MarkGenerator() + mark = MarkGenerator(_ispytest=True) with pytest.raises(AttributeError): mark._some_name From 1839713b71db4f9d656c5ae3de32a9abcaa99009 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 03:07:23 +0000 Subject: [PATCH 0350/2846] build(deps): bump pytest-mock in /testing/plugins_integration Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.3.1 to 3.4.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.3.1...v3.4.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 99ddef0706e..b2ca3e3236a 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -6,7 +6,7 @@ pytest-cov==2.10.1 pytest-django==4.1.0 pytest-flakes==4.0.3 pytest-html==3.1.1 -pytest-mock==3.3.1 +pytest-mock==3.4.0 pytest-rerunfailures==9.1.1 pytest-sugar==0.9.4 pytest-trio==0.7.0 From 92ba96b0612e6b06bb8f4ab05bd75481d2504806 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:11:00 +0200 Subject: [PATCH 0351/2846] code: convert from py.path to pathlib --- changelog/8174.trivial.rst | 5 +++ src/_pytest/_code/code.py | 55 +++++++++++++++------------- src/_pytest/fixtures.py | 8 +++- src/_pytest/nodes.py | 8 ++-- src/_pytest/python.py | 6 ++- testing/code/test_excinfo.py | 71 ++++++++++++++++++------------------ testing/code/test_source.py | 11 +++--- 7 files changed, 90 insertions(+), 74 deletions(-) create mode 100644 changelog/8174.trivial.rst diff --git a/changelog/8174.trivial.rst b/changelog/8174.trivial.rst new file mode 100644 index 00000000000..001ae4cb193 --- /dev/null +++ b/changelog/8174.trivial.rst @@ -0,0 +1,5 @@ +The following changes have been made to internal pytest types/functions: + +- The ``path`` property of ``_pytest.code.Code`` returns ``Path`` instead of ``py.path.local``. +- The ``path`` property of ``_pytest.code.TracebackEntry`` returns ``Path`` instead of ``py.path.local``. +- The ``_pytest.code.getfslineno()`` function returns ``Path`` instead of ``py.path.local``. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 423069330a5..043a23a79af 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -43,6 +43,8 @@ from _pytest._io.saferepr import saferepr from _pytest.compat import final from _pytest.compat import get_real_func +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath if TYPE_CHECKING: from typing_extensions import Literal @@ -78,16 +80,16 @@ def name(self) -> str: return self.raw.co_name @property - def path(self) -> Union[py.path.local, str]: + def path(self) -> Union[Path, str]: """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: - p = py.path.local(self.raw.co_filename) + p = absolutepath(self.raw.co_filename) # maybe don't try this checking - if not p.check(): - raise OSError("py.path check failed.") + if not p.exists(): + raise OSError("path check failed.") return p except OSError: # XXX maybe try harder like the weird logic @@ -223,7 +225,7 @@ def statement(self) -> "Source": return source.getstatement(self.lineno) @property - def path(self) -> Union[py.path.local, str]: + def path(self) -> Union[Path, str]: """Path to the source code.""" return self.frame.code.path @@ -336,10 +338,10 @@ def f(cur: TracebackType) -> Iterable[TracebackEntry]: def cut( self, - path=None, + path: Optional[Union[Path, str]] = None, lineno: Optional[int] = None, firstlineno: Optional[int] = None, - excludepath: Optional[py.path.local] = None, + excludepath: Optional[Path] = None, ) -> "Traceback": """Return a Traceback instance wrapping part of this Traceback. @@ -353,17 +355,19 @@ def cut( for x in self: code = x.frame.code codepath = code.path + if path is not None and codepath != path: + continue if ( - (path is None or codepath == path) - and ( - excludepath is None - or not isinstance(codepath, py.path.local) - or not codepath.relto(excludepath) - ) - and (lineno is None or x.lineno == lineno) - and (firstlineno is None or x.frame.code.firstlineno == firstlineno) + excludepath is not None + and isinstance(codepath, Path) + and excludepath in codepath.parents ): - return Traceback(x._rawentry, self._excinfo) + continue + if lineno is not None and x.lineno != lineno: + continue + if firstlineno is not None and x.frame.code.firstlineno != firstlineno: + continue + return Traceback(x._rawentry, self._excinfo) return self @overload @@ -801,7 +805,8 @@ def repr_traceback_entry( message = "in %s" % (entry.name) else: message = excinfo and excinfo.typename or "" - path = self._makepath(entry.path) + entry_path = entry.path + path = self._makepath(entry_path) reprfileloc = ReprFileLocation(path, entry.lineno + 1, message) localsrepr = self.repr_locals(entry.locals) return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style) @@ -814,15 +819,15 @@ def repr_traceback_entry( lines.extend(self.get_exconly(excinfo, indent=4)) return ReprEntry(lines, None, None, None, style) - def _makepath(self, path): - if not self.abspath: + def _makepath(self, path: Union[Path, str]) -> str: + if not self.abspath and isinstance(path, Path): try: - np = py.path.local().bestrelpath(path) + np = bestrelpath(Path.cwd(), path) except OSError: - return path + return str(path) if len(np) < len(str(path)): - path = np - return path + return np + return str(path) def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": traceback = excinfo.traceback @@ -1181,7 +1186,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line("") -def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: +def getfslineno(obj: object) -> Tuple[Union[str, Path], int]: """Return source location (path, lineno) for the given object. If the source cannot be determined return ("", -1). @@ -1203,7 +1208,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: except TypeError: return "", -1 - fspath = fn and py.path.local(fn) or "" + fspath = fn and absolutepath(fn) or "" lineno = -1 if fspath: try: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index c24ab7069cb..6db1c5906f0 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -5,6 +5,7 @@ import warnings from collections import defaultdict from collections import deque +from pathlib import Path from types import TracebackType from typing import Any from typing import Callable @@ -58,6 +59,7 @@ from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath from _pytest.store import StoreKey if TYPE_CHECKING: @@ -718,7 +720,11 @@ def _factorytraceback(self) -> List[str]: for fixturedef in self._get_fixturestack(): factory = fixturedef.func fs, lineno = getfslineno(factory) - p = self._pyfuncitem.session.fspath.bestrelpath(fs) + if isinstance(fs, Path): + session: Session = self._pyfuncitem.session + p = bestrelpath(Path(session.fspath), fs) + else: + p = fs args = _format_args(factory) lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) return lines diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 1b3ec5571b1..da2a0a7ea79 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -39,7 +39,7 @@ SEP = "/" -tracebackcutdir = py.path.local(_pytest.__file__).dirpath() +tracebackcutdir = Path(_pytest.__file__).parent def iterparentnodeids(nodeid: str) -> Iterator[str]: @@ -416,9 +416,7 @@ def repr_failure( return self._repr_failure_py(excinfo, style) -def get_fslocation_from_item( - node: "Node", -) -> Tuple[Union[str, py.path.local], Optional[int]]: +def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[int]]: """Try to extract the actual location from a node, depending on available attributes: * "location": a pair (path, lineno) @@ -474,7 +472,7 @@ def repr_failure( # type: ignore[override] def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: if hasattr(self, "fspath"): traceback = excinfo.traceback - ntraceback = traceback.cut(path=self.fspath) + ntraceback = traceback.cut(path=Path(self.fspath)) if ntraceback == traceback: ntraceback = ntraceback.cut(excludepath=tracebackcutdir) excinfo.traceback = ntraceback.filter() diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 3ff04455fbf..27bbb24fe2c 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -340,7 +340,11 @@ def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]: fspath: Union[py.path.local, str] = file_path lineno = compat_co_firstlineno else: - fspath, lineno = getfslineno(obj) + path, lineno = getfslineno(obj) + if isinstance(path, Path): + fspath = py.path.local(path) + else: + fspath = path modpath = self.getmodpath() assert isinstance(lineno, int) return fspath, lineno, modpath diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 44d7ab549e8..19c888403f2 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,7 +1,6 @@ import importlib import io import operator -import os import queue import sys import textwrap @@ -12,14 +11,14 @@ from typing import TYPE_CHECKING from typing import Union -import py - import _pytest import pytest from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import FormattedExcinfo from _pytest._io import TerminalWriter +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pathlib import bestrelpath from _pytest.pathlib import import_path from _pytest.pytester import LineMatcher from _pytest.pytester import Pytester @@ -150,9 +149,10 @@ def xyz(): " except somenoname: # type: ignore[name-defined] # noqa: F821", ] - def test_traceback_cut(self): + def test_traceback_cut(self) -> None: co = _pytest._code.Code.from_function(f) path, firstlineno = co.path, co.firstlineno + assert isinstance(path, Path) traceback = self.excinfo.traceback newtraceback = traceback.cut(path=path, firstlineno=firstlineno) assert len(newtraceback) == 1 @@ -163,11 +163,11 @@ def test_traceback_cut_excludepath(self, pytester: Pytester) -> None: p = pytester.makepyfile("def f(): raise ValueError") with pytest.raises(ValueError) as excinfo: import_path(p).f() # type: ignore[attr-defined] - basedir = py.path.local(pytest.__file__).dirpath() + basedir = Path(pytest.__file__).parent newtraceback = excinfo.traceback.cut(excludepath=basedir) for x in newtraceback: - if hasattr(x, "path"): - assert not py.path.local(x.path).relto(basedir) + assert isinstance(x.path, Path) + assert basedir not in x.path.parents assert newtraceback[-1].frame.code.path == p def test_traceback_filter(self): @@ -376,7 +376,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 isinstance(item.path, py.path.local) and item.path.basename == "test.txt": + if isinstance(item.path, Path) and item.path.name == "test.txt": assert str(item.source) == "{{ h()}}:" @@ -392,16 +392,16 @@ def test_entrysource_Queue_example(): assert s.startswith("def get") -def test_codepath_Queue_example(): +def test_codepath_Queue_example() -> None: try: queue.Queue().get(timeout=0.001) except queue.Empty: excinfo = _pytest._code.ExceptionInfo.from_current() entry = excinfo.traceback[-1] path = entry.path - assert isinstance(path, py.path.local) - assert path.basename.lower() == "queue.py" - assert path.check() + assert isinstance(path, Path) + assert path.name.lower() == "queue.py" + assert path.exists() def test_match_succeeds(): @@ -805,21 +805,21 @@ def entry(): raised = 0 - orig_getcwd = os.getcwd + orig_path_cwd = Path.cwd def raiseos(): nonlocal raised upframe = sys._getframe().f_back assert upframe is not None - if upframe.f_code.co_name == "checked_call": + if upframe.f_code.co_name == "_makepath": # Only raise with expected calls, but not via e.g. inspect for # py38-windows. raised += 1 raise OSError(2, "custom_oserror") - return orig_getcwd() + return orig_path_cwd() - monkeypatch.setattr(os, "getcwd", raiseos) - assert p._makepath(__file__) == __file__ + monkeypatch.setattr(Path, "cwd", raiseos) + assert p._makepath(Path(__file__)) == __file__ assert raised == 1 repr_tb = p.repr_traceback(excinfo) @@ -1015,7 +1015,9 @@ def f(): assert line.endswith("mod.py") assert tw_mock.lines[10] == ":3: ValueError" - def test_toterminal_long_filenames(self, importasmod, tw_mock): + def test_toterminal_long_filenames( + self, importasmod, tw_mock, monkeypatch: MonkeyPatch + ) -> None: mod = importasmod( """ def f(): @@ -1023,25 +1025,22 @@ def f(): """ ) excinfo = pytest.raises(ValueError, mod.f) - path = py.path.local(mod.__file__) - old = path.dirpath().chdir() - try: - repr = excinfo.getrepr(abspath=False) - repr.toterminal(tw_mock) - x = py.path.local().bestrelpath(path) - if len(x) < len(str(path)): - msg = tw_mock.get_write_msg(-2) - assert msg == "mod.py" - assert tw_mock.lines[-1] == ":3: ValueError" - - repr = excinfo.getrepr(abspath=True) - repr.toterminal(tw_mock) + path = Path(mod.__file__) + monkeypatch.chdir(path.parent) + repr = excinfo.getrepr(abspath=False) + repr.toterminal(tw_mock) + x = bestrelpath(Path.cwd(), path) + if len(x) < len(str(path)): msg = tw_mock.get_write_msg(-2) - assert msg == path - line = tw_mock.lines[-1] - assert line == ":3: ValueError" - finally: - old.chdir() + assert msg == "mod.py" + assert tw_mock.lines[-1] == ":3: ValueError" + + repr = excinfo.getrepr(abspath=True) + repr.toterminal(tw_mock) + msg = tw_mock.get_write_msg(-2) + assert msg == str(path) + line = tw_mock.lines[-1] + assert line == ":3: ValueError" @pytest.mark.parametrize( "reproptions", diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 04d0ea9323d..6b8443fd243 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -6,13 +6,12 @@ import linecache import sys import textwrap +from pathlib import Path from types import CodeType from typing import Any from typing import Dict from typing import Optional -import py.path - import pytest from _pytest._code import Code from _pytest._code import Frame @@ -352,8 +351,8 @@ def f(x) -> None: fspath, lineno = getfslineno(f) - assert isinstance(fspath, py.path.local) - assert fspath.basename == "test_source.py" + assert isinstance(fspath, Path) + assert fspath.name == "test_source.py" assert lineno == f.__code__.co_firstlineno - 1 # see findsource class A: @@ -362,8 +361,8 @@ class A: fspath, lineno = getfslineno(A) _, A_lineno = inspect.findsource(A) - assert isinstance(fspath, py.path.local) - assert fspath.basename == "test_source.py" + assert isinstance(fspath, Path) + assert fspath.name == "test_source.py" assert lineno == A_lineno assert getfslineno(3) == ("", -1) From 8b220fad4de5e36d3b62d57ca0121b4865f7e518 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:48:56 +0200 Subject: [PATCH 0352/2846] testing/test_helpconfig: remove unclear comment --- testing/test_helpconfig.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index c2533ef304a..9a433b1b17e 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -16,7 +16,6 @@ def test_version_less_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> 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__}"]) From 170a2c50408c551c31dbe51e9572c174d0c5cdde Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:49:24 +0200 Subject: [PATCH 0353/2846] testing/test_config: check inipath instead of inifile inifile is soft-deprecated in favor of inipath. --- testing/test_config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/testing/test_config.py b/testing/test_config.py index eacc9c9ebdd..8c1441e0680 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -11,7 +11,6 @@ from typing import Union import attr -import py.path import _pytest._code import pytest @@ -28,6 +27,7 @@ from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import locate_config from _pytest.monkeypatch import MonkeyPatch +from _pytest.pathlib import absolutepath from _pytest.pytester import Pytester @@ -854,8 +854,8 @@ def test_inifilename(self, tmp_path: Path) -> None: ) ) - inifile = "../../foo/bar.ini" - option_dict = {"inifilename": inifile, "capture": "no"} + inifilename = "../../foo/bar.ini" + option_dict = {"inifilename": inifilename, "capture": "no"} cwd = tmp_path.joinpath("a/b") cwd.mkdir(parents=True) @@ -873,14 +873,14 @@ def test_inifilename(self, tmp_path: Path) -> None: with MonkeyPatch.context() as mp: mp.chdir(cwd) config = Config.fromdictargs(option_dict, ()) - inipath = py.path.local(inifile) + inipath = absolutepath(inifilename) assert config.args == [str(cwd)] - assert config.option.inifilename == inifile + assert config.option.inifilename == inifilename assert config.option.capture == "no" # this indicates this is the file used for getting configuration values - assert config.inifile == inipath + assert config.inipath == inipath assert config.inicfg.get("name") == "value" assert config.inicfg.get("should_not_be_set") is None From a21841300826fd67b43e5bb3ff1a04e11759dcc1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:51:23 +0200 Subject: [PATCH 0354/2846] pathlib: missing type annotation for fnmatch_ex --- 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 2e452eb1cc9..d3908a3fdc0 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -387,7 +387,7 @@ def resolve_from_str(input: str, rootpath: Path) -> Path: return rootpath.joinpath(input) -def fnmatch_ex(pattern: str, path) -> bool: +def fnmatch_ex(pattern: str, path: Union[str, "os.PathLike[str]"]) -> bool: """A port of FNMatcher from py.path.common which works with PurePath() instances. The difference between this algorithm and PurePath.match() is that the From 4faed282613dbbe7196543cc8b45885269f39d4a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 15:16:01 +0200 Subject: [PATCH 0355/2846] testing: convert some tmpdir to tmp_path The tmpdir fixture (and its factory variant) is soft-deprecated in favor of the tmp_path fixture. --- testing/test_cacheprovider.py | 15 ++- testing/test_pathlib.py | 204 +++++++++++++++++++--------------- 2 files changed, 124 insertions(+), 95 deletions(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index ebd455593f3..2cb657efc16 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -8,6 +8,7 @@ from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +from _pytest.tmpdir import TempPathFactory pytest_plugins = ("pytester",) @@ -139,9 +140,11 @@ def test_custom_rel_cache_dir(self, pytester: Pytester) -> None: pytester.runpytest() assert pytester.path.joinpath(rel_cache_dir).is_dir() - def test_custom_abs_cache_dir(self, pytester: Pytester, tmpdir_factory) -> None: - tmp = str(tmpdir_factory.mktemp("tmp")) - abs_cache_dir = os.path.join(tmp, "custom_cache_dir") + def test_custom_abs_cache_dir( + self, pytester: Pytester, tmp_path_factory: TempPathFactory + ) -> None: + tmp = tmp_path_factory.mktemp("tmp") + abs_cache_dir = tmp / "custom_cache_dir" pytester.makeini( """ [pytest] @@ -152,7 +155,7 @@ def test_custom_abs_cache_dir(self, pytester: Pytester, tmpdir_factory) -> None: ) pytester.makepyfile(test_errored="def test_error():\n assert False") pytester.runpytest() - assert Path(abs_cache_dir).is_dir() + assert abs_cache_dir.is_dir() def test_custom_cache_dir_with_env_var( self, pytester: Pytester, monkeypatch: MonkeyPatch @@ -185,9 +188,9 @@ def test_cache_reportheader(env, pytester: Pytester, monkeypatch: MonkeyPatch) - def test_cache_reportheader_external_abspath( - pytester: Pytester, tmpdir_factory + pytester: Pytester, tmp_path_factory: TempPathFactory ) -> None: - external_cache = tmpdir_factory.mktemp( + external_cache = tmp_path_factory.mktemp( "test_cache_reportheader_external_abspath_abs" ) diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index f60b9f26369..48149084ece 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -1,8 +1,11 @@ import os.path +import pickle import sys import unittest.mock from pathlib import Path from textwrap import dedent +from types import ModuleType +from typing import Generator import py @@ -20,6 +23,7 @@ from _pytest.pathlib import resolve_package_path from _pytest.pathlib import symlink_or_skip from _pytest.pathlib import visit +from _pytest.tmpdir import TempPathFactory class TestFNMatcherPort: @@ -96,38 +100,40 @@ class TestImportPath: """ @pytest.fixture(scope="session") - def path1(self, tmpdir_factory): - path = tmpdir_factory.mktemp("path") + def path1(self, tmp_path_factory: TempPathFactory) -> Generator[Path, None, None]: + path = tmp_path_factory.mktemp("path") self.setuptestfs(path) yield path - assert path.join("samplefile").check() + assert path.joinpath("samplefile").exists() - def setuptestfs(self, path): + def setuptestfs(self, path: Path) -> None: # print "setting up test fs for", repr(path) - samplefile = path.ensure("samplefile") - samplefile.write("samplefile\n") + samplefile = path / "samplefile" + samplefile.write_text("samplefile\n") - execfile = path.ensure("execfile") - execfile.write("x=42") + execfile = path / "execfile" + execfile.write_text("x=42") - execfilepy = path.ensure("execfile.py") - execfilepy.write("x=42") + execfilepy = path / "execfile.py" + execfilepy.write_text("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( + path.joinpath("samplepickle").write_bytes(pickle.dumps(d, 1)) + + sampledir = path / "sampledir" + sampledir.mkdir() + sampledir.joinpath("otherfile").touch() + + otherdir = path / "otherdir" + otherdir.mkdir() + otherdir.joinpath("__init__.py").touch() + + module_a = otherdir / "a.py" + module_a.write_text("from .b import stuff as result\n") + module_b = otherdir / "b.py" + module_b.write_text('stuff="got it"\n') + module_c = otherdir / "c.py" + module_c.write_text( dedent( """ import py; @@ -136,8 +142,8 @@ def setuptestfs(self, path): """ ) ) - module_d = otherdir.ensure("d.py") - module_d.write( + module_d = otherdir / "d.py" + module_d.write_text( dedent( """ import py; @@ -147,122 +153,141 @@ def setuptestfs(self, path): ) ) - def test_smoke_test(self, path1): - obj = import_path(path1.join("execfile.py")) + def test_smoke_test(self, path1: Path) -> None: + obj = import_path(path1 / "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") + def test_renamed_dir_creates_mismatch( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: + tmp_path.joinpath("a").mkdir() + p = tmp_path.joinpath("a", "test_x123.py") + p.touch() import_path(p) - tmpdir.join("a").move(tmpdir.join("b")) + tmp_path.joinpath("a").rename(tmp_path.joinpath("b")) with pytest.raises(ImportPathMismatchError): - import_path(tmpdir.join("b", "test_x123.py")) + import_path(tmp_path.joinpath("b", "test_x123.py")) # Errors can be ignored. monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "1") - import_path(tmpdir.join("b", "test_x123.py")) + import_path(tmp_path.joinpath("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")) + import_path(tmp_path.joinpath("b", "test_x123.py")) - def test_messy_name(self, tmpdir): + def test_messy_name(self, tmp_path: Path) -> None: # http://bitbucket.org/hpk42/py-trunk/issue/129 - path = tmpdir.ensure("foo__init__.py") + path = tmp_path / "foo__init__.py" + path.touch() 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") + def test_dir(self, tmp_path: Path) -> None: + p = tmp_path / "hello_123" + p.mkdir() + p_init = p / "__init__.py" + p_init.touch() 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")) + def test_a(self, path1: Path) -> None: + otherdir = path1 / "otherdir" + mod = import_path(otherdir / "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")) + def test_b(self, path1: Path) -> None: + otherdir = path1 / "otherdir" + mod = import_path(otherdir / "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")) + def test_c(self, path1: Path) -> None: + otherdir = path1 / "otherdir" + mod = import_path(otherdir / "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")) + def test_d(self, path1: Path) -> None: + otherdir = path1 / "otherdir" + mod = import_path(otherdir / "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") + def test_import_after(self, tmp_path: Path) -> None: + tmp_path.joinpath("xxxpackage").mkdir() + tmp_path.joinpath("xxxpackage", "__init__.py").touch() + mod1path = tmp_path.joinpath("xxxpackage", "module1.py") + mod1path.touch() 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): + def test_check_filepath_consistency( + self, monkeypatch: MonkeyPatch, tmp_path: Path + ) -> None: name = "pointsback123" - ModuleType = type(os) - p = tmpdir.ensure(name + ".py") + p = tmp_path.joinpath(name + ".py") + p.touch() for ending in (".pyc", ".pyo"): mod = ModuleType(name) - pseudopath = tmpdir.ensure(name + ending) + pseudopath = tmp_path.joinpath(name + ending) + pseudopath.touch() 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") + pseudopath = tmp_path.joinpath(name + "123.py") + pseudopath.touch() 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 modfile == str(pseudopath) assert orig == p assert issubclass(ImportPathMismatchError, ImportError) - def test_issue131_on__init__(self, tmpdir): + def test_issue131_on__init__(self, tmp_path: Path) -> None: # __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") + tmp_path.joinpath("proja").mkdir() + p1 = tmp_path.joinpath("proja", "__init__.py") + p1.touch() + tmp_path.joinpath("sub", "proja").mkdir(parents=True) + p2 = tmp_path.joinpath("sub", "proja", "__init__.py") + p2.touch() 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") + def test_ensuresyspath_append(self, tmp_path: Path) -> None: + root1 = tmp_path / "root1" + root1.mkdir() + file1 = root1 / "x123.py" + file1.touch() 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): + def test_invalid_path(self, tmp_path: Path) -> None: with pytest.raises(ImportError): - import_path(tmpdir.join("invalid.py")) + import_path(tmp_path / "invalid.py") @pytest.fixture - def simple_module(self, tmpdir): - fn = tmpdir.join("mymod.py") - fn.write( + def simple_module(self, tmp_path: Path) -> Path: + fn = tmp_path / "mymod.py" + fn.write_text( dedent( """ def foo(x): return 40 + x @@ -271,19 +296,21 @@ def foo(x): return 40 + x ) return fn - def test_importmode_importlib(self, simple_module): + def test_importmode_importlib(self, simple_module: Path) -> None: """`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 + assert str(simple_module.parent) not in sys.path - def test_importmode_twice_is_different_module(self, simple_module): + def test_importmode_twice_is_different_module(self, simple_module: Path) -> None: """`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): + def test_no_meta_path_found( + self, simple_module: Path, monkeypatch: MonkeyPatch + ) -> None: """Even without any meta_path should still import module.""" monkeypatch.setattr(sys, "meta_path", []) module = import_path(simple_module, mode="importlib") @@ -299,7 +326,7 @@ def test_no_meta_path_found(self, simple_module, monkeypatch): import_path(simple_module, mode="importlib") -def test_resolve_package_path(tmp_path): +def test_resolve_package_path(tmp_path: Path) -> None: pkg = tmp_path / "pkg1" pkg.mkdir() (pkg / "__init__.py").touch() @@ -309,7 +336,7 @@ def test_resolve_package_path(tmp_path): assert resolve_package_path(pkg.joinpath("subdir", "__init__.py")) == pkg -def test_package_unimportable(tmp_path): +def test_package_unimportable(tmp_path: Path) -> None: pkg = tmp_path / "pkg1-1" pkg.mkdir() pkg.joinpath("__init__.py").touch() @@ -323,7 +350,7 @@ def test_package_unimportable(tmp_path): assert not resolve_package_path(pkg) -def test_access_denied_during_cleanup(tmp_path, monkeypatch): +def test_access_denied_during_cleanup(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: """Ensure that deleting a numbered dir does not fail because of OSErrors (#4262).""" path = tmp_path / "temp-1" path.mkdir() @@ -338,7 +365,7 @@ def renamed_failed(*args): assert not lock_path.is_file() -def test_long_path_during_cleanup(tmp_path): +def test_long_path_during_cleanup(tmp_path: Path) -> None: """Ensure that deleting long path works (particularly on Windows (#6775)).""" path = (tmp_path / ("a" * 250)).resolve() if sys.platform == "win32": @@ -354,14 +381,14 @@ def test_long_path_during_cleanup(tmp_path): assert not os.path.isdir(extended_path) -def test_get_extended_length_path_str(): +def test_get_extended_length_path_str() -> None: 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" -def test_suppress_error_removing_lock(tmp_path): +def test_suppress_error_removing_lock(tmp_path: Path) -> None: """ensure_deletable should be resilient if lock file cannot be removed (#5456, #7491)""" path = tmp_path / "dir" path.mkdir() @@ -406,15 +433,14 @@ def test_commonpath() -> None: 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"") +def test_visit_ignores_errors(tmp_path: Path) -> None: + symlink_or_skip("recursive", tmp_path / "recursive") + tmp_path.joinpath("foo").write_bytes(b"") + tmp_path.joinpath("bar").write_bytes(b"") - assert [entry.name for entry in visit(tmpdir, recurse=lambda entry: False)] == [ - "bar", - "foo", - ] + assert [ + entry.name for entry in visit(str(tmp_path), recurse=lambda entry: False) + ] == ["bar", "foo"] @pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only") From ca4effc8225edf7fc828a4291642c82349ed8107 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Dec 2020 14:52:10 +0200 Subject: [PATCH 0356/2846] Convert most of the collection code from py.path to pathlib --- changelog/8174.trivial.rst | 1 + src/_pytest/config/__init__.py | 2 +- src/_pytest/main.py | 84 +++++++++++++++++----------------- src/_pytest/python.py | 61 ++++++++++++------------ testing/test_collection.py | 4 +- 5 files changed, 77 insertions(+), 75 deletions(-) diff --git a/changelog/8174.trivial.rst b/changelog/8174.trivial.rst index 001ae4cb193..7649764618f 100644 --- a/changelog/8174.trivial.rst +++ b/changelog/8174.trivial.rst @@ -3,3 +3,4 @@ The following changes have been made to internal pytest types/functions: - The ``path`` property of ``_pytest.code.Code`` returns ``Path`` instead of ``py.path.local``. - The ``path`` property of ``_pytest.code.TracebackEntry`` returns ``Path`` instead of ``py.path.local``. - The ``_pytest.code.getfslineno()`` function returns ``Path`` instead of ``py.path.local``. +- The ``_pytest.python.path_matches_patterns()`` function takes ``Path`` instead of ``py.path.local``. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c9a0e78bfcf..760b0f55c7b 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -348,7 +348,7 @@ def __init__(self) -> None: self._conftestpath2mod: Dict[Path, types.ModuleType] = {} self._confcutdir: Optional[Path] = None self._noconftest = False - self._duplicatepaths: Set[py.path.local] = set() + self._duplicatepaths: Set[Path] = set() # plugins that were explicitly skipped with pytest.skip # list of (module name, skip reason) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index e7c31ecc1d5..79afdde6155 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -37,6 +37,7 @@ from _pytest.outcomes import exit from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath +from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import visit from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -353,11 +354,14 @@ def pytest_runtestloop(session: "Session") -> bool: return True -def _in_venv(path: py.path.local) -> bool: +def _in_venv(path: Path) -> bool: """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(): + bindir = path.joinpath("Scripts" if sys.platform.startswith("win") else "bin") + try: + if not bindir.is_dir(): + return False + except OSError: return False activates = ( "activate", @@ -367,33 +371,32 @@ def _in_venv(path: py.path.local) -> bool: "Activate.bat", "Activate.ps1", ) - return any([fname.basename in activates for fname in bindir.listdir()]) + return any(fname.name in activates for fname in bindir.iterdir()) -def pytest_ignore_collect(path: py.path.local, config: Config) -> Optional[bool]: - path_ = Path(path) - ignore_paths = config._getconftest_pathlist("collect_ignore", path=path_.parent) +def pytest_ignore_collect(fspath: Path, config: Config) -> Optional[bool]: + ignore_paths = config._getconftest_pathlist("collect_ignore", path=fspath.parent) ignore_paths = ignore_paths or [] excludeopt = config.getoption("ignore") if excludeopt: ignore_paths.extend(absolutepath(x) for x in excludeopt) - if path_ in ignore_paths: + if fspath in ignore_paths: return True ignore_globs = config._getconftest_pathlist( - "collect_ignore_glob", path=path_.parent + "collect_ignore_glob", path=fspath.parent ) ignore_globs = ignore_globs or [] excludeglobopt = config.getoption("ignore_glob") if excludeglobopt: ignore_globs.extend(absolutepath(x) for x in excludeglobopt) - if any(fnmatch.fnmatch(str(path), str(glob)) for glob in ignore_globs): + if any(fnmatch.fnmatch(str(fspath), str(glob)) for glob in ignore_globs): return True allow_in_venv = config.getoption("collect_in_virtualenv") - if not allow_in_venv and _in_venv(path): + if not allow_in_venv and _in_venv(fspath): return True return None @@ -538,21 +541,21 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): return False norecursepatterns = self.config.getini("norecursedirs") - if any(path.check(fnmatch=pat) for pat in norecursepatterns): + if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns): return False return True def _collectfile( - self, path: py.path.local, handle_dupes: bool = True + self, fspath: Path, handle_dupes: bool = True ) -> Sequence[nodes.Collector]: - fspath = Path(path) + path = py.path.local(fspath) assert ( - path.isfile() + fspath.is_file() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( - path, path.isdir(), path.exists(), path.islink() + fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink() ) - ihook = self.gethookproxy(path) - if not self.isinitpath(path): + ihook = self.gethookproxy(fspath) + if not self.isinitpath(fspath): if ihook.pytest_ignore_collect( fspath=fspath, path=path, config=self.config ): @@ -562,10 +565,10 @@ def _collectfile( keepduplicates = self.config.getoption("keepduplicates") if not keepduplicates: duplicate_paths = self.config.pluginmanager._duplicatepaths - if path in duplicate_paths: + if fspath in duplicate_paths: return () else: - duplicate_paths.add(path) + duplicate_paths.add(fspath) return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return] @@ -652,10 +655,8 @@ 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: Dict[py.path.local, Sequence[nodes.Collector]] = {} - node_cache2: Dict[ - Tuple[Type[nodes.Collector], py.path.local], nodes.Collector - ] = ({}) + node_cache1: Dict[Path, Sequence[nodes.Collector]] = {} + node_cache2: Dict[Tuple[Type[nodes.Collector], Path], nodes.Collector] = ({}) # Keep track of any collected collectors in matchnodes paths, so they # are not collected more than once. @@ -679,31 +680,31 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: break if parent.is_dir(): - pkginit = py.path.local(parent / "__init__.py") - if pkginit.isfile() and pkginit not in node_cache1: + pkginit = parent / "__init__.py" + if pkginit.is_file() 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]] + node_cache1[Path(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.is_dir(): assert not names, "invalid arg {!r}".format((argpath, names)) - seen_dirs: Set[py.path.local] = set() + seen_dirs: Set[Path] = set() for direntry in visit(str(argpath), self._recurse): if not direntry.is_file(): continue - path = py.path.local(direntry.path) - dirpath = path.dirpath() + path = Path(direntry.path) + dirpath = path.parent if dirpath not in seen_dirs: # Collect packages first. seen_dirs.add(dirpath) - pkginit = dirpath.join("__init__.py") + pkginit = dirpath / "__init__.py" if pkginit.exists(): for x in self._collectfile(pkginit): yield x @@ -714,23 +715,22 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: continue for x in self._collectfile(path): - key = (type(x), x.fspath) - if key in node_cache2: - yield node_cache2[key] + key2 = (type(x), Path(x.fspath)) + if key2 in node_cache2: + yield node_cache2[key2] else: - node_cache2[key] = x + node_cache2[key2] = x yield x else: assert argpath.is_file() - argpath_ = py.path.local(argpath) - if argpath_ in node_cache1: - col = node_cache1[argpath_] + if argpath in node_cache1: + col = node_cache1[argpath] else: - collect_root = pkg_roots.get(argpath_.dirname, self) - col = collect_root._collectfile(argpath_, handle_dupes=False) + collect_root = pkg_roots.get(str(argpath.parent), self) + col = collect_root._collectfile(argpath, handle_dupes=False) if col: - node_cache1[argpath_] = col + node_cache1[argpath] = col matching = [] work: List[ @@ -846,7 +846,7 @@ def resolve_collection_argument( This function ensures the path exists, and returns a tuple: - (py.path.path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"]) + (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: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 27bbb24fe2c..b4605092000 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -66,6 +66,8 @@ from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail from _pytest.outcomes import skip +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import import_path from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import parts @@ -190,11 +192,10 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: def pytest_collect_file( fspath: Path, path: py.path.local, parent: nodes.Collector ) -> Optional["Module"]: - ext = path.ext - if ext == ".py": + if fspath.suffix == ".py": if not parent.session.isinitpath(fspath): if not path_matches_patterns( - path, parent.config.getini("python_files") + ["__init__.py"] + fspath, parent.config.getini("python_files") + ["__init__.py"] ): return None ihook = parent.session.gethookproxy(fspath) @@ -205,13 +206,13 @@ def pytest_collect_file( return None -def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool: +def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: """Return whether path matches any of the patterns in the list of globs given.""" - return any(path.fnmatch(pattern) for pattern in patterns) + return any(fnmatch_ex(pattern, path) for pattern in patterns) -def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module": - if path.basename == "__init__.py": +def pytest_pycollect_makemodule(fspath: Path, path: py.path.local, parent) -> "Module": + if fspath.name == "__init__.py": pkg: Package = Package.from_parent(parent, fspath=path) return pkg mod: Module = Module.from_parent(parent, fspath=path) @@ -677,21 +678,21 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): return False norecursepatterns = self.config.getini("norecursedirs") - if any(path.check(fnmatch=pat) for pat in norecursepatterns): + if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns): return False return True def _collectfile( - self, path: py.path.local, handle_dupes: bool = True + self, fspath: Path, handle_dupes: bool = True ) -> Sequence[nodes.Collector]: - fspath = Path(path) + path = py.path.local(fspath) assert ( - path.isfile() + fspath.is_file() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( - path, path.isdir(), path.exists(), path.islink() + path, fspath.is_dir(), fspath.exists(), fspath.is_symlink() ) - ihook = self.session.gethookproxy(path) - if not self.session.isinitpath(path): + ihook = self.session.gethookproxy(fspath) + if not self.session.isinitpath(fspath): if ihook.pytest_ignore_collect( fspath=fspath, path=path, config=self.config ): @@ -701,32 +702,32 @@ def _collectfile( keepduplicates = self.config.getoption("keepduplicates") if not keepduplicates: duplicate_paths = self.config.pluginmanager._duplicatepaths - if path in duplicate_paths: + if fspath in duplicate_paths: return () else: - duplicate_paths.add(path) + duplicate_paths.add(fspath) return ihook.pytest_collect_file(fspath=fspath, 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") - if init_module.check(file=1) and path_matches_patterns( + this_path = Path(self.fspath).parent + init_module = this_path / "__init__.py" + if init_module.is_file() and path_matches_patterns( init_module, self.config.getini("python_files") ): - yield Module.from_parent(self, fspath=init_module) - pkg_prefixes: Set[py.path.local] = set() + yield Module.from_parent(self, fspath=py.path.local(init_module)) + pkg_prefixes: Set[Path] = set() for direntry in visit(str(this_path), recurse=self._recurse): - path = py.path.local(direntry.path) + path = Path(direntry.path) # We will visit our own __init__.py file, in which case we skip it. if direntry.is_file(): - if direntry.name == "__init__.py" and path.dirpath() == this_path: + if direntry.name == "__init__.py" and path.parent == this_path: continue parts_ = parts(direntry.path) if any( - str(pkg_prefix) in parts_ and pkg_prefix.join("__init__.py") != path + str(pkg_prefix) in parts_ and pkg_prefix / "__init__.py" != path for pkg_prefix in pkg_prefixes ): continue @@ -736,7 +737,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: elif not direntry.is_dir(): # Broken symlink or invalid/missing file. continue - elif path.join("__init__.py").check(file=1): + elif path.joinpath("__init__.py").is_file(): pkg_prefixes.add(path) @@ -1416,13 +1417,13 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() - curdir = py.path.local() + curdir = Path.cwd() tw = _pytest.config.create_terminal_writer(config) verbose = config.getvalue("verbose") - def get_best_relpath(func): + def get_best_relpath(func) -> str: loc = getlocation(func, str(curdir)) - return curdir.bestrelpath(py.path.local(loc)) + return bestrelpath(curdir, Path(loc)) def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: argname = fixture_def.argname @@ -1472,7 +1473,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() - curdir = py.path.local() + curdir = Path.cwd() tw = _pytest.config.create_terminal_writer(config) verbose = config.getvalue("verbose") @@ -1494,7 +1495,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: ( len(fixturedef.baseid), fixturedef.func.__module__, - curdir.bestrelpath(py.path.local(loc)), + bestrelpath(curdir, Path(loc)), fixturedef.argname, fixturedef, ) diff --git a/testing/test_collection.py b/testing/test_collection.py index 9733b4fbd47..3dd9283eced 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -212,12 +212,12 @@ def test__in_venv(self, pytester: Pytester, fname: str) -> None: bindir = "Scripts" if sys.platform.startswith("win") else "bin" # no bin/activate, not a virtualenv base_path = pytester.mkdir("venv") - assert _in_venv(py.path.local(base_path)) is False + assert _in_venv(base_path) is False # with bin/activate, totally a virtualenv bin_path = base_path.joinpath(bindir) bin_path.mkdir() bin_path.joinpath(fname).touch() - assert _in_venv(py.path.local(base_path)) is True + assert _in_venv(base_path) is True def test_custom_norecursedirs(self, pytester: Pytester) -> None: pytester.makeini( From 5e323becb7df4ecafc7fba16b31a5a8f83d5e28d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 22 Dec 2020 21:13:34 +0200 Subject: [PATCH 0357/2846] Revert "doc: temporary workaround for pytest-pygments lexing error" Support was added in pytest-pygments 2.2.0. This reverts commit 0feeddf8edb87052402fafe690d019e3eb75dfa4. --- doc/en/fixture.rst | 2 +- doc/en/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index c74984563ab..752385adc89 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -1975,7 +1975,7 @@ Example: Running this test will *skip* the invocation of ``data_set`` with value ``2``: -.. code-block:: +.. code-block:: pytest $ pytest test_fixture_marks.py -v =========================== test session starts ============================ diff --git a/doc/en/requirements.txt b/doc/en/requirements.txt index fa37acfb447..20246acb750 100644 --- a/doc/en/requirements.txt +++ b/doc/en/requirements.txt @@ -1,5 +1,5 @@ pallets-sphinx-themes -pygments-pytest>=1.1.0 +pygments-pytest>=2.2.0 sphinx-removed-in>=0.2.0 sphinx>=3.1,<4 sphinxcontrib-trio From 35d6a7e78e016b0bbdbbbb7674cbe2207155ca49 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Dec 2020 14:57:04 -0800 Subject: [PATCH 0358/2846] Add badge for pre-commit.ci See #8186 --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 46b07e59d15..778faf89e50 100644 --- a/README.rst +++ b/README.rst @@ -23,6 +23,10 @@ .. image:: https://github.com/pytest-dev/pytest/workflows/main/badge.svg :target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Amain +.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/master.svg + :target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/master + :alt: pre-commit.ci status + .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black From 6d3a66d947a57fed99dcb4bae47062cd9ce6a5f2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Dec 2020 19:54:07 +0200 Subject: [PATCH 0359/2846] nodes: avoid needing to expose NodeKeywords for typing It adds no value over exporting just the ABC so do that to reduce the API surface. --- src/_pytest/nodes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index da2a0a7ea79..c6eb49dec4a 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,10 +1,12 @@ import os import warnings from pathlib import Path +from typing import Any from typing import Callable from typing import Iterable from typing import Iterator from typing import List +from typing import MutableMapping from typing import Optional from typing import overload from typing import Set @@ -148,8 +150,9 @@ def __init__( #: Filesystem path where this node was collected from (can be None). self.fspath = fspath or getattr(parent, "fspath", None) + # The explicit annotation is to avoid publicly exposing NodeKeywords. #: Keywords/markers collected from all scopes. - self.keywords = NodeKeywords(self) + self.keywords: MutableMapping[str, Any] = NodeKeywords(self) #: The marker objects belonging to this node. self.own_markers: List[Mark] = [] From bd76042344b3c3318dddf991c08d49bbce2251bb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Dec 2020 20:49:17 +0200 Subject: [PATCH 0360/2846] python: export pytest.Metafunc for typing purposes The type cannot be constructed directly, but is exported for use in type annotations, since it is reachable through existing public API. --- changelog/7469.deprecation.rst | 1 + changelog/7469.feature.rst | 1 + doc/en/deprecations.rst | 4 ++-- doc/en/funcarg_compare.rst | 4 ++-- doc/en/reference.rst | 4 ++-- src/_pytest/python.py | 12 +++++++++++- src/pytest/__init__.py | 2 ++ testing/python/metafunc.py | 2 +- 8 files changed, 22 insertions(+), 8 deletions(-) diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst index 6bbc8075522..bcf4266d8bd 100644 --- a/changelog/7469.deprecation.rst +++ b/changelog/7469.deprecation.rst @@ -3,5 +3,6 @@ Directly constructing the following classes is now deprecated: - ``_pytest.mark.structures.Mark`` - ``_pytest.mark.structures.MarkDecorator`` - ``_pytest.mark.structures.MarkGenerator`` +- ``_pytest.python.Metafunc`` 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.feature.rst b/changelog/7469.feature.rst index 81f93d1f7af..0ab2b48c42e 100644 --- a/changelog/7469.feature.rst +++ b/changelog/7469.feature.rst @@ -5,6 +5,7 @@ The newly-exported types are: - ``pytest.Mark`` for :class:`marks `. - ``pytest.MarkDecorator`` for :class:`mark decorators `. - ``pytest.MarkGenerator`` for the :class:`pytest.mark ` singleton. +- ``pytest.Metafunc`` for the :class:`metafunc ` argument to the `pytest_generate_tests ` hook. Constructing them directly is not supported; they are only meant for use in type annotations. Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 5ef1053e0b4..ec2397e596f 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -397,8 +397,8 @@ Metafunc.addcall .. versionremoved:: 4.0 -``_pytest.python.Metafunc.addcall`` was a precursor to the current parametrized mechanism. Users should use -:meth:`_pytest.python.Metafunc.parametrize` instead. +``Metafunc.addcall`` was a precursor to the current parametrized mechanism. Users should use +:meth:`pytest.Metafunc.parametrize` instead. Example: diff --git a/doc/en/funcarg_compare.rst b/doc/en/funcarg_compare.rst index 0c4913edff8..5e2a050063c 100644 --- a/doc/en/funcarg_compare.rst +++ b/doc/en/funcarg_compare.rst @@ -47,7 +47,7 @@ There are several limitations and difficulties with this approach: 2. parametrizing the "db" resource is not straight forward: you need to apply a "parametrize" decorator or implement a :py:func:`~hookspec.pytest_generate_tests` hook - calling :py:func:`~python.Metafunc.parametrize` which + calling :py:func:`~pytest.Metafunc.parametrize` which 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 @@ -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:`metafunc.parametrize(indirect=True) <_pytest.python.Metafunc.parametrize>` calls. +:py:func:`metafunc.parametrize(indirect=True) ` calls. Of course it's perfectly fine to combine parametrization and scoping: diff --git a/doc/en/reference.rst b/doc/en/reference.rst index c8e8dca7561..7f2ae01058f 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -138,7 +138,7 @@ pytest.mark.parametrize **Tutorial**: :doc:`parametrize`. -This mark has the same signature as :py:meth:`_pytest.python.Metafunc.parametrize`; see there. +This mark has the same signature as :py:meth:`pytest.Metafunc.parametrize`; see there. .. _`pytest.mark.skip ref`: @@ -870,7 +870,7 @@ Mark Metafunc ~~~~~~~~ -.. autoclass:: _pytest.python.Metafunc +.. autoclass:: pytest.Metafunc() :members: Module diff --git a/src/_pytest/python.py b/src/_pytest/python.py index b4605092000..31d91853f2f 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -55,6 +55,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.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.fixtures import FuncFixtureInfo from _pytest.main import Session @@ -467,7 +468,12 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]: fixtureinfo = definition._fixtureinfo metafunc = Metafunc( - definition, fixtureinfo, self.config, cls=cls, module=module + definition=definition, + fixtureinfo=fixtureinfo, + config=self.config, + cls=cls, + module=module, + _ispytest=True, ) methods = [] if hasattr(module, "pytest_generate_tests"): @@ -971,7 +977,11 @@ def __init__( config: Config, cls=None, module=None, + *, + _ispytest: bool = False, ) -> None: + check_ispytest(_ispytest) + #: Access to the underlying :class:`_pytest.python.FunctionDefinition`. self.definition = definition diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 74cf00ee2c4..f97b0ac2e8c 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -40,6 +40,7 @@ from _pytest.python import Class from _pytest.python import Function from _pytest.python import Instance +from _pytest.python import Metafunc from _pytest.python import Module from _pytest.python import Package from _pytest.python_api import approx @@ -95,6 +96,7 @@ "Mark", "MarkDecorator", "MarkGenerator", + "Metafunc", "Module", "MonkeyPatch", "Package", diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index c50ea53d255..58a902a3a59 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -47,7 +47,7 @@ class DefinitionMock(python.FunctionDefinition): names = getfuncargnames(func) fixtureinfo: Any = FuncFixtureInfoMock(names) definition: Any = DefinitionMock._create(func, "mock::nodeid") - return python.Metafunc(definition, fixtureinfo, config) + return python.Metafunc(definition, fixtureinfo, config, _ispytest=True) def test_no_funcargs(self) -> None: def function(): From 96ea867fec556a8d0e2b60392927572da38c88df Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Dec 2020 21:23:23 +0200 Subject: [PATCH 0361/2846] runner: export pytest.CallInfo for typing purposes The type cannot be constructed directly, but is exported for use in type annotations, since it is reachable through existing public API. This also documents `from_call` as public, because at least pytest-forked uses it, so we must treat it as public already anyway. --- changelog/7469.deprecation.rst | 1 + changelog/7469.feature.rst | 1 + doc/en/reference.rst | 2 +- src/_pytest/runner.py | 73 +++++++++++++++++++++++----------- src/pytest/__init__.py | 2 + 5 files changed, 54 insertions(+), 25 deletions(-) diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst index bcf4266d8bd..0d7908ef8ac 100644 --- a/changelog/7469.deprecation.rst +++ b/changelog/7469.deprecation.rst @@ -4,5 +4,6 @@ Directly constructing the following classes is now deprecated: - ``_pytest.mark.structures.MarkDecorator`` - ``_pytest.mark.structures.MarkGenerator`` - ``_pytest.python.Metafunc`` +- ``_pytest.runner.CallInfo`` 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.feature.rst b/changelog/7469.feature.rst index 0ab2b48c42e..f9948d686f9 100644 --- a/changelog/7469.feature.rst +++ b/changelog/7469.feature.rst @@ -6,6 +6,7 @@ The newly-exported types are: - ``pytest.MarkDecorator`` for :class:`mark decorators `. - ``pytest.MarkGenerator`` for the :class:`pytest.mark ` singleton. - ``pytest.Metafunc`` for the :class:`metafunc ` argument to the `pytest_generate_tests ` hook. +- ``pytest.runner.CallInfo`` for the :class:`CallInfo ` type passed to various hooks. Constructing them directly is not supported; they are only meant for use in type annotations. Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 7f2ae01058f..bc6c5670a5c 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -758,7 +758,7 @@ Full reference to objects accessible from :ref:`fixtures ` or :ref:`hoo CallInfo ~~~~~~~~ -.. autoclass:: _pytest.runner.CallInfo() +.. autoclass:: pytest.CallInfo() :members: diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 794690ddb0b..df046a78aca 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -26,6 +26,7 @@ from _pytest._code.code import TerminalRepr from _pytest.compat import final from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.nodes import Node @@ -260,34 +261,47 @@ def call_runtest_hook( @final -@attr.s(repr=False) +@attr.s(repr=False, init=False, auto_attribs=True) class CallInfo(Generic[TResult]): - """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[TResult]") - excinfo = attr.ib(type=Optional[ExceptionInfo[BaseException]]) - start = attr.ib(type=float) - stop = attr.ib(type=float) - duration = attr.ib(type=float) - when = attr.ib(type="Literal['collect', 'setup', 'call', 'teardown']") + """Result/Exception info of a function invocation.""" + + _result: Optional[TResult] + #: The captured exception of the call, if it raised. + excinfo: Optional[ExceptionInfo[BaseException]] + #: The system time when the call started, in seconds since the epoch. + start: float + #: The system time when the call ended, in seconds since the epoch. + stop: float + #: The call duration, in seconds. + duration: float + #: The context of invocation: "collect", "setup", "call" or "teardown". + when: "Literal['collect', 'setup', 'call', 'teardown']" + + def __init__( + self, + result: Optional[TResult], + excinfo: Optional[ExceptionInfo[BaseException]], + start: float, + stop: float, + duration: float, + when: "Literal['collect', 'setup', 'call', 'teardown']", + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._result = result + self.excinfo = excinfo + self.start = start + self.stop = stop + self.duration = duration + self.when = when @property def result(self) -> TResult: + """The return value of the call, if it didn't raise. + + Can only be accessed if excinfo is None. + """ if self.excinfo is not None: raise AttributeError(f"{self!r} has no valid result") # The cast is safe because an exception wasn't raised, hence @@ -304,6 +318,16 @@ def from_call( Union[Type[BaseException], Tuple[Type[BaseException], ...]] ] = None, ) -> "CallInfo[TResult]": + """Call func, wrapping the result in a CallInfo. + + :param func: + The function to call. Called without arguments. + :param when: + The phase in which the function is called. + :param reraise: + Exception or exceptions that shall propagate if raised by the + function, instead of being wrapped in the CallInfo. + """ excinfo = None start = timing.time() precise_start = timing.perf_counter() @@ -325,6 +349,7 @@ def from_call( when=when, result=result, excinfo=excinfo, + _ispytest=True, ) def __repr__(self) -> str: diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index f97b0ac2e8c..53917340fd7 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -48,6 +48,7 @@ from _pytest.recwarn import deprecated_call from _pytest.recwarn import WarningsRecorder from _pytest.recwarn import warns +from _pytest.runner import CallInfo from _pytest.tmpdir import TempdirFactory from _pytest.tmpdir import TempPathFactory from _pytest.warning_types import PytestAssertRewriteWarning @@ -69,6 +70,7 @@ "_fillfuncargs", "approx", "Cache", + "CallInfo", "CaptureFixture", "Class", "cmdline", From 8550c29180afb6f1138c81645f7d92f644aaf74d Mon Sep 17 00:00:00 2001 From: antonblr Date: Tue, 22 Dec 2020 20:27:00 -0800 Subject: [PATCH 0362/2846] coverage: Include code that runs in subprocesses --- .github/workflows/main.yml | 17 +++------ scripts/append_codecov_token.py | 36 ------------------- ...{report-coverage.sh => upload-coverage.sh} | 2 -- 3 files changed, 4 insertions(+), 51 deletions(-) delete mode 100644 scripts/append_codecov_token.py rename scripts/{report-coverage.sh => upload-coverage.sh} (88%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index beb50178528..a3ea24b7cb0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -137,22 +137,13 @@ jobs: - name: Test with coverage if: "matrix.use_coverage" - env: - _PYTEST_TOX_COVERAGE_RUN: "coverage run -m" - COVERAGE_PROCESS_START: ".coveragerc" - _PYTEST_TOX_EXTRA_DEP: "coverage-enable-subprocess" - run: "tox -e ${{ matrix.tox_env }}" - - - name: Prepare coverage token - if: (matrix.use_coverage && ( github.repository == 'pytest-dev/pytest' || github.event_name == 'pull_request' )) - run: | - python scripts/append_codecov_token.py + run: "tox -e ${{ matrix.tox_env }}-coverage" - - name: Report coverage - if: (matrix.use_coverage) + - name: Upload coverage + if: matrix.use_coverage && github.repository == 'pytest-dev/pytest' env: CODECOV_NAME: ${{ matrix.name }} - run: bash scripts/report-coverage.sh -F GHA,${{ runner.os }} + run: bash scripts/upload-coverage.sh -F GHA,${{ runner.os }} linting: runs-on: ubuntu-latest diff --git a/scripts/append_codecov_token.py b/scripts/append_codecov_token.py deleted file mode 100644 index 5c617aafb54..00000000000 --- a/scripts/append_codecov_token.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Appends the codecov token to the 'codecov.yml' file at the root of the repository. - -This is done by CI during PRs and builds on the pytest-dev repository so we can -upload coverage, at least until codecov grows some native integration with GitHub Actions. - -See discussion in https://github.com/pytest-dev/pytest/pull/6441 for more information. -""" -import os.path -from textwrap import dedent - - -def main(): - this_dir = os.path.dirname(__file__) - cov_file = os.path.join(this_dir, "..", "codecov.yml") - - assert os.path.isfile(cov_file), "{cov_file} does not exist".format( - cov_file=cov_file - ) - - with open(cov_file, "a") as f: - # token from: https://codecov.io/gh/pytest-dev/pytest/settings - # use same URL to regenerate it if needed - text = dedent( - """ - codecov: - token: "1eca3b1f-31a2-4fb8-a8c3-138b441b50a7" - """ - ) - f.write(text) - - print("Token updated:", cov_file) - - -if __name__ == "__main__": - main() diff --git a/scripts/report-coverage.sh b/scripts/upload-coverage.sh similarity index 88% rename from scripts/report-coverage.sh rename to scripts/upload-coverage.sh index fbcf20ca929..ad3dd482814 100755 --- a/scripts/report-coverage.sh +++ b/scripts/upload-coverage.sh @@ -10,9 +10,7 @@ else PATH="$PWD/.tox/${TOXENV##*,}/bin:$PATH" fi -python -m coverage combine python -m coverage xml -python -m coverage report -m # Set --connect-timeout to work around https://github.com/curl/curl/issues/4461 curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash -o codecov-upload.sh bash codecov-upload.sh -Z -X fix -f coverage.xml "$@" From bf9b59b3c8571b26f65159ed3fec831eae434561 Mon Sep 17 00:00:00 2001 From: Christophe Bedard Date: Sun, 27 Dec 2020 09:55:21 -0500 Subject: [PATCH 0363/2846] Add missing space in '--version' help message --- src/_pytest/helpconfig.py | 2 +- testing/test_helpconfig.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 4384d07b261..b9360cecf67 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -51,7 +51,7 @@ def pytest_addoption(parser: Parser) -> None: action="count", default=0, dest="version", - help="display pytest version and information about plugins." + help="display pytest version and information about plugins. " "When given twice, also display information about plugins.", ) group._addoption( diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index 9a433b1b17e..571a4783e67 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -28,6 +28,9 @@ def test_help(pytester: Pytester) -> None: For example: -m 'mark1 and not mark2'. reporting: --durations=N * + -V, --version display pytest version and information about plugins. + When given twice, also display information about + plugins. *setup.cfg* *minversion* *to see*markers*pytest --markers* From ee03e31831900c3a7aba9f94a9693a833a3ab9de Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Dec 2020 11:56:09 +0200 Subject: [PATCH 0364/2846] [pre-commit.ci] pre-commit autoupdate (#8201) * [pre-commit.ci] pre-commit autoupdate * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * manual fixes after configuration update * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Anthony Sottile --- .pre-commit-config.yaml | 20 +++++----- doc/en/doctest.rst | 2 +- doc/en/writing_plugins.rst | 3 +- doc/en/xunit_setup.rst | 14 +++---- scripts/prepare-release-pr.py | 10 ++++- scripts/release-on-comment.py | 5 ++- src/_pytest/_code/code.py | 6 +-- src/_pytest/_io/saferepr.py | 7 +++- src/_pytest/cacheprovider.py | 3 +- src/_pytest/capture.py | 10 ++++- src/_pytest/compat.py | 3 +- src/_pytest/config/__init__.py | 39 ++++++++++++++----- src/_pytest/config/findpaths.py | 4 +- src/_pytest/doctest.py | 19 ++++++--- src/_pytest/fixtures.py | 21 +++++++--- src/_pytest/freeze_support.py | 3 +- src/_pytest/hookspec.py | 25 +++++++----- src/_pytest/junitxml.py | 2 +- src/_pytest/logging.py | 6 ++- src/_pytest/main.py | 8 ++-- src/_pytest/mark/__init__.py | 5 ++- src/_pytest/mark/expression.py | 10 +++-- src/_pytest/mark/structures.py | 8 +--- src/_pytest/monkeypatch.py | 14 +++++-- src/_pytest/nodes.py | 5 ++- src/_pytest/pytester.py | 19 ++++++--- src/_pytest/python.py | 11 ++++-- src/_pytest/reports.py | 2 +- src/_pytest/terminal.py | 7 +++- src/_pytest/threadexception.py | 4 +- src/_pytest/tmpdir.py | 5 ++- .../test_compare_recursive_dataclasses.py | 10 ++++- testing/io/test_terminalwriter.py | 15 +++++-- testing/python/approx.py | 3 +- testing/python/metafunc.py | 10 ++++- testing/test_capture.py | 3 +- testing/test_debugging.py | 4 +- testing/test_doctest.py | 4 +- testing/test_mark.py | 13 +++++-- testing/test_mark_expression.py | 18 +++++++-- testing/test_monkeypatch.py | 4 +- testing/test_pluginmanager.py | 4 +- testing/test_runner_xunit.py | 4 +- testing/test_stepwise.py | 7 +++- testing/test_warnings.py | 2 +- 45 files changed, 280 insertions(+), 121 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68cc3273bba..c8e19b283f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,16 @@ repos: - repo: https://github.com/psf/black - rev: 19.10b0 + rev: 20.8b1 hooks: - id: black args: [--safe, --quiet] - repo: https://github.com/asottile/blacken-docs - rev: v1.8.0 + rev: v1.9.1 hooks: - id: blacken-docs - additional_dependencies: [black==19.10b0] + additional_dependencies: [black==20.8b1] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v3.4.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.3 + rev: 3.8.4 hooks: - id: flake8 language_version: python3 @@ -29,23 +29,21 @@ repos: - flake8-typing-imports==1.9.0 - flake8-docstrings==1.5.0 - repo: https://github.com/asottile/reorder_python_imports - rev: v2.3.5 + rev: v2.3.6 hooks: - id: reorder-python-imports args: ['--application-directories=.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.7.2 + rev: v2.7.4 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.11.0 + rev: v1.16.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/pygrep-hooks - rev: v1.6.0 + rev: v1.7.0 hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index f8d010679f0..486868bb806 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -48,7 +48,7 @@ and functions, including from test modules: # content of mymodule.py def something(): - """ a doctest in a docstring + """a doctest in a docstring >>> something() 42 """ diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 908366d5290..f53f561cfad 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -762,8 +762,7 @@ declaring the hook functions directly in your plugin module, for example: """Simple plugin to defer pytest-xdist hook functions.""" def pytest_testnodedown(self, node, error): - """standard xdist hook function. - """ + """standard xdist hook function.""" def pytest_configure(config): diff --git a/doc/en/xunit_setup.rst b/doc/en/xunit_setup.rst index 8b3366f62ae..4fea863be63 100644 --- a/doc/en/xunit_setup.rst +++ b/doc/en/xunit_setup.rst @@ -36,7 +36,7 @@ which will usually be called once for all the functions: def teardown_module(module): - """ teardown any state that was previously setup with a setup_module + """teardown any state that was previously setup with a setup_module method. """ @@ -52,14 +52,14 @@ and after all test methods of the class are called: @classmethod def setup_class(cls): - """ setup any state specific to the execution of the given class (which + """setup any state specific to the execution of the given class (which usually contains tests). """ @classmethod def teardown_class(cls): - """ teardown any state that was previously setup with a call to + """teardown any state that was previously setup with a call to setup_class. """ @@ -71,13 +71,13 @@ Similarly, the following methods are called around each method invocation: .. code-block:: python def setup_method(self, method): - """ setup any state tied to the execution of the given method in a + """setup any state tied to the execution of the given method in a class. setup_method is invoked for every test method of a class. """ def teardown_method(self, method): - """ teardown any state that was previously setup with a setup_method + """teardown any state that was previously setup with a setup_method call. """ @@ -89,13 +89,13 @@ you can also use the following functions to implement fixtures: .. code-block:: python def setup_function(function): - """ setup any state tied to the execution of the given function. + """setup any state tied to the execution of the given function. Invoked for every test function in the module. """ def teardown_function(function): - """ teardown any state that was previously setup with a setup_function + """teardown any state that was previously setup with a setup_function call. """ diff --git a/scripts/prepare-release-pr.py b/scripts/prepare-release-pr.py index 538a5af5a41..296de46ea0c 100644 --- a/scripts/prepare-release-pr.py +++ b/scripts/prepare-release-pr.py @@ -90,7 +90,10 @@ def prepare_release_pr(base_branch: str, is_major: bool, token: str) -> None: cmdline = ["tox", "-e", "release", "--", version, "--skip-check-links"] print("Running", " ".join(cmdline)) run( - cmdline, 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" @@ -105,7 +108,10 @@ def prepare_release_pr(base_branch: str, is_major: bool, token: str) -> None: body = PR_BODY.format(version=version) repo = login(token) pr = repo.create_pull( - f"Prepare release {version}", base=base_branch, head=release_branch, body=body, + f"Prepare release {version}", + base=base_branch, + head=release_branch, + body=body, ) print(f"Pull request {Fore.CYAN}{pr.url}{Fore.RESET} created.") diff --git a/scripts/release-on-comment.py b/scripts/release-on-comment.py index 44431a4fc3f..f8af9c0fc83 100644 --- a/scripts/release-on-comment.py +++ b/scripts/release-on-comment.py @@ -153,7 +153,10 @@ def trigger_release(payload_path: Path, token: str) -> None: cmdline = ["tox", "-e", "release", "--", version, "--skip-check-links"] print("Running", " ".join(cmdline)) run( - cmdline, 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" diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 043a23a79af..b8521756067 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -272,9 +272,9 @@ def ishidden(self) -> bool: Mostly for internal use. """ - tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = ( - False - ) + tbh: Union[ + bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool] + ] = False 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 diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 5eb1e088905..440b8cbbb54 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -107,7 +107,12 @@ def _format( if objid in context or p is None: # Type ignored because _format is private. super()._format( # type: ignore[misc] - object, stream, indent, allowance, context, level, + object, + stream, + indent, + allowance, + context, + level, ) return diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 03acd03109e..480319c03b4 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -219,7 +219,8 @@ def pytest_make_collect_report(self, collector: nodes.Collector): # Sort any lf-paths to the beginning. lf_paths = self.lfplugin._last_failed_paths res.result = sorted( - res.result, key=lambda x: 0 if Path(str(x.fspath)) in lf_paths else 1, + res.result, + key=lambda x: 0 if Path(str(x.fspath)) in lf_paths else 1, ) return diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 086302658cb..355f42591a7 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -556,7 +556,11 @@ def __init__(self, in_, out, err) -> None: def __repr__(self) -> str: return "".format( - self.out, self.err, self.in_, self._state, self._in_suspended, + self.out, + self.err, + self.in_, + self._state, + self._in_suspended, ) def start_capturing(self) -> None: @@ -843,7 +847,9 @@ def __init__( def _start(self) -> None: if self._capture is None: self._capture = MultiCapture( - in_=None, out=self.captureclass(1), err=self.captureclass(2), + in_=None, + out=self.captureclass(1), + err=self.captureclass(2), ) self._capture.start_capturing() diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index c7f86ea9c0a..0b87c7bbc08 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -143,7 +143,8 @@ def getfuncargnames( parameters = signature(function).parameters except (ValueError, TypeError) as e: fail( - f"Could not determine arguments of {function!r}: {e}", pytrace=False, + f"Could not determine arguments of {function!r}: {e}", + pytrace=False, ) arg_names = tuple( diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 760b0f55c7b..c029c29a3a2 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -104,7 +104,9 @@ class ExitCode(enum.IntEnum): class ConftestImportFailure(Exception): def __init__( - self, path: Path, excinfo: Tuple[Type[Exception], Exception, TracebackType], + self, + path: Path, + excinfo: Tuple[Type[Exception], Exception, TracebackType], ) -> None: super().__init__(path, excinfo) self.path = path @@ -269,7 +271,9 @@ def get_config( config = Config( pluginmanager, invocation_params=Config.InvocationParams( - args=args or (), plugins=plugins, dir=Path.cwd(), + args=args or (), + plugins=plugins, + dir=Path.cwd(), ), ) @@ -364,7 +368,10 @@ def __init__(self) -> None: encoding: str = getattr(err, "encoding", "utf8") try: err = open( - os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding, + os.dup(err.fileno()), + mode=err.mode, + buffering=1, + encoding=encoding, ) except Exception: pass @@ -516,7 +523,9 @@ def _try_load_conftest( @lru_cache(maxsize=128) def _getconftestmodules( - self, path: Path, importmode: Union[str, ImportMode], + self, + path: Path, + importmode: Union[str, ImportMode], ) -> List[types.ModuleType]: if self._noconftest: return [] @@ -541,7 +550,10 @@ def _getconftestmodules( return clist def _rget_with_confmod( - self, name: str, path: Path, importmode: Union[str, ImportMode], + self, + name: str, + path: Path, + importmode: Union[str, ImportMode], ) -> Tuple[types.ModuleType, Any]: modules = self._getconftestmodules(path, importmode) for mod in reversed(modules): @@ -552,7 +564,9 @@ def _rget_with_confmod( raise KeyError(name) def _importconftest( - self, conftestpath: Path, importmode: Union[str, ImportMode], + self, + conftestpath: Path, + 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 @@ -590,7 +604,9 @@ def _importconftest( return mod def _check_non_top_pytest_plugins( - self, mod: types.ModuleType, conftestpath: Path, + self, + mod: types.ModuleType, + conftestpath: Path, ) -> None: if ( hasattr(mod, "pytest_plugins") @@ -1227,7 +1243,11 @@ def _checkversion(self) -> None: if Version(minver) > Version(pytest.__version__): raise pytest.UsageError( "%s: 'minversion' requires pytest-%s, actual pytest-%s'" - % (self.inipath, minver, pytest.__version__,) + % ( + self.inipath, + minver, + pytest.__version__, + ) ) def _validate_config_options(self) -> None: @@ -1502,7 +1522,8 @@ def _warn_about_missing_assertion(self, mode: str) -> None: "(are you using python -O?)\n" ) self.issue_config_time_warning( - PytestConfigWarning(warning_text), stacklevel=3, + PytestConfigWarning(warning_text), + stacklevel=3, ) def _warn_about_skipped_plugins(self) -> None: diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 2edf54536ba..05f21ece5d4 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -83,9 +83,7 @@ def make_scalar(v: object) -> Union[str, List[str]]: def locate_config( args: Iterable[Path], -) -> Tuple[ - Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]], -]: +) -> Tuple[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).""" config_names = [ diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 24f8882579b..255ca80b913 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -121,7 +121,9 @@ def pytest_unconfigure() -> None: def pytest_collect_file( - fspath: Path, path: py.path.local, parent: Collector, + fspath: Path, + path: py.path.local, + parent: Collector, ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: config = parent.config if fspath.suffix == ".py": @@ -193,7 +195,11 @@ def __init__( self.continue_on_failure = continue_on_failure def report_failure( - self, out, test: "doctest.DocTest", example: "doctest.Example", got: str, + self, + out, + test: "doctest.DocTest", + example: "doctest.Example", + got: str, ) -> None: failure = doctest.DocTestFailure(test, example, got) if self.continue_on_failure: @@ -303,13 +309,14 @@ def _disable_output_capturing_for_darwin(self) -> None: # TODO: Type ignored -- breaks Liskov Substitution. def repr_failure( # type: ignore[override] - self, excinfo: ExceptionInfo[BaseException], + self, + excinfo: ExceptionInfo[BaseException], ) -> Union[str, TerminalRepr]: import doctest failures: Optional[ Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]] - ] = (None) + ] = None if isinstance( excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException) ): @@ -510,7 +517,9 @@ def _find_lineno(self, obj, source_lines): obj = getattr(obj, "fget", obj) # Type ignored because this is a private function. return doctest.DocTestFinder._find_lineno( # type: ignore - self, obj, source_lines, + self, + obj, + source_lines, ) def _find( diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d3ec1296ab0..53f33d3e13d 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -238,7 +238,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 - the specified scope. """ + the specified scope.""" assert scopenum < scopenum_function # function try: callspec = item.callspec # type: ignore[attr-defined] @@ -443,7 +443,7 @@ def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None: fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2index: Dict[str, int] = {} - self._fixturemanager: FixtureManager = (pyfuncitem.session._fixturemanager) + self._fixturemanager: FixtureManager = pyfuncitem.session._fixturemanager @property def fixturenames(self) -> List[str]: @@ -700,7 +700,10 @@ def _schedule_finalizers( ) def _check_scope( - self, argname: str, invoking_scope: "_Scope", requested_scope: "_Scope", + self, + argname: str, + invoking_scope: "_Scope", + requested_scope: "_Scope", ) -> None: if argname == "request": return @@ -907,7 +910,8 @@ def toterminal(self, tw: TerminalWriter) -> None: ) for line in lines[1:]: tw.line( - f"{FormattedExcinfo.flow_marker} {line.strip()}", red=True, + f"{FormattedExcinfo.flow_marker} {line.strip()}", + red=True, ) tw.line() tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1)) @@ -1167,7 +1171,8 @@ def _params_converter( def wrap_function_to_error_out_if_called_directly( - function: _FixtureFunction, fixture_marker: "FixtureFunctionMarker", + 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.""" @@ -1332,7 +1337,11 @@ def fixture( ``@pytest.fixture(name='')``. """ fixture_marker = FixtureFunctionMarker( - scope=scope, params=params, autouse=autouse, ids=ids, name=name, + scope=scope, + params=params, + autouse=autouse, + ids=ids, + name=name, ) # Direct decoration. diff --git a/src/_pytest/freeze_support.py b/src/_pytest/freeze_support.py index 8b93ed5f7f8..69b7d59ff69 100644 --- a/src/_pytest/freeze_support.py +++ b/src/_pytest/freeze_support.py @@ -18,7 +18,8 @@ def freeze_includes() -> List[str]: def _iter_all_modules( - package: Union[str, types.ModuleType], prefix: str = "", + package: Union[str, types.ModuleType], + prefix: str = "", ) -> Iterator[str]: """Iterate over the names of all modules that can be found in the given package, recursively. diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 22bebf5b783..41c12a2ccd8 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -540,7 +540,8 @@ def pytest_runtest_logreport(report: "TestReport") -> None: @hookspec(firstresult=True) def pytest_report_to_serializable( - config: "Config", report: Union["CollectReport", "TestReport"], + config: "Config", + report: Union["CollectReport", "TestReport"], ) -> Optional[Dict[str, Any]]: """Serialize the given report object into a data structure suitable for sending over the wire, e.g. converted to JSON.""" @@ -548,7 +549,8 @@ def pytest_report_to_serializable( @hookspec(firstresult=True) def pytest_report_from_serializable( - config: "Config", data: Dict[str, Any], + config: "Config", + data: Dict[str, Any], ) -> Optional[Union["CollectReport", "TestReport"]]: """Restore a report object previously serialized with pytest_report_to_serializable().""" @@ -597,7 +599,8 @@ 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. @@ -701,7 +704,10 @@ def pytest_report_header( def pytest_report_collectionfinish( - config: "Config", startpath: Path, startdir: py.path.local, items: Sequence["Item"], + config: "Config", + startpath: Path, + startdir: py.path.local, + items: Sequence["Item"], ) -> Union[str, List[str]]: """Return a string or list of strings to be displayed after collection has finished successfully. @@ -731,9 +737,7 @@ def pytest_report_collectionfinish( @hookspec(firstresult=True) def pytest_report_teststatus( report: Union["CollectReport", "TestReport"], config: "Config" -) -> Tuple[ - str, str, Union[str, Mapping[str, bool]], -]: +) -> Tuple[str, str, Union[str, Mapping[str, bool]]]: """Return result-category, shortletter and verbose word for status reporting. @@ -758,7 +762,9 @@ def pytest_report_teststatus( def pytest_terminal_summary( - terminalreporter: "TerminalReporter", exitstatus: "ExitCode", config: "Config", + terminalreporter: "TerminalReporter", + exitstatus: "ExitCode", + config: "Config", ) -> None: """Add a section to terminal summary reporting. @@ -865,7 +871,8 @@ def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]: def pytest_internalerror( - excrepr: "ExceptionRepr", excinfo: "ExceptionInfo[BaseException]", + excrepr: "ExceptionRepr", + excinfo: "ExceptionInfo[BaseException]", ) -> Optional[bool]: """Called for internal errors. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index c4761cd3b87..690fd976ca9 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -486,7 +486,7 @@ def __init__( ) self.node_reporters: Dict[ Tuple[Union[str, TestReport], object], _NodeReporter - ] = ({}) + ] = {} self.node_reporters_ordered: List[_NodeReporter] = [] self.global_properties: List[Tuple[str, str]] = [] diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 2e4847328ab..e0d71c7eb54 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -685,9 +685,11 @@ def pytest_runtest_logreport(self) -> None: def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]: """Implement the internals of the pytest_runtest_xxx() hooks.""" with catching_logs( - self.caplog_handler, level=self.log_level, + self.caplog_handler, + level=self.log_level, ) as caplog_handler, catching_logs( - self.report_handler, level=self.log_level, + self.report_handler, + level=self.log_level, ) as report_handler: caplog_handler.reset() report_handler.reset() diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 79afdde6155..5036601f9bb 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -116,7 +116,9 @@ def pytest_addoption(parser: Parser) -> None: 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.", + "--strict", + action="store_true", + help="(deprecated) alias to --strict-markers.", ) group._addoption( "-c", @@ -656,11 +658,11 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # Keep track of any collected nodes in here, so we don't duplicate fixtures. node_cache1: Dict[Path, Sequence[nodes.Collector]] = {} - node_cache2: Dict[Tuple[Type[nodes.Collector], Path], nodes.Collector] = ({}) + node_cache2: Dict[Tuple[Type[nodes.Collector], Path], nodes.Collector] = {} # Keep track of any collected collectors in matchnodes paths, so they # are not collected more than once. - matchnodes_cache: 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: Dict[str, Package] = {} diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 329a11c4ae8..97fb36ef73b 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -56,7 +56,10 @@ def param( @pytest.mark.parametrize( "test_input,expected", - [("3+5", 8), pytest.param("6*9", 42, marks=pytest.mark.xfail),], + [ + ("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/mark/expression.py b/src/_pytest/mark/expression.py index dc3991b10c4..2e7dcf93cd4 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -102,7 +102,8 @@ def lex(self, input: str) -> Iterator[Token]: pos += len(value) else: raise ParseError( - pos + 1, 'unexpected character "{}"'.format(input[pos]), + pos + 1, + 'unexpected character "{}"'.format(input[pos]), ) yield Token(TokenType.EOF, "", pos) @@ -120,7 +121,8 @@ 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, + " OR ".join(type.value for type in expected), + self.current.type.value, ), ) @@ -204,7 +206,9 @@ def compile(self, input: str) -> "Expression": """ astexpr = expression(Scanner(input)) code: types.CodeType = compile( - astexpr, filename="", mode="eval", + astexpr, + filename="", + mode="eval", ) return Expression(code) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index ae6920735a2..d2f21e1684d 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -460,15 +460,11 @@ def __call__( # type: ignore[override] ... class _UsefixturesMarkDecorator(MarkDecorator): - def __call__( # type: ignore[override] - self, *fixtures: str - ) -> MarkDecorator: + def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override] ... class _FilterwarningsMarkDecorator(MarkDecorator): - def __call__( # type: ignore[override] - self, *filters: str - ) -> MarkDecorator: + def __call__(self, *filters: str) -> MarkDecorator: # type: ignore[override] ... diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index d012b8a535a..ffef87173a8 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -124,7 +124,7 @@ class MonkeyPatch: def __init__(self) -> None: self._setattr: List[Tuple[object, str, object]] = [] - self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = ([]) + self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = [] self._cwd: Optional[str] = None self._savesyspath: Optional[List[str]] = None @@ -157,13 +157,21 @@ def test_partial(monkeypatch): @overload def setattr( - self, target: str, name: object, value: Notset = ..., raising: bool = ..., + self, + target: str, + name: object, + value: Notset = ..., + raising: bool = ..., ) -> None: ... @overload def setattr( - self, target: object, name: str, value: object, raising: bool = ..., + self, + target: object, + name: str, + value: object, + raising: bool = ..., ) -> None: ... diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index c6eb49dec4a..2a96d55ad05 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -231,7 +231,10 @@ def warn(self, warning: Warning) -> None: path, lineno = get_fslocation_from_item(self) assert lineno is not None warnings.warn_explicit( - warning, category=None, filename=str(path), lineno=lineno + 1, + warning, + category=None, + filename=str(path), + lineno=lineno + 1, ) # Methods for ordering nodes. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 0d1f8f278f9..4544d2c2bbb 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -291,13 +291,15 @@ def getcall(self, name: str) -> ParsedCall: @overload def getreports( - self, names: "Literal['pytest_collectreport']", + self, + names: "Literal['pytest_collectreport']", ) -> Sequence[CollectReport]: ... @overload def getreports( - self, names: "Literal['pytest_runtest_logreport']", + self, + names: "Literal['pytest_runtest_logreport']", ) -> Sequence[TestReport]: ... @@ -354,13 +356,15 @@ def matchreport( @overload def getfailures( - self, names: "Literal['pytest_collectreport']", + self, + names: "Literal['pytest_collectreport']", ) -> Sequence[CollectReport]: ... @overload def getfailures( - self, names: "Literal['pytest_runtest_logreport']", + self, + names: "Literal['pytest_runtest_logreport']", ) -> Sequence[TestReport]: ... @@ -419,7 +423,10 @@ def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> N outcomes = self.listoutcomes() assertoutcome( - outcomes, passed=passed, skipped=skipped, failed=failed, + outcomes, + passed=passed, + skipped=skipped, + failed=failed, ) def clear(self) -> None: @@ -659,7 +666,7 @@ def __init__( self._request = request self._mod_collections: WeakKeyDictionary[ Collector, List[Union[Item, Collector]] - ] = (WeakKeyDictionary()) + ] = WeakKeyDictionary() if request.function: name: str = request.function.__name__ else: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 31d91853f2f..50ea60c2dff 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1202,7 +1202,9 @@ def _validate_ids( return new_ids def _resolve_arg_value_types( - self, argnames: Sequence[str], indirect: Union[bool, 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 @@ -1240,7 +1242,9 @@ def _resolve_arg_value_types( return valtypes def _validate_if_using_arg_names( - self, argnames: Sequence[str], indirect: Union[bool, 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. @@ -1691,7 +1695,8 @@ def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: # TODO: Type ignored -- breaks Liskov Substitution. def repr_failure( # type: ignore[override] - self, excinfo: ExceptionInfo[BaseException], + self, + excinfo: ExceptionInfo[BaseException], ) -> Union[str, TerminalRepr]: style = self.config.getoption("tbstyle", "auto") if style == "auto": diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 58f12517c5b..bcd40fb362e 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -307,7 +307,7 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": Tuple[str, int, str], str, TerminalRepr, - ] = (None) + ] = None else: if not isinstance(excinfo, ExceptionInfo): outcome = "failed" diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index d3d1a4b666e..eea9214e70f 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -468,7 +468,9 @@ def pytest_internalerror(self, excrepr: ExceptionRepr) -> bool: return True def pytest_warning_recorded( - self, warning_message: warnings.WarningMessage, nodeid: str, + self, + warning_message: warnings.WarningMessage, + nodeid: str, ) -> None: from _pytest.warnings import warning_record_to_str @@ -1306,7 +1308,8 @@ def _get_line_with_reprcrash_message( def _folded_skips( - startpath: Path, skipped: Sequence[CollectReport], + startpath: Path, + skipped: Sequence[CollectReport], ) -> List[Tuple[int, str, Optional[int], str]]: d: Dict[Tuple[str, Optional[int], str], List[CollectReport]] = {} for event in skipped: diff --git a/src/_pytest/threadexception.py b/src/_pytest/threadexception.py index 1c1f62fdb73..d084dc6e6a2 100644 --- a/src/_pytest/threadexception.py +++ b/src/_pytest/threadexception.py @@ -69,7 +69,9 @@ def thread_exception_runtest_hook() -> Generator[None, None, None]: 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, + cm.args.exc_type, + cm.args.exc_value, + cm.args.exc_traceback, ) ) warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg)) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 08c445e2bf8..29c7e19d7b4 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -53,7 +53,10 @@ def __init__( @classmethod def from_config( - cls, config: Config, *, _ispytest: bool = False, + cls, + config: Config, + *, + _ispytest: bool = False, ) -> "TempPathFactory": """Create a factory according to pytest configuration. diff --git a/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py index 167140e16a6..0945790f004 100644 --- a/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py +++ b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py @@ -29,10 +29,16 @@ class C3: def test_recursive_dataclasses(): left = C3( - S(10, "ten"), C2(C(S(1, "one"), S(2, "two")), S(2, "three")), "equal", "left", + S(10, "ten"), + C2(C(S(1, "one"), S(2, "two")), S(2, "three")), + "equal", + "left", ) right = C3( - S(20, "xxx"), C2(C(S(1, "one"), S(2, "yyy")), S(3, "three")), "equal", "right", + S(20, "xxx"), + C2(C(S(1, "one"), S(2, "yyy")), S(3, "three")), + "equal", + "right", ) assert left == right diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py index fac7593eadd..4866c94a558 100644 --- a/testing/io/test_terminalwriter.py +++ b/testing/io/test_terminalwriter.py @@ -258,13 +258,22 @@ def test_combining(self) -> None: id="with markup and code_highlight", ), pytest.param( - True, False, "assert 0\n", id="with markup but no code_highlight", + 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", + 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", + False, + False, + "assert 0\n", + id="neither markup nor code_highlight", ), ], ) diff --git a/testing/python/approx.py b/testing/python/approx.py index e76d6b774d6..db6124e3914 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -514,7 +514,8 @@ def test_foo(): ) def test_expected_value_type_error(self, x, name): with pytest.raises( - TypeError, match=fr"pytest.approx\(\) does not support nested {name}:", + TypeError, + match=fr"pytest.approx\(\) does not support nested {name}:", ): approx(x) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 58a902a3a59..6577ff18ebf 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -514,7 +514,10 @@ def getini(self, name): ] for config, expected in values: result = idmaker( - ("a",), [pytest.param("string")], idfn=lambda _: "ação", config=config, + ("a",), + [pytest.param("string")], + idfn=lambda _: "ação", + config=config, ) assert result == [expected] @@ -546,7 +549,10 @@ def getini(self, name): ] for config, expected in values: result = idmaker( - ("a",), [pytest.param("string")], ids=["ação"], config=config, + ("a",), + [pytest.param("string")], + ids=["ação"], + config=config, ) assert result == [expected] diff --git a/testing/test_capture.py b/testing/test_capture.py index 3a5c617fe5a..4d89f0b9e40 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1431,7 +1431,8 @@ def test_capattr(): @pytest.mark.skipif( - not sys.platform.startswith("win"), reason="only 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_debugging.py b/testing/test_debugging.py index e1b57299d25..9cdd411667d 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -877,7 +877,9 @@ def test_pdb_custom_cls_without_pdb( assert custom_pdb_calls == [] def test_pdb_custom_cls_with_set_trace( - self, pytester: Pytester, monkeypatch: MonkeyPatch, + self, + pytester: Pytester, + monkeypatch: MonkeyPatch, ) -> None: pytester.makepyfile( custom_pdb=""" diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 08d0aacf68c..b63665349a1 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -69,7 +69,9 @@ def my_func(): @pytest.mark.parametrize("filename", ["__init__", "whatever"]) def test_collect_module_two_doctest_no_modulelevel( - self, pytester: Pytester, filename: str, + self, + pytester: Pytester, + filename: str, ) -> None: path = pytester.makepyfile( **{ diff --git a/testing/test_mark.py b/testing/test_mark.py index 5f4b3e063e4..420faf91ec9 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -356,8 +356,14 @@ def test_func(arg): "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",), + ( + "(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", @@ -863,7 +869,8 @@ def test_one(): assert passed + skipped + failed == 0 @pytest.mark.parametrize( - "keyword", ["__", "+", ".."], + "keyword", + ["__", "+", ".."], ) def test_no_magic_values(self, pytester: Pytester, keyword: str) -> None: """Make sure the tests do not match on magic values, diff --git a/testing/test_mark_expression.py b/testing/test_mark_expression.py index faca02d9330..d37324f51cd 100644 --- a/testing/test_mark_expression.py +++ b/testing/test_mark_expression.py @@ -70,7 +70,11 @@ def test_syntax_oddeties(expr: str, expected: bool) -> None: ("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",), + ( + " (", + 3, + "expected not OR left parenthesis OR identifier; got end of input", + ), ( ")", 1, @@ -81,7 +85,11 @@ def test_syntax_oddeties(expr: str, expected: bool) -> None: 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", + 4, + "expected not OR left parenthesis OR identifier; got end of input", + ), ( "not not", 8, @@ -98,7 +106,11 @@ def test_syntax_oddeties(expr: str, expected: bool) -> None: 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 and or", + 11, + "expected not OR left parenthesis OR identifier; got or", + ), ("ident ident", 7, "expected end of input; got identifier"), ), ) diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 0b97a0e5adb..95521818021 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -348,7 +348,9 @@ class SampleInherit(Sample): @pytest.mark.parametrize( - "Sample", [Sample, SampleInherit], ids=["new", "new-inherit"], + "Sample", + [Sample, SampleInherit], + ids=["new", "new-inherit"], ) def test_issue156_undo_staticmethod(Sample: Type[Sample]) -> None: monkeypatch = MonkeyPatch() diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index a5282a50795..9835b24a046 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -346,7 +346,9 @@ def test_import_plugin_dotted_name( assert mod.x == 3 def test_consider_conftest_deps( - self, pytester: Pytester, pytestpm: PytestPluginManager, + self, + pytester: Pytester, + pytestpm: PytestPluginManager, ) -> None: mod = import_path(pytester.makepyfile("pytest_plugins='xyz'")) with pytest.raises(ImportError): diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index e90d761f633..e077ac41e2c 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -242,7 +242,9 @@ def test_function2(hello): @pytest.mark.parametrize("arg", ["", "arg"]) def test_setup_teardown_function_level_with_optional_argument( - pytester: Pytester, monkeypatch, arg: str, + pytester: Pytester, + monkeypatch, + arg: str, ) -> None: """Parameter to setup/teardown xunit-style functions parameter is now optional (#1728).""" import sys diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index ff2ec16b707..85489fce803 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -138,7 +138,12 @@ def test_fail_and_continue_with_stepwise(stepwise_pytester: Pytester) -> None: @pytest.mark.parametrize("stepwise_skip", ["--stepwise-skip", "--sw-skip"]) 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", + "-v", + "--strict-markers", + "--stepwise", + stepwise_skip, + "--fail", + "--fail-last", ) assert _strip_resource_warnings(result.stderr.lines) == [] diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 574f3f1ec02..8c9c227b26a 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -662,7 +662,7 @@ def capwarn(self, pytester: Pytester): class CapturedWarnings: captured: List[ Tuple[warnings.WarningMessage, Optional[Tuple[str, int, str]]] - ] = ([]) + ] = [] @classmethod def pytest_warning_recorded(cls, warning_message, when, nodeid, location): From 48c9a96a03261e7cfa5aad0367a9186d9032904a Mon Sep 17 00:00:00 2001 From: Anton <44246099+antonblr@users.noreply.github.com> Date: Wed, 30 Dec 2020 19:00:37 -0800 Subject: [PATCH 0365/2846] Fix failing staticmethod tests if they are inherited (#8205) * Fix failing staticmethod tests if they are inherited * add comments, set default=None --- AUTHORS | 1 + changelog/8061.bugfix.rst | 1 + src/_pytest/compat.py | 7 ++++++- testing/python/fixtures.py | 14 ++++++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 changelog/8061.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 20798f3093d..abac9f010a1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -29,6 +29,7 @@ Andy Freeland Anthon van der Neut Anthony Shaw Anthony Sottile +Anton Grinevich Anton Lodder Antony Lee Arel Cordero diff --git a/changelog/8061.bugfix.rst b/changelog/8061.bugfix.rst new file mode 100644 index 00000000000..2c8980fb34e --- /dev/null +++ b/changelog/8061.bugfix.rst @@ -0,0 +1 @@ +Fixed failing staticmethod test cases if they are inherited from a parent test class. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 0b87c7bbc08..b354fcb3f63 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -163,7 +163,12 @@ def getfuncargnames( # it's passed as an unbound method or function, remove the first # parameter name. if is_method or ( - cls and not isinstance(cls.__dict__.get(name, None), staticmethod) + # Not using `getattr` because we don't want to resolve the staticmethod. + # Not using `cls.__dict__` because we want to check the entire MRO. + cls + and not isinstance( + inspect.getattr_static(cls, name, default=None), staticmethod + ) ): arg_names = arg_names[1:] # Remove any names that will be replaced with mocks. diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 862a65abe10..12340e690eb 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -59,6 +59,20 @@ def static(arg1, arg2, x=1): assert getfuncargnames(A.static, cls=A) == ("arg1", "arg2") +def test_getfuncargnames_staticmethod_inherited() -> None: + """Test getfuncargnames for inherited staticmethods (#8061)""" + + class A: + @staticmethod + def static(arg1, arg2, x=1): + raise NotImplementedError() + + class B(A): + pass + + assert getfuncargnames(B.static, cls=B) == ("arg1", "arg2") + + def test_getfuncargnames_partial(): """Check getfuncargnames for methods defined with functools.partial (#5701)""" import functools From 5f11a35b99e3606b003f695b908496c363796074 Mon Sep 17 00:00:00 2001 From: mefmund Date: Thu, 31 Dec 2020 19:25:44 +0100 Subject: [PATCH 0366/2846] Add missing fixture (#8207) Co-authored-by: Florian Bruhin Co-authored-by: Bruno Oliveira --- doc/en/fixture.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 752385adc89..a629bb7d47f 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -879,9 +879,9 @@ Here's what that might look like: admin_client.delete_user(user) - def test_email_received(receiving_user, email): + def test_email_received(sending_user, receiving_user, email): email = Email(subject="Hey!", body="How's it going?") - sending_user.send_email(_email, receiving_user) + sending_user.send_email(email, receiving_user) assert email in receiving_user.inbox Because ``receiving_user`` is the last fixture to run during setup, it's the first to run From 7d306e9e86f114a812bcf234211ff7b8533d11f9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 30 Dec 2020 14:37:00 +0200 Subject: [PATCH 0367/2846] reports: BaseReport.{passed,failed,skipped} more friendly to mypy Not smart enough to understand the previous code. --- src/_pytest/reports.py | 17 +++++++++++++---- testing/test_terminal.py | 6 +++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index bcd40fb362e..c78d5423974 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -65,6 +65,7 @@ class BaseReport: ] sections: List[Tuple[str, str]] nodeid: str + outcome: "Literal['passed', 'failed', 'skipped']" def __init__(self, **kw: Any) -> None: self.__dict__.update(kw) @@ -141,9 +142,17 @@ def capstderr(self) -> str: content for (prefix, content) in self.get_sections("Captured stderr") ) - passed = property(lambda x: x.outcome == "passed") - failed = property(lambda x: x.outcome == "failed") - skipped = property(lambda x: x.outcome == "skipped") + @property + def passed(self) -> bool: + return self.outcome == "passed" + + @property + def failed(self) -> bool: + return self.outcome == "failed" + + @property + def skipped(self) -> bool: + return self.outcome == "skipped" @property def fspath(self) -> str: @@ -348,7 +357,7 @@ class CollectReport(BaseReport): def __init__( self, nodeid: str, - outcome: "Literal['passed', 'skipped', 'failed']", + outcome: "Literal['passed', 'failed', 'skipped']", longrepr, result: Optional[List[Union[Item, Collector]]], sections: Iterable[Tuple[str, str]] = (), diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 6d0a23fe0f1..e536f70989c 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2230,19 +2230,19 @@ class X: ev1 = cast(CollectReport, X()) ev1.when = "execute" - ev1.skipped = True + ev1.skipped = True # type: ignore[misc] ev1.longrepr = longrepr ev2 = cast(CollectReport, X()) ev2.when = "execute" ev2.longrepr = longrepr - ev2.skipped = True + ev2.skipped = True # type: ignore[misc] # ev3 might be a collection report ev3 = cast(CollectReport, X()) ev3.when = "collect" ev3.longrepr = longrepr - ev3.skipped = True + ev3.skipped = True # type: ignore[misc] values = _folded_skips(Path.cwd(), [ev1, ev2, ev3]) assert len(values) == 1 From 73c410523097a699559ce5ae12b9caf9c50972fc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 30 Dec 2020 14:44:43 +0200 Subject: [PATCH 0368/2846] reports: improve a type annotation --- src/_pytest/reports.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index c78d5423974..d2d7115b2e5 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -358,7 +358,9 @@ def __init__( self, nodeid: str, outcome: "Literal['passed', 'failed', 'skipped']", - longrepr, + longrepr: Union[ + None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr + ], result: Optional[List[Union[Item, Collector]]], sections: Iterable[Tuple[str, str]] = (), **extra, From 20c59e3aa4446faf2fd1ac04322e8076c2aebe3f Mon Sep 17 00:00:00 2001 From: sousajf1 Date: Fri, 1 Jan 2021 15:25:11 +0000 Subject: [PATCH 0369/2846] pytest-dev#8204 migrate some tests to tmp_path fixture (#8209) migrating some tests from tmpdir to tmp_path fixture --- testing/code/test_excinfo.py | 6 +++--- testing/test_argcomplete.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 19c888403f2..e6a9cbaf737 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -364,12 +364,12 @@ def test_excinfo_no_sourcecode(): assert s == " File '':1 in \n ???\n" -def test_excinfo_no_python_sourcecode(tmpdir): +def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None: # XXX: simplified locally testable version - tmpdir.join("test.txt").write("{{ h()}}:") + tmp_path.joinpath("test.txt").write_text("{{ h()}}:") jinja2 = pytest.importorskip("jinja2") - loader = jinja2.FileSystemLoader(str(tmpdir)) + loader = jinja2.FileSystemLoader(str(tmp_path)) env = jinja2.Environment(loader=loader) template = env.get_template("test.txt") excinfo = pytest.raises(ValueError, template.render, h=h) diff --git a/testing/test_argcomplete.py b/testing/test_argcomplete.py index a3224be5126..8c10e230b0c 100644 --- a/testing/test_argcomplete.py +++ b/testing/test_argcomplete.py @@ -1,7 +1,9 @@ import subprocess import sys +from pathlib import Path import pytest +from _pytest.monkeypatch import MonkeyPatch # Test for _argcomplete but not specific for any application. @@ -65,19 +67,22 @@ def __call__(self, prefix, **kwargs): class TestArgComplete: @pytest.mark.skipif("sys.platform in ('win32', 'darwin')") - def test_compare_with_compgen(self, tmpdir): + def test_compare_with_compgen( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: from _pytest._argcomplete import FastFilesCompleter ffc = FastFilesCompleter() fc = FilesCompleter() - with tmpdir.as_cwd(): - assert equal_with_bash("", ffc, fc, out=sys.stdout) + monkeypatch.chdir(tmp_path) - tmpdir.ensure("data") + assert equal_with_bash("", ffc, fc, out=sys.stdout) - for x in ["d", "data", "doesnotexist", ""]: - assert equal_with_bash(x, ffc, fc, out=sys.stdout) + tmp_path.cwd().joinpath("data").touch() + + for x in ["d", "data", "doesnotexist", ""]: + assert equal_with_bash(x, ffc, fc, out=sys.stdout) @pytest.mark.skipif("sys.platform in ('win32', 'darwin')") def test_remove_dir_prefix(self): From ac428f67ebb2469d91476cbe8ec7e10da6f6b106 Mon Sep 17 00:00:00 2001 From: sousajo Date: Fri, 1 Jan 2021 16:55:03 +0000 Subject: [PATCH 0370/2846] pytest-dev#8204 migrate tests on testing/code/test_source to tmp_path --- testing/code/test_source.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 6b8443fd243..083a7911f55 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -17,6 +17,7 @@ from _pytest._code import Frame from _pytest._code import getfslineno from _pytest._code import Source +from _pytest.pathlib import import_path def test_source_str_function() -> None: @@ -285,7 +286,9 @@ def g(): assert lines == ["def f():", " def g():", " pass"] -def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot) -> None: +def test_source_of_class_at_eof_without_newline( + tmpdir, _sys_snapshot, tmp_path: Path +) -> None: # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. source = Source( @@ -295,9 +298,10 @@ def method(self): x = 1 """ ) - path = tmpdir.join("a.py") - path.write(source) - s2 = Source(tmpdir.join("a.py").pyimport().A) + path = tmp_path.joinpath("a.py") + path.write_text(str(source)) + mod: Any = import_path(path) + s2 = Source(mod.A) assert str(source).strip() == str(s2).strip() From b02f1c8ae74d8c5273e3d651539e931af64c34fa Mon Sep 17 00:00:00 2001 From: Hong Xu Date: Fri, 1 Jan 2021 12:21:39 -0800 Subject: [PATCH 0371/2846] DOC: Update multiple references to testdir to pytester In https://docs.pytest.org/en/stable/reference.html#testdir, it is suggested: > New code should avoid using testdir in favor of pytester. Multiple spots in the documents still use testdir and they can be quite confusing (especially the plugin writing guide). --- CONTRIBUTING.rst | 14 +++++++------- doc/en/writing_plugins.rst | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2669cb19509..054f809a818 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -324,20 +324,20 @@ 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 `pytester fixture `_, as a "black-box" test. For example, to ensure a simple test passes you can write: .. code-block:: python - def test_true_assertion(testdir): - testdir.makepyfile( + def test_true_assertion(pytester): + pytester.makepyfile( """ def test_foo(): assert True """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(failed=0, passed=1) @@ -346,14 +346,14 @@ Alternatively, it is possible to make checks based on the actual output of the t .. code-block:: python - def test_true_assertion(testdir): - testdir.makepyfile( + def test_true_assertion(pytester): + pytester.makepyfile( """ def test_foo(): assert False """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*assert False*", "*1 failed*"]) When choosing a file where to write a new test, take a look at the existing files and see if there's diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index f53f561cfad..e9806a6664d 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -337,7 +337,7 @@ testing directory: Alternatively you can invoke pytest with the ``-p pytester`` command line option. -This will allow you to use the :py:class:`testdir <_pytest.pytester.Testdir>` +This will allow you to use the :py:class:`pytester <_pytest.pytester.Pytester>` fixture for testing your plugin code. Let's demonstrate what you can do with the plugin with an example. Imagine we @@ -374,17 +374,17 @@ string value of ``Hello World!`` if we do not supply a value or ``Hello return _hello -Now the ``testdir`` fixture provides a convenient API for creating temporary +Now the ``pytester`` fixture provides a convenient API for creating temporary ``conftest.py`` files and test files. It also allows us to run the tests and return a result object, with which we can assert the tests' outcomes. .. code-block:: python - def test_hello(testdir): + def test_hello(pytester): """Make sure that our plugin works.""" # create a temporary conftest.py file - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -399,7 +399,7 @@ return a result object, with which we can assert the tests' outcomes. ) # create a temporary pytest test file - testdir.makepyfile( + pytester.makepyfile( """ def test_hello_default(hello): assert hello() == "Hello World!" @@ -410,7 +410,7 @@ return a result object, with which we can assert the tests' outcomes. ) # run all tests with pytest - result = testdir.runpytest() + result = pytester.runpytest() # check that all 4 tests passed result.assert_outcomes(passed=4) @@ -430,9 +430,9 @@ Additionally it is possible to copy examples for an example folder before runnin # content of test_example.py - def test_plugin(testdir): - testdir.copy_example("test_example.py") - testdir.runpytest("-k", "test_example") + def test_plugin(pytester): + pytester.copy_example("test_example.py") + pytester.runpytest("-k", "test_example") def test_example(): From 6c575ad8c8aa298a8e8d11612d837c51880d528a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 14:42:14 +0200 Subject: [PATCH 0372/2846] fixtures: simplify FixtureRequest._get_fixturestack() --- src/_pytest/fixtures.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 53f33d3e13d..aed81029f44 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -607,14 +607,11 @@ def _get_active_fixturedef( def _get_fixturestack(self) -> List["FixtureDef[Any]"]: current = self values: List[FixtureDef[Any]] = [] - while 1: - fixturedef = getattr(current, "_fixturedef", None) - if fixturedef is None: - values.reverse() - return values - values.append(fixturedef) - assert isinstance(current, SubRequest) + while isinstance(current, SubRequest): + values.append(current._fixturedef) # type: ignore[has-type] current = current._parent_request + values.reverse() + return values def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: """Create a SubRequest based on "self" and call the execute method From ade253c7906b082add837fbac8c193ec85847fbc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 23:18:17 +0200 Subject: [PATCH 0373/2846] fixtures: type annotate FixtureRequest.keywords --- src/_pytest/fixtures.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index aed81029f44..5bdee3096b1 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -16,6 +16,7 @@ from typing import Iterable from typing import Iterator from typing import List +from typing import MutableMapping from typing import Optional from typing import overload from typing import Sequence @@ -525,9 +526,10 @@ def fspath(self) -> py.path.local: return self._pyfuncitem.fspath # type: ignore @property - def keywords(self): + def keywords(self) -> MutableMapping[str, Any]: """Keywords/markers dictionary for the underlying node.""" - return self.node.keywords + node: nodes.Node = self.node + return node.keywords @property def session(self) -> "Session": From 8ee6d0a8666f91c9c537afacbe3c61f54e342f28 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 26 Nov 2020 14:50:44 +0200 Subject: [PATCH 0374/2846] Always use getfixturemarker() to access _pytestfixturefunction Keep knowledge of how the marker is stored encapsulated in one place. --- src/_pytest/nose.py | 24 +++++++++++++++--------- testing/python/integration.py | 4 +++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/_pytest/nose.py b/src/_pytest/nose.py index bb8f99772ac..de91af85af6 100644 --- a/src/_pytest/nose.py +++ b/src/_pytest/nose.py @@ -2,11 +2,12 @@ from _pytest import python from _pytest import unittest from _pytest.config import hookimpl +from _pytest.fixtures import getfixturemarker from _pytest.nodes import Item @hookimpl(trylast=True) -def pytest_runtest_setup(item): +def pytest_runtest_setup(item) -> None: if is_potential_nosetest(item): if not call_optional(item.obj, "setup"): # Call module level setup if there is no object level one. @@ -15,7 +16,7 @@ def pytest_runtest_setup(item): item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item) -def teardown_nose(item): +def teardown_nose(item) -> None: if is_potential_nosetest(item): if not call_optional(item.obj, "teardown"): call_optional(item.parent.obj, "teardown") @@ -29,11 +30,16 @@ def is_potential_nosetest(item: Item) -> bool: ) -def call_optional(obj, name): +def call_optional(obj: object, name: str) -> bool: method = getattr(obj, name, None) - 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. - method() - return True + if method is None: + return False + is_fixture = getfixturemarker(method) is not None + if is_fixture: + return False + if not callable(method): + return False + # If there are any problems allow the exception to raise rather than + # silently ignoring it. + method() + return True diff --git a/testing/python/integration.py b/testing/python/integration.py index 5dce6bdca28..8576fcee341 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -3,6 +3,7 @@ import pytest from _pytest import runner from _pytest._code import getfslineno +from _pytest.fixtures import getfixturemarker from _pytest.pytester import Pytester @@ -334,7 +335,8 @@ def test_fix(fix): def test_pytestconfig_is_session_scoped() -> None: from _pytest.fixtures import pytestconfig - marker = pytestconfig._pytestfixturefunction # type: ignore + marker = getfixturemarker(pytestconfig) + assert marker is not None assert marker.scope == "session" From 2ff88098a7340142ed2c6d2c090b8c9fac001a5e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 6 Oct 2020 20:19:52 +0300 Subject: [PATCH 0375/2846] python: inline a simple method I don't think it adds much value! --- src/_pytest/python.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 50ea60c2dff..29ebd176bbb 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -922,10 +922,6 @@ def copy(self) -> "CallSpec2": cs._idlist = list(self._idlist) return cs - def _checkargnotcontained(self, arg: str) -> None: - if arg in self.params or arg in self.funcargs: - raise ValueError(f"duplicate {arg!r}") - def getparam(self, name: str) -> object: try: return self.params[name] @@ -947,7 +943,8 @@ def setmulti2( param_index: int, ) -> None: for arg, val in zip(argnames, valset): - self._checkargnotcontained(arg) + if arg in self.params or arg in self.funcargs: + raise ValueError(f"duplicate {arg!r}") valtype_for_arg = valtypes[arg] if valtype_for_arg == "params": self.params[arg] = val From 14b5f5e528e52e22d05f086060c5f0dc08a6b37b Mon Sep 17 00:00:00 2001 From: Hong Xu Date: Sat, 2 Jan 2021 00:34:52 -0800 Subject: [PATCH 0376/2846] DOC: Mark pytest module Pytest document currently does not index the top-level package name `pytest`, which causes some trouble when building documentation that cross-refers to the pytest package via ``:mod:`pytest` ``. --- doc/en/reference.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index bc6c5670a5c..51c52b33ae9 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -3,6 +3,8 @@ API Reference ============= +.. module:: pytest + This page contains the full reference to pytest's API. .. contents:: From 8e00df4c4b62f08df0003e578c581e0b5728e571 Mon Sep 17 00:00:00 2001 From: bengartner Date: Mon, 4 Jan 2021 07:58:11 -0600 Subject: [PATCH 0377/2846] Add dot prefix if file makefile extension is invalid for pathlib (#8222) --- AUTHORS | 1 + changelog/8192.bugfix.rst | 3 +++ src/_pytest/pytester.py | 14 ++++++++++++++ testing/test_pytester.py | 31 +++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 changelog/8192.bugfix.rst diff --git a/AUTHORS b/AUTHORS index abac9f010a1..e75baa8cd92 100644 --- a/AUTHORS +++ b/AUTHORS @@ -40,6 +40,7 @@ Aron Curzon Aviral Verma Aviv Palivoda Barney Gale +Ben Gartner Ben Webb Benjamin Peterson Bernard Pratz diff --git a/changelog/8192.bugfix.rst b/changelog/8192.bugfix.rst new file mode 100644 index 00000000000..5b26ecbe45c --- /dev/null +++ b/changelog/8192.bugfix.rst @@ -0,0 +1,3 @@ +``testdir.makefile` now silently accepts values which don't start with ``.`` to maintain backward compatibility with older pytest versions. + +``pytester.makefile`` now issues a clearer error if the ``.`` is missing in the ``ext`` argument. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 4544d2c2bbb..95b22b3b23e 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -64,6 +64,7 @@ from _pytest.tmpdir import TempPathFactory from _pytest.warning_types import PytestWarning + if TYPE_CHECKING: from typing_extensions import Literal @@ -750,6 +751,11 @@ def _makefile( ) -> Path: items = list(files.items()) + if ext and not ext.startswith("."): + raise ValueError( + f"pytester.makefile expects a file extension, try .{ext} instead of {ext}" + ) + def to_text(s: Union[Any, bytes]) -> str: return s.decode(encoding) if isinstance(s, bytes) else str(s) @@ -1559,6 +1565,14 @@ def finalize(self) -> None: def makefile(self, ext, *args, **kwargs) -> py.path.local: """See :meth:`Pytester.makefile`.""" + if ext and not ext.startswith("."): + # pytester.makefile is going to throw a ValueError in a way that + # testdir.makefile did not, because + # pathlib.Path is stricter suffixes than py.path + # This ext arguments is likely user error, but since testdir has + # allowed this, we will prepend "." as a workaround to avoid breaking + # testdir usage that worked before + ext = "." + ext return py.path.local(str(self._pytester.makefile(ext, *args, **kwargs))) def makeconftest(self, source) -> py.path.local: diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 57d6f4fd9eb..5823d51155c 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -17,6 +17,7 @@ from _pytest.pytester import Pytester from _pytest.pytester import SysModulesSnapshot from _pytest.pytester import SysPathsSnapshot +from _pytest.pytester import Testdir def test_make_hook_recorder(pytester: Pytester) -> None: @@ -816,3 +817,33 @@ def test_makefile_joins_absolute_path(pytester: Pytester) -> None: def test_testtmproot(testdir) -> None: """Check test_tmproot is a py.path attribute for backward compatibility.""" assert testdir.test_tmproot.check(dir=1) + + +def test_testdir_makefile_dot_prefixes_extension_silently( + testdir: Testdir, +) -> None: + """For backwards compat #8192""" + p1 = testdir.makefile("foo.bar", "") + assert ".foo.bar" in str(p1) + + +def test_pytester_makefile_dot_prefixes_extension_with_warning( + pytester: Pytester, +) -> None: + with pytest.raises( + ValueError, + match="pytester.makefile expects a file extension, try .foo.bar instead of foo.bar", + ): + pytester.makefile("foo.bar", "") + + +def test_testdir_makefile_ext_none_raises_type_error(testdir) -> None: + """For backwards compat #8192""" + with pytest.raises(TypeError): + testdir.makefile(None, "") + + +def test_testdir_makefile_ext_empty_string_makes_file(testdir) -> None: + """For backwards compat #8192""" + p1 = testdir.makefile("", "") + assert "test_testdir_makefile" in str(p1) From 65b8391ead4ef3849e6181e7ec899625b78ba992 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 5 Jan 2021 19:39:50 +0100 Subject: [PATCH 0378/2846] doc: Add note about training early bird discount --- doc/en/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index ad2057ff14a..58f6c1d86c7 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -2,7 +2,7 @@ .. sidebar:: Next Open Trainings - - `Professional testing with Python `_, via Python Academy, February 1-3 2021, Leipzig (Germany) and remote. + - `Professional testing with Python `_, via Python Academy, February 1-3 2021, remote and Leipzig (Germany). **Early-bird discount available until January 15th**. Also see `previous talks and blogposts `_. From 78fb97105f38dc286353bbc331a243b6e753fe3c Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 6 Jan 2021 13:33:33 +0100 Subject: [PATCH 0379/2846] Make code.FormattedExcinfo.get_source more defensive When line_index was a large negative number, get_source failed on `source.lines[line_index]`. Use the same dummy Source as with a large positive line_index. --- src/_pytest/_code/code.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index b8521756067..af3bdf0561b 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -721,11 +721,11 @@ def get_source( ) -> List[str]: """Return formatted and marked up source lines.""" lines = [] - if source is None or line_index >= len(source.lines): + if source is not None and line_index < 0: + line_index += len(source.lines) + if source is None or line_index >= len(source.lines) or line_index < 0: source = Source("???") line_index = 0 - if line_index < 0: - line_index += len(source) space_prefix = " " if short: lines.append(space_prefix + source.lines[line_index].strip()) From 80c33c817879ca9cf1eb66e4fa2ff7a01f33bb39 Mon Sep 17 00:00:00 2001 From: Gergely Imreh Date: Fri, 8 Jan 2021 11:43:51 +0000 Subject: [PATCH 0380/2846] Add missing import into example script in documentation --- doc/en/skipping.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 282820545c3..610d3d43bca 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -405,6 +405,7 @@ test instances when using parametrize: .. code-block:: python + import sys import pytest From af78efc7fa903bd7f445c451212ab625c11954cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Jan 2021 03:15:59 +0000 Subject: [PATCH 0381/2846] build(deps): bump pytest-mock in /testing/plugins_integration Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.4.0 to 3.5.1. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.4.0...v3.5.1) 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 b2ca3e3236a..c48a5cfd257 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -6,7 +6,7 @@ pytest-cov==2.10.1 pytest-django==4.1.0 pytest-flakes==4.0.3 pytest-html==3.1.1 -pytest-mock==3.4.0 +pytest-mock==3.5.1 pytest-rerunfailures==9.1.1 pytest-sugar==0.9.4 pytest-trio==0.7.0 From cfa0c3b0d94eea25db8f7de57ce93c52598009db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Jan 2021 03:16:03 +0000 Subject: [PATCH 0382/2846] build(deps): bump django in /testing/plugins_integration Bumps [django](https://github.com/django/django) from 3.1.4 to 3.1.5. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.1.4...3.1.5) 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 b2ca3e3236a..f5036023097 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.4 +django==3.1.5 pytest-asyncio==0.14.0 pytest-bdd==4.0.2 pytest-cov==2.10.1 From 42d5545f42f7f11345913efedf852cbea3753e58 Mon Sep 17 00:00:00 2001 From: Anton <44246099+antonblr@users.noreply.github.com> Date: Wed, 13 Jan 2021 17:02:26 -0800 Subject: [PATCH 0383/2846] unittest: cleanup unexpected success handling (#8231) * unittest: cleanup unexpected success handling * update comment --- src/_pytest/skipping.py | 11 +---------- src/_pytest/unittest.py | 24 ++++++++++++------------ testing/test_unittest.py | 9 +++++++-- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 9aacfecee7a..c7afef5db87 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -234,7 +234,6 @@ def evaluate_xfail_marks(item: Item) -> Optional[Xfail]: skipped_by_mark_key = StoreKey[bool]() # Saves the xfail mark evaluation. Can be refreshed during call if None. xfailed_key = StoreKey[Optional[Xfail]]() -unexpectedsuccess_key = StoreKey[str]() @hookimpl(tryfirst=True) @@ -271,15 +270,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): outcome = yield rep = outcome.get_result() 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] - if reason: - rep.longrepr = f"Unexpected success: {reason}" - else: - rep.longrepr = "Unexpected success" - rep.outcome = "failed" - elif item.config.option.runxfail: + if 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 diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 55f15efe4b7..cc616578b09 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -30,10 +30,10 @@ from _pytest.python import PyCollector from _pytest.runner import CallInfo from _pytest.skipping import skipped_by_mark_key -from _pytest.skipping import unexpectedsuccess_key if TYPE_CHECKING: import unittest + import twisted.trial.unittest from _pytest.fixtures import _Scope @@ -273,9 +273,18 @@ def addExpectedFailure( self._addexcinfo(sys.exc_info()) def addUnexpectedSuccess( - self, testcase: "unittest.TestCase", reason: str = "" + self, + testcase: "unittest.TestCase", + reason: Optional["twisted.trial.unittest.Todo"] = None, ) -> None: - self._store[unexpectedsuccess_key] = reason + msg = "Unexpected success" + if reason: + msg += f": {reason.reason}" + # Preserve unittest behaviour - fail the test. Explicitly not an XPASS. + try: + fail(msg, pytrace=False) + except fail.Exception: + self._addexcinfo(sys.exc_info()) def addSuccess(self, testcase: "unittest.TestCase") -> None: pass @@ -283,15 +292,6 @@ def addSuccess(self, testcase: "unittest.TestCase") -> None: def stopTest(self, testcase: "unittest.TestCase") -> None: pass - def _expecting_failure(self, test_method) -> bool: - """Return True if the given unittest method (or the entire class) is marked - with @expectedFailure.""" - expecting_failure_method = getattr( - test_method, "__unittest_expecting_failure__", False - ) - expecting_failure_class = getattr(self, "__unittest_expecting_failure__", False) - return bool(expecting_failure_class or expecting_failure_method) - def runtest(self) -> None: from _pytest.debugging import maybe_wrap_pytest_function_for_tracing diff --git a/testing/test_unittest.py b/testing/test_unittest.py index feee09286c2..69bafc26d61 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -765,7 +765,8 @@ def test_failing_test_is_xfail(self): @pytest.mark.parametrize("runner", ["pytest", "unittest"]) def test_unittest_expected_failure_for_passing_test_is_fail( - pytester: Pytester, runner + pytester: Pytester, + runner: str, ) -> None: script = pytester.makepyfile( """ @@ -782,7 +783,11 @@ def test_passing_test_is_fail(self): if runner == "pytest": result = pytester.runpytest("-rxX") result.stdout.fnmatch_lines( - ["*MyTestCase*test_passing_test_is_fail*", "*1 failed*"] + [ + "*MyTestCase*test_passing_test_is_fail*", + "Unexpected success", + "*1 failed*", + ] ) else: result = pytester.runpython(script) From addbd3161e37edffebb2c0f6527da49b0515d1a1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 14 Jan 2021 16:51:18 +0200 Subject: [PATCH 0384/2846] nose,fixtures: use the public item API for adding finalizers --- src/_pytest/fixtures.py | 10 +++------- src/_pytest/nose.py | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 5bdee3096b1..43a40a86449 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -543,10 +543,8 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._addfinalizer(finalizer, scope=self.scope) def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None: - colitem = self._getscopeitem(scope) - self._pyfuncitem.session._setupstate.addfinalizer( - finalizer=finalizer, colitem=colitem - ) + item = self._getscopeitem(scope) + item.addfinalizer(finalizer) def applymarker(self, marker: Union[str, MarkDecorator]) -> None: """Apply a marker to a single test function invocation. @@ -694,9 +692,7 @@ def _schedule_finalizers( self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" ) -> None: # If fixture function failed it might have registered finalizers. - self.session._setupstate.addfinalizer( - functools.partial(fixturedef.finish, request=subrequest), subrequest.node - ) + subrequest.node.addfinalizer(lambda: fixturedef.finish(request=subrequest)) def _check_scope( self, diff --git a/src/_pytest/nose.py b/src/_pytest/nose.py index de91af85af6..5bba030a509 100644 --- a/src/_pytest/nose.py +++ b/src/_pytest/nose.py @@ -13,7 +13,7 @@ def pytest_runtest_setup(item) -> None: # 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. - item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item) + item.addfinalizer(lambda: teardown_nose(item)) def teardown_nose(item) -> None: From 096bae6c68840c19bdc97cdfdf99b96dbcb2c427 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 14 Jan 2021 17:05:01 +0200 Subject: [PATCH 0385/2846] unittest: add clarifying comment on unittest.SkipTest -> pytest.skip code I was tempted to remove it, until I figured out why it was there. --- src/_pytest/unittest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index cc616578b09..6a90188cab9 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -343,6 +343,10 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: except AttributeError: pass + # Convert unittest.SkipTest to pytest.skip. + # This is actually only needed for nose, which reuses unittest.SkipTest for + # its own nose.SkipTest. For unittest TestCases, SkipTest is already + # handled internally, and doesn't reach here. unittest = sys.modules.get("unittest") if ( unittest @@ -350,7 +354,6 @@ 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. call2 = CallInfo[None].from_call( lambda: pytest.skip(str(excinfo.value)), call.when ) From 3dde519f53b4480a3b30d58a1c71ca4505ae5ff7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 14 Jan 2021 17:42:38 +0200 Subject: [PATCH 0386/2846] nose: type annotate with some resulting refactoring --- src/_pytest/nose.py | 48 +++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/_pytest/nose.py b/src/_pytest/nose.py index 5bba030a509..16d5224e9fa 100644 --- a/src/_pytest/nose.py +++ b/src/_pytest/nose.py @@ -1,33 +1,35 @@ """Run testsuites written for nose.""" -from _pytest import python -from _pytest import unittest from _pytest.config import hookimpl from _pytest.fixtures import getfixturemarker from _pytest.nodes import Item +from _pytest.python import Function +from _pytest.unittest import TestCaseFunction @hookimpl(trylast=True) -def pytest_runtest_setup(item) -> None: - if is_potential_nosetest(item): - if not call_optional(item.obj, "setup"): - # 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. - item.addfinalizer(lambda: teardown_nose(item)) - - -def teardown_nose(item) -> None: - if is_potential_nosetest(item): - if not call_optional(item.obj, "teardown"): - call_optional(item.parent.obj, "teardown") - - -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( - item, unittest.TestCaseFunction - ) +def pytest_runtest_setup(item: Item) -> None: + if not isinstance(item, Function): + return + # Don't do nose style setup/teardown on direct unittest style classes. + if isinstance(item, TestCaseFunction): + return + + # Capture the narrowed type of item for the teardown closure, + # see https://github.com/python/mypy/issues/2608 + func = item + + if not call_optional(func.obj, "setup"): + # Call module level setup if there is no object level one. + assert func.parent is not None + call_optional(func.parent.obj, "setup") # type: ignore[attr-defined] + + def teardown_nose() -> None: + if not call_optional(func.obj, "teardown"): + assert func.parent is not None + call_optional(func.parent.obj, "teardown") # type: ignore[attr-defined] + + # XXX This implies we only call teardown when setup worked. + func.addfinalizer(teardown_nose) def call_optional(obj: object, name: str) -> bool: From 7f989203ed58119bf63e026cbb99df274c7700d6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 14 Jan 2021 11:58:59 +0200 Subject: [PATCH 0387/2846] Improve way in which skip location is fixed up when skipped by mark When `pytest.skip()` is called inside a test function, the skip location should be reported as the line that made the call, however when `pytest.skip()` is called by the `pytest.mark.skip` and similar mechanisms, the location should be reported at the item's location, because the exact location is some irrelevant internal code. Currently the item-location case is implemented by the caller setting a boolean key on the item's store and the `skipping` plugin checking it and fixing up the location if needed. This is really roundabout IMO and breaks encapsulation. Instead, allow the caller to specify directly on the skip exception whether to use the item's location or not. For now, this is entirely private. --- src/_pytest/outcomes.py | 5 +++++ src/_pytest/reports.py | 7 ++++++- src/_pytest/skipping.py | 18 +----------------- src/_pytest/unittest.py | 6 ++---- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 8f6203fd7fa..756b4098b36 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -58,9 +58,14 @@ def __init__( msg: Optional[str] = None, pytrace: bool = True, allow_module_level: bool = False, + *, + _use_item_location: bool = False, ) -> None: OutcomeException.__init__(self, msg=msg, pytrace=pytrace) self.allow_module_level = allow_module_level + # If true, the skip location is reported as the item's location, + # instead of the place that raises the exception/calls skip(). + self._use_item_location = _use_item_location class Failed(OutcomeException): diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index d2d7115b2e5..303f731ddaa 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -324,7 +324,12 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": elif isinstance(excinfo.value, skip.Exception): outcome = "skipped" r = excinfo._getreprcrash() - longrepr = (str(r.path), r.lineno, r.message) + if excinfo.value._use_item_location: + filename, line = item.reportinfo()[:2] + assert line is not None + longrepr = str(filename), line + 1, r.message + else: + longrepr = (str(r.path), r.lineno, r.message) else: outcome = "failed" if call.when == "call": diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index c7afef5db87..1ad312919ca 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -230,8 +230,6 @@ def evaluate_xfail_marks(item: Item) -> Optional[Xfail]: return None -# Whether skipped due to skip or skipif marks. -skipped_by_mark_key = StoreKey[bool]() # Saves the xfail mark evaluation. Can be refreshed during call if None. xfailed_key = StoreKey[Optional[Xfail]]() @@ -239,9 +237,8 @@ def evaluate_xfail_marks(item: Item) -> Optional[Xfail]: @hookimpl(tryfirst=True) def pytest_runtest_setup(item: Item) -> None: skipped = evaluate_skip_marks(item) - item._store[skipped_by_mark_key] = skipped is not None if skipped: - skip(skipped.reason) + raise skip.Exception(skipped.reason, _use_item_location=True) item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) if xfailed and not item.config.option.runxfail and not xfailed.run: @@ -292,19 +289,6 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): rep.outcome = "passed" rep.wasxfail = xfailed.reason - if ( - item._store.get(skipped_by_mark_key, True) - and rep.skipped - and type(rep.longrepr) is tuple - ): - # 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. - _, _, reason = rep.longrepr - filename, line = item.reportinfo()[:2] - assert line is not None - rep.longrepr = str(filename), line + 1, reason - def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: if hasattr(report, "wasxfail"): diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 6a90188cab9..719eb4e8823 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -29,7 +29,6 @@ from _pytest.python import Function from _pytest.python import PyCollector from _pytest.runner import CallInfo -from _pytest.skipping import skipped_by_mark_key if TYPE_CHECKING: import unittest @@ -150,7 +149,7 @@ def cleanup(*args): def fixture(self, request: FixtureRequest) -> Generator[None, None, None]: if _is_skipped(self): reason = self.__unittest_skip_why__ - pytest.skip(reason) + raise pytest.skip.Exception(reason, _use_item_location=True) if setup is not None: try: if pass_self: @@ -256,9 +255,8 @@ def addFailure( def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None: try: - skip(reason) + raise pytest.skip.Exception(reason, _use_item_location=True) except skip.Exception: - self._store[skipped_by_mark_key] = True self._addexcinfo(sys.exc_info()) def addExpectedFailure( From 25e657bfc15aecd8fa8787a028ffc10fc9ac96d5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 14 Jan 2021 18:14:39 +0200 Subject: [PATCH 0388/2846] Deprecate raising unittest.SkipTest to skip tests during collection It is not very clear why this code exists -- we are not running any unittest or nose code during collection, and really these frameworks don't have the concept of collection at all, and just raising these exceptions at e.g. the module level would cause an error. So unless I'm missing something, I don't think anyone is using this. Deprecate it so we can eventually clear up this code and keep unittest more tightly restricted to its plugin. --- changelog/8242.deprecation.rst | 7 +++++++ doc/en/deprecations.rst | 14 ++++++++++++++ src/_pytest/deprecated.py | 5 +++++ src/_pytest/runner.py | 7 +++++++ testing/deprecated_test.py | 17 +++++++++++++++++ 5 files changed, 50 insertions(+) create mode 100644 changelog/8242.deprecation.rst diff --git a/changelog/8242.deprecation.rst b/changelog/8242.deprecation.rst new file mode 100644 index 00000000000..b2e8566eaa9 --- /dev/null +++ b/changelog/8242.deprecation.rst @@ -0,0 +1,7 @@ +Raising :class:`unittest.SkipTest` to skip collection of tests during the +pytest collection phase is deprecated. Use :func:`pytest.skip` instead. + +Note: This deprecation only relates to using `unittest.SkipTest` during test +collection. You are probably not doing that. Ordinary usage of +:class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` / +:func:`unittest.skip` in unittest test cases is fully supported. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index ec2397e596f..0dcbd8ceb36 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -18,6 +18,20 @@ 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 `. +Raising ``unittest.SkipTest`` during collection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.3 + +Raising :class:`unittest.SkipTest` to skip collection of tests during the +pytest collection phase is deprecated. Use :func:`pytest.skip` instead. + +Note: This deprecation only relates to using `unittest.SkipTest` during test +collection. You are probably not doing that. Ordinary usage of +:class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` / +:func:`unittest.skip` in unittest test cases is fully supported. + + The ``--strict`` command-line option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 19b31d66538..fa91f909769 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -64,6 +64,11 @@ PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") +UNITTEST_SKIP_DURING_COLLECTION = PytestDeprecationWarning( + "Raising unittest.SkipTest to skip tests during collection is deprecated. " + "Use pytest.skip() instead." +) + # You want to make some `__init__` or function "private". # diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index df046a78aca..844e41f8057 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -2,6 +2,7 @@ import bdb import os import sys +import warnings from typing import Callable from typing import cast from typing import Dict @@ -27,6 +28,7 @@ from _pytest.compat import final from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest +from _pytest.deprecated import UNITTEST_SKIP_DURING_COLLECTION from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.nodes import Node @@ -374,6 +376,11 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: # Type ignored because unittest is loaded dynamically. skip_exceptions.append(unittest.SkipTest) # type: ignore if isinstance(call.excinfo.value, tuple(skip_exceptions)): + if unittest is not None and isinstance( + call.excinfo.value, unittest.SkipTest # type: ignore[attr-defined] + ): + warnings.warn(UNITTEST_SKIP_DURING_COLLECTION, stacklevel=2) + outcome = "skipped" r_ = collector._repr_failure_py(call.excinfo, "line") assert isinstance(r_, ExceptionChainRepr), repr(r_) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 6d92d181f99..18300f62a1a 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -136,3 +136,20 @@ def __init__(self, foo: int, *, _ispytest: bool = False) -> None: # Doesn't warn. PrivateInit(10, _ispytest=True) + + +def test_raising_unittest_skiptest_during_collection_is_deprecated( + pytester: Pytester, +) -> None: + pytester.makepyfile( + """ + import unittest + raise unittest.SkipTest() + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*PytestDeprecationWarning: Raising unittest.SkipTest*", + ] + ) From a9e43152bc5081afccc6de6ef5526ff35c525fed Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 17 Jan 2021 14:23:07 +0100 Subject: [PATCH 0389/2846] alter the PyObjMixin to carry over typing information from Node as PyObjMixin is always supposed to be mixed in the mro before nodes.Node the behavior doesn't change, but all the typing information carry over to help mypy. extracted from #8037 --- changelog/8248.trivial.rst | 1 + src/_pytest/python.py | 18 +++++------------- 2 files changed, 6 insertions(+), 13 deletions(-) create mode 100644 changelog/8248.trivial.rst diff --git a/changelog/8248.trivial.rst b/changelog/8248.trivial.rst new file mode 100644 index 00000000000..0a9319d9cd5 --- /dev/null +++ b/changelog/8248.trivial.rst @@ -0,0 +1 @@ +Internal Restructure: let python.PyObjMixing inherit from nodes.Node to carry over typing information. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 29ebd176bbb..eabd7b31154 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -23,7 +23,6 @@ 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 @@ -255,20 +254,13 @@ def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object): return res -class PyobjMixin: - _ALLOW_MARKERS = True - - # Function and attributes that the mixin needs (for type-checking only). - if TYPE_CHECKING: - name: str = "" - parent: Optional[nodes.Node] = None - own_markers: List[Mark] = [] +class PyobjMixin(nodes.Node): + """this mix-in inherits from Node to carry over the typing information - def getparent(self, cls: Type[nodes._NodeType]) -> Optional[nodes._NodeType]: - ... + as its intended to always mix in before a node + its position in the mro is unaffected""" - def listchain(self) -> List[nodes.Node]: - ... + _ALLOW_MARKERS = True @property def module(self): From 9ba1821e9121c3ee49a68b4863cd7ee50de4a5a5 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Sun, 17 Jan 2021 19:23:57 +0100 Subject: [PATCH 0390/2846] Fix faulthandler for Twisted Logger when used with "--capture=no" The Twisted Logger will return an invalid file descriptor since it is not backed by an FD. So, let's also forward this to the same code path as with `pytest-xdist`. --- AUTHORS | 1 + changelog/8249.bugfix.rst | 1 + src/_pytest/faulthandler.py | 7 ++++++- testing/test_faulthandler.py | 25 +++++++++++++++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 changelog/8249.bugfix.rst diff --git a/AUTHORS b/AUTHORS index e75baa8cd92..0c721a49610 100644 --- a/AUTHORS +++ b/AUTHORS @@ -21,6 +21,7 @@ Anders Hovmöller Andras Mitzki Andras Tim Andrea Cimatoribus +Andreas Motl Andreas Zeidler Andrey Paramonov Andrzej Klajnert diff --git a/changelog/8249.bugfix.rst b/changelog/8249.bugfix.rst new file mode 100644 index 00000000000..aa084c75738 --- /dev/null +++ b/changelog/8249.bugfix.rst @@ -0,0 +1 @@ +Fix the ``faulthandler`` plugin for occasions when running with ``twisted.logger`` and using ``pytest --capture=no``. diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index d0cc0430c49..ff673b5b164 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -69,7 +69,12 @@ def pytest_unconfigure(self, config: Config) -> None: @staticmethod def _get_stderr_fileno(): try: - return sys.stderr.fileno() + fileno = sys.stderr.fileno() + # The Twisted Logger will return an invalid file descriptor since it is not backed + # by an FD. So, let's also forward this to the same code path as with pytest-xdist. + if fileno == -1: + raise AttributeError() + return fileno except (AttributeError, io.UnsupportedOperation): # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file. # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index caf39813cf4..370084c125f 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -1,3 +1,4 @@ +import io import sys import pytest @@ -135,3 +136,27 @@ def test(): result.stdout.no_fnmatch_line(warning_line) result.stdout.fnmatch_lines("*1 passed*") assert result.ret == 0 + + +def test_get_stderr_fileno_invalid_fd() -> None: + """Test for faulthandler being able to handle invalid file descriptors for stderr (#8249).""" + from _pytest.faulthandler import FaultHandlerHooks + + class StdErrWrapper(io.StringIO): + """ + Mimic ``twisted.logger.LoggingFile`` to simulate returning an invalid file descriptor. + + https://github.com/twisted/twisted/blob/twisted-20.3.0/src/twisted/logger/_io.py#L132-L139 + """ + + def fileno(self): + return -1 + + wrapper = StdErrWrapper() + + with pytest.MonkeyPatch.context() as mp: + mp.setattr("sys.stderr", wrapper) + + # Even when the stderr wrapper signals an invalid file descriptor, + # ``_get_stderr_fileno()`` should return the real one. + assert FaultHandlerHooks._get_stderr_fileno() == 2 From eef2d1a8e268ef727eec8f1c38119ee76cdaebc2 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Tue, 19 Jan 2021 06:45:45 -0800 Subject: [PATCH 0391/2846] Fix pep8 import order in docs (#8253) --- doc/en/example/parametrize.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 6e2f53984ee..a65ee5f2fd9 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -97,10 +97,10 @@ the argument name: # content of test_time.py - import pytest - from datetime import datetime, timedelta + import pytest + testdata = [ (datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)), (datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)), From adc0f29b8f8fa8a63e0592c38503855a5b615a29 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 20 Jan 2021 10:05:36 -0300 Subject: [PATCH 0392/2846] Always handle faulthandler stderr even if already enabled It seems the code that would not install pytest's faulthandler support if it was already enabled is not really needed at all, and even detrimental when using `python -X dev -m pytest` to run Python in "dev" mode. Also simplified the plugin by removing the hook class, now the hooks will always be active so there's no need to delay the hook definitions anymore. Fix #8258 --- changelog/8258.bugfix.rst | 3 + src/_pytest/faulthandler.py | 134 +++++++++++++++-------------------- testing/test_faulthandler.py | 29 +++----- 3 files changed, 70 insertions(+), 96 deletions(-) create mode 100644 changelog/8258.bugfix.rst diff --git a/changelog/8258.bugfix.rst b/changelog/8258.bugfix.rst new file mode 100644 index 00000000000..6518ec0b738 --- /dev/null +++ b/changelog/8258.bugfix.rst @@ -0,0 +1,3 @@ +Fixed issue where pytest's ``faulthandler`` support would not dump traceback on crashes +if the :mod:`faulthandler` module was already enabled during pytest startup (using +``python -X dev -m pytest`` for example). diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index ff673b5b164..9592de82d6d 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -25,92 +25,72 @@ def pytest_addoption(parser: Parser) -> None: def pytest_configure(config: Config) -> None: import faulthandler - if not faulthandler.is_enabled(): - # faulthhandler is not enabled, so install plugin that does the actual work - # of enabling faulthandler before each test executes. - config.pluginmanager.register(FaultHandlerHooks(), "faulthandler-hooks") - else: - # 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: - config.issue_config_time_warning( - pytest.PytestConfigWarning( - "faulthandler module enabled before pytest configuration step, " - "'faulthandler_timeout' option ignored" - ), - stacklevel=2, - ) - - -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: Config) -> None: - import faulthandler + stderr_fd_copy = os.dup(get_stderr_fileno()) + config._store[fault_handler_stderr_key] = open(stderr_fd_copy, "w") + faulthandler.enable(file=config._store[fault_handler_stderr_key]) - 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: Config) -> None: - import faulthandler +def pytest_unconfigure(config: Config) -> None: + import faulthandler - faulthandler.disable() - # close our dup file installed during pytest_configure - # re-enable the faulthandler, attaching it to the default sys.stderr - # so we can see crashes after pytest has finished, usually during - # garbage collection during interpreter shutdown + faulthandler.disable() + # Close the dup file installed during pytest_configure. + if fault_handler_stderr_key in config._store: config._store[fault_handler_stderr_key].close() del config._store[fault_handler_stderr_key] - faulthandler.enable(file=self._get_stderr_fileno()) + # Re-enable the faulthandler, attaching it to the default sys.stderr + # so we can see crashes after pytest has finished, usually during + # garbage collection during interpreter shutdown. + faulthandler.enable(file=get_stderr_fileno()) + + +def get_stderr_fileno() -> int: + try: + fileno = sys.stderr.fileno() + # The Twisted Logger will return an invalid file descriptor since it is not backed + # by an FD. So, let's also forward this to the same code path as with pytest-xdist. + if fileno == -1: + raise AttributeError() + return fileno + except (AttributeError, io.UnsupportedOperation): + # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file. + # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors + # This is potentially dangerous, but the best we can do. + return sys.__stderr__.fileno() + + +def get_timeout_config_value(config: Config) -> float: + return float(config.getini("faulthandler_timeout") or 0.0) + + +@pytest.hookimpl(hookwrapper=True, trylast=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: + timeout = get_timeout_config_value(item.config) + stderr = item.config._store[fault_handler_stderr_key] + if timeout > 0 and stderr is not None: + import faulthandler - @staticmethod - def _get_stderr_fileno(): + faulthandler.dump_traceback_later(timeout, file=stderr) try: - fileno = sys.stderr.fileno() - # The Twisted Logger will return an invalid file descriptor since it is not backed - # by an FD. So, let's also forward this to the same code path as with pytest-xdist. - if fileno == -1: - raise AttributeError() - return fileno - except (AttributeError, io.UnsupportedOperation): - # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file. - # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors - # This is potentially dangerous, but the best we can do. - return sys.__stderr__.fileno() - - @staticmethod - 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: 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: - import faulthandler - - faulthandler.dump_traceback_later(timeout, file=stderr) - try: - yield - finally: - faulthandler.cancel_dump_traceback_later() - else: yield + finally: + faulthandler.cancel_dump_traceback_later() + else: + yield - @pytest.hookimpl(tryfirst=True) - def pytest_enter_pdb(self) -> None: - """Cancel any traceback dumping due to timeout before entering pdb.""" - import faulthandler - faulthandler.cancel_dump_traceback_later() +@pytest.hookimpl(tryfirst=True) +def pytest_enter_pdb() -> None: + """Cancel any traceback dumping due to timeout before entering pdb.""" + import faulthandler - @pytest.hookimpl(tryfirst=True) - def pytest_exception_interact(self) -> None: - """Cancel any traceback dumping due to an interactive exception being - raised.""" - import faulthandler + faulthandler.cancel_dump_traceback_later() + + +@pytest.hookimpl(tryfirst=True) +def pytest_exception_interact() -> None: + """Cancel any traceback dumping due to an interactive exception being + raised.""" + import faulthandler - faulthandler.cancel_dump_traceback_later() + faulthandler.cancel_dump_traceback_later() diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index 370084c125f..411e841a31a 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -94,7 +94,7 @@ def test_cancel_timeout_on_hook(monkeypatch, hook_name) -> None: 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 + from _pytest import faulthandler as faulthandler_plugin called = [] @@ -104,19 +104,18 @@ def test_cancel_timeout_on_hook(monkeypatch, hook_name) -> None: # call our hook explicitly, we can trust that pytest will call the hook # for us at the appropriate moment - hook_func = getattr(FaultHandlerHooks, hook_name) - hook_func(self=None) + hook_func = getattr(faulthandler_plugin, hook_name) + hook_func() assert called == [1] -@pytest.mark.parametrize("faulthandler_timeout", [0, 2]) -def test_already_initialized(faulthandler_timeout: int, pytester: Pytester) -> None: - """Test for faulthandler being initialized earlier than pytest (#6575).""" +def test_already_initialized_crash(pytester: Pytester) -> None: + """Even if faulthandler is already initialized, we still dump tracebacks on crashes (#8258).""" pytester.makepyfile( """ def test(): import faulthandler - assert faulthandler.is_enabled() + faulthandler._sigabrt() """ ) result = pytester.run( @@ -125,22 +124,14 @@ def test(): "faulthandler", "-mpytest", pytester.path, - "-o", - f"faulthandler_timeout={faulthandler_timeout}", ) - # ensure warning is emitted if faulthandler_timeout is configured - warning_line = "*faulthandler.py*faulthandler module enabled before*" - if faulthandler_timeout > 0: - result.stdout.fnmatch_lines(warning_line) - else: - result.stdout.no_fnmatch_line(warning_line) - result.stdout.fnmatch_lines("*1 passed*") - assert result.ret == 0 + result.stderr.fnmatch_lines(["*Fatal Python error*"]) + assert result.ret != 0 def test_get_stderr_fileno_invalid_fd() -> None: """Test for faulthandler being able to handle invalid file descriptors for stderr (#8249).""" - from _pytest.faulthandler import FaultHandlerHooks + from _pytest.faulthandler import get_stderr_fileno class StdErrWrapper(io.StringIO): """ @@ -159,4 +150,4 @@ def fileno(self): # Even when the stderr wrapper signals an invalid file descriptor, # ``_get_stderr_fileno()`` should return the real one. - assert FaultHandlerHooks._get_stderr_fileno() == 2 + assert get_stderr_fileno() == 2 From d4f8e4b40ce481df0f34b2eac5d61aefae4ed2e1 Mon Sep 17 00:00:00 2001 From: Hong Xu Date: Thu, 21 Jan 2021 04:58:52 -0800 Subject: [PATCH 0393/2846] Explain how to create binary files in the doc of `pytest.makefile`. (#8255) --- src/_pytest/pytester.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 95b22b3b23e..8ca21d1c538 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -777,7 +777,7 @@ def to_text(s: Union[Any, bytes]) -> str: return ret def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: - r"""Create new file(s) in the test directory. + r"""Create new text file(s) in the test directory. :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`. @@ -797,6 +797,12 @@ def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n") + To create binary files, use :meth:`pathlib.Path.write_bytes` directly: + + .. code-block:: python + + filename = pytester.path.joinpath("foo.bin") + filename.write_bytes(b"...") """ return self._makefile(ext, args, kwargs) From da70f61f67cc37209a5d97bca18303bdd7bccbc8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 30 Dec 2020 16:02:16 +0200 Subject: [PATCH 0394/2846] runner: complete type annotations of SetupState --- src/_pytest/runner.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 844e41f8057..a49879432a2 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -403,22 +403,22 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: class SetupState: """Shared state for setting up/tearing down test items or collectors.""" - def __init__(self): + def __init__(self) -> None: self.stack: List[Node] = [] self._finalizers: Dict[Node, List[Callable[[], object]]] = {} - def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None: + def addfinalizer(self, finalizer: Callable[[], object], colitem: Node) -> None: """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 :/ self._finalizers.setdefault(colitem, []).append(finalizer) - def _pop_and_teardown(self): + def _pop_and_teardown(self) -> None: colitem = self.stack.pop() self._teardown_with_finalization(colitem) - def _callfinalizers(self, colitem) -> None: + def _callfinalizers(self, colitem: Node) -> None: finalizers = self._finalizers.pop(colitem, None) exc = None while finalizers: @@ -433,7 +433,7 @@ def _callfinalizers(self, colitem) -> None: if exc: raise exc - def _teardown_with_finalization(self, colitem) -> None: + def _teardown_with_finalization(self, colitem: Node) -> None: self._callfinalizers(colitem) colitem.teardown() for colitem in self._finalizers: @@ -446,11 +446,11 @@ def teardown_all(self) -> None: self._teardown_with_finalization(key) assert not self._finalizers - def teardown_exact(self, item, nextitem) -> None: + def teardown_exact(self, item: Item, nextitem: Optional[Item]) -> None: needed_collectors = nextitem and nextitem.listchain() or [] self._teardown_towards(needed_collectors) - def _teardown_towards(self, needed_collectors) -> None: + def _teardown_towards(self, needed_collectors: List[Node]) -> None: exc = None while self.stack: if self.stack == needed_collectors[: len(self.stack)]: @@ -465,7 +465,7 @@ def _teardown_towards(self, needed_collectors) -> None: if exc: raise exc - def prepare(self, colitem) -> None: + def prepare(self, colitem: Item) -> None: """Setup objects along the collector chain to the test-method.""" # Check if the last collection node has raised an error. From f7b0b1dd1f6f2722da2cc1a908fdd2843cbdb111 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 30 Dec 2020 17:20:19 +0200 Subject: [PATCH 0395/2846] runner: use node's Store to keep private SetupState state instead of an attribute This way it gets proper typing and decoupling. --- src/_pytest/runner.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index a49879432a2..7087c6c590a 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -33,8 +33,10 @@ from _pytest.nodes import Item from _pytest.nodes import Node from _pytest.outcomes import Exit +from _pytest.outcomes import OutcomeException from _pytest.outcomes import Skipped from _pytest.outcomes import TEST_OUTCOME +from _pytest.store import StoreKey if TYPE_CHECKING: from typing_extensions import Literal @@ -465,14 +467,16 @@ def _teardown_towards(self, needed_collectors: List[Node]) -> None: if exc: raise exc + _prepare_exc_key = StoreKey[Union[OutcomeException, Exception]]() + def prepare(self, colitem: Item) -> None: """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] - raise exc + prepare_exc = col._store.get(self._prepare_exc_key, None) + if prepare_exc: + raise prepare_exc needed_collectors = colitem.listchain() for col in needed_collectors[len(self.stack) :]: @@ -480,7 +484,7 @@ def prepare(self, colitem: Item) -> None: try: col.setup() except TEST_OUTCOME as e: - col._prepare_exc = e # type: ignore[attr-defined] + col._store[self._prepare_exc_key] = e raise e From 410622f719fd0175cf875f528f122788715b1f05 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 30 Dec 2020 17:27:43 +0200 Subject: [PATCH 0396/2846] runner: reorder SetupState method to make more sense The setup stuff happens before the teardown stuff, so put it first so that reading the code from top to bottom makes more sense. --- src/_pytest/runner.py | 52 +++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 7087c6c590a..d46dba56f5a 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -409,6 +409,26 @@ def __init__(self) -> None: self.stack: List[Node] = [] self._finalizers: Dict[Node, List[Callable[[], object]]] = {} + _prepare_exc_key = StoreKey[Union[OutcomeException, Exception]]() + + def prepare(self, colitem: Item) -> None: + """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: + prepare_exc = col._store.get(self._prepare_exc_key, None) + if prepare_exc: + raise prepare_exc + + needed_collectors = colitem.listchain() + for col in needed_collectors[len(self.stack) :]: + self.stack.append(col) + try: + col.setup() + except TEST_OUTCOME as e: + col._store[self._prepare_exc_key] = e + raise e + def addfinalizer(self, finalizer: Callable[[], object], colitem: Node) -> None: """Attach a finalizer to the given colitem.""" assert colitem and not isinstance(colitem, tuple) @@ -441,13 +461,6 @@ def _teardown_with_finalization(self, colitem: Node) -> None: for colitem in self._finalizers: assert colitem in self.stack - 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: Item, nextitem: Optional[Item]) -> None: needed_collectors = nextitem and nextitem.listchain() or [] self._teardown_towards(needed_collectors) @@ -467,25 +480,12 @@ def _teardown_towards(self, needed_collectors: List[Node]) -> None: if exc: raise exc - _prepare_exc_key = StoreKey[Union[OutcomeException, Exception]]() - - def prepare(self, colitem: Item) -> None: - """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: - prepare_exc = col._store.get(self._prepare_exc_key, None) - if prepare_exc: - raise prepare_exc - - needed_collectors = colitem.listchain() - for col in needed_collectors[len(self.stack) :]: - self.stack.append(col) - try: - col.setup() - except TEST_OUTCOME as e: - col._store[self._prepare_exc_key] = e - raise e + 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 collect_one_node(collector: Collector) -> CollectReport: From 42ae8180ddf36fe4810bdfcce72aa72cf8cd3b43 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 30 Dec 2020 17:33:09 +0200 Subject: [PATCH 0397/2846] runner: inline SetupState._teardown_towards() Doesn't add much. --- src/_pytest/runner.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index d46dba56f5a..e132a2d8f4a 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -463,9 +463,6 @@ def _teardown_with_finalization(self, colitem: Node) -> None: def teardown_exact(self, item: Item, nextitem: Optional[Item]) -> None: needed_collectors = nextitem and nextitem.listchain() or [] - self._teardown_towards(needed_collectors) - - def _teardown_towards(self, needed_collectors: List[Node]) -> None: exc = None while self.stack: if self.stack == needed_collectors[: len(self.stack)]: From 5f4e55fb6d01a1dd199ed0f484e460be7d0e222a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 30 Dec 2020 17:34:00 +0200 Subject: [PATCH 0398/2846] runner: remove dead code in teardown_all() When the stack is empty, the finalizers which are supposed to be attached to nodes in the stack really ought to be empty as well. So the code here is dead. If this doesn't happen, the assert will trigger. --- src/_pytest/runner.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index e132a2d8f4a..017573cec9f 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -480,8 +480,6 @@ def teardown_exact(self, item: Item, nextitem: Optional[Item]) -> None: 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 From ceb4d6f6d595532be599e6382b571f3b4f614df9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 30 Dec 2020 17:36:42 +0200 Subject: [PATCH 0399/2846] runner: inline a couple of SetupState methods Code is clearer this way. --- src/_pytest/runner.py | 6 ------ testing/test_runner.py | 6 ++++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 017573cec9f..5d35f520ac6 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -438,9 +438,6 @@ def addfinalizer(self, finalizer: Callable[[], object], colitem: Node) -> None: def _pop_and_teardown(self) -> None: colitem = self.stack.pop() - self._teardown_with_finalization(colitem) - - def _callfinalizers(self, colitem: Node) -> None: finalizers = self._finalizers.pop(colitem, None) exc = None while finalizers: @@ -454,9 +451,6 @@ def _callfinalizers(self, colitem: Node) -> None: exc = e if exc: raise exc - - def _teardown_with_finalization(self, colitem: Node) -> None: - self._callfinalizers(colitem) colitem.teardown() for colitem in self._finalizers: assert colitem in self.stack diff --git a/testing/test_runner.py b/testing/test_runner.py index 8ce0f67354f..20c81a62f22 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -64,11 +64,12 @@ def fin3(): item = pytester.getitem("def test_func(): pass") ss = runner.SetupState() + ss.prepare(item) ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) ss.addfinalizer(fin3, item) with pytest.raises(Exception) as err: - ss._callfinalizers(item) + ss.teardown_exact(item, None) assert err.value.args == ("oops",) assert r == ["fin3", "fin1"] @@ -83,10 +84,11 @@ def fin2(): item = pytester.getitem("def test_func(): pass") ss = runner.SetupState() + ss.prepare(item) ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) with pytest.raises(Exception) as err: - ss._callfinalizers(item) + ss.teardown_exact(item, None) assert err.value.args == ("oops2",) def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None: From 2b14edb108f32c49c15b5e0c0ef4d27880b09e0f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 12:08:12 +0200 Subject: [PATCH 0400/2846] runner: express SetupState.teardown_all() in terms of teardown_exact() and remove it Makes it easier to understand with fewer methods. --- src/_pytest/runner.py | 13 +++++-------- testing/python/fixtures.py | 2 +- testing/test_runner.py | 13 +++++++------ 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 5d35f520ac6..525aafc76db 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -105,7 +105,7 @@ def pytest_sessionstart(session: "Session") -> None: def pytest_sessionfinish(session: "Session") -> None: - session._setupstate.teardown_all() + session._setupstate.teardown_exact(None) def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool: @@ -177,7 +177,7 @@ def pytest_runtest_call(item: Item) -> None: def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None: _update_current_test_var(item, "teardown") - item.session._setupstate.teardown_exact(item, nextitem) + item.session._setupstate.teardown_exact(nextitem) _update_current_test_var(item, None) @@ -455,7 +455,7 @@ def _pop_and_teardown(self) -> None: for colitem in self._finalizers: assert colitem in self.stack - def teardown_exact(self, item: Item, nextitem: Optional[Item]) -> None: + def teardown_exact(self, nextitem: Optional[Item]) -> None: needed_collectors = nextitem and nextitem.listchain() or [] exc = None while self.stack: @@ -470,11 +470,8 @@ def teardown_exact(self, item: Item, nextitem: Optional[Item]) -> None: exc = e if exc: raise exc - - def teardown_all(self) -> None: - while self.stack: - self._pop_and_teardown() - assert not self._finalizers + if nextitem is None: + assert not self._finalizers def collect_one_node(collector: Collector) -> CollectReport: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 12340e690eb..d1297339645 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -856,7 +856,7 @@ def test_func(something): pass teardownlist = parent.obj.teardownlist ss = item.session._setupstate assert not teardownlist - ss.teardown_exact(item, None) + ss.teardown_exact(None) print(ss.stack) assert teardownlist == [1] diff --git a/testing/test_runner.py b/testing/test_runner.py index 20c81a62f22..aca1bd7ceb0 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -34,9 +34,10 @@ def test_setup(self, pytester: Pytester) -> None: def test_teardown_exact_stack_empty(self, pytester: Pytester) -> None: item = pytester.getitem("def test_func(): pass") ss = runner.SetupState() - ss.teardown_exact(item, None) - ss.teardown_exact(item, None) - ss.teardown_exact(item, None) + ss.prepare(item) + ss.teardown_exact(None) + ss.teardown_exact(None) + ss.teardown_exact(None) def test_setup_fails_and_failure_is_cached(self, pytester: Pytester) -> None: item = pytester.getitem( @@ -69,7 +70,7 @@ def fin3(): ss.addfinalizer(fin2, item) ss.addfinalizer(fin3, item) with pytest.raises(Exception) as err: - ss.teardown_exact(item, None) + ss.teardown_exact(None) assert err.value.args == ("oops",) assert r == ["fin3", "fin1"] @@ -88,7 +89,7 @@ def fin2(): ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) with pytest.raises(Exception) as err: - ss.teardown_exact(item, None) + ss.teardown_exact(None) assert err.value.args == ("oops2",) def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None: @@ -106,7 +107,7 @@ def fin_module(): ss.addfinalizer(fin_func, item) ss.prepare(item) with pytest.raises(Exception, match="oops1"): - ss.teardown_exact(item, None) + ss.teardown_exact(None) assert module_teardown From d1fcd425a3da18ecec35a6028093ebff830edd46 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 12:51:33 +0200 Subject: [PATCH 0401/2846] runner: inline SetupState._pop_and_teardown() This will enable a simplification in the next commit. --- src/_pytest/runner.py | 37 +++++++++++++++++-------------------- testing/test_runner.py | 2 +- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 525aafc76db..8102a6019f2 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -436,25 +436,6 @@ def addfinalizer(self, finalizer: Callable[[], object], colitem: Node) -> None: # assert colitem in self.stack # some unit tests don't setup stack :/ self._finalizers.setdefault(colitem, []).append(finalizer) - def _pop_and_teardown(self) -> None: - colitem = self.stack.pop() - finalizers = self._finalizers.pop(colitem, None) - exc = None - while finalizers: - fin = finalizers.pop() - try: - fin() - except TEST_OUTCOME as e: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if exc is None: - exc = e - if exc: - raise exc - colitem.teardown() - for colitem in self._finalizers: - assert colitem in self.stack - def teardown_exact(self, nextitem: Optional[Item]) -> None: needed_collectors = nextitem and nextitem.listchain() or [] exc = None @@ -462,7 +443,23 @@ def teardown_exact(self, nextitem: Optional[Item]) -> None: if self.stack == needed_collectors[: len(self.stack)]: break try: - self._pop_and_teardown() + colitem = self.stack.pop() + finalizers = self._finalizers.pop(colitem, None) + inner_exc = None + while finalizers: + fin = finalizers.pop() + try: + fin() + except TEST_OUTCOME as e: + # XXX Only first exception will be seen by user, + # ideally all should be reported. + if inner_exc is None: + inner_exc = e + if inner_exc: + raise inner_exc + colitem.teardown() + for colitem in self._finalizers: + assert colitem in self.stack except TEST_OUTCOME as e: # XXX Only first exception will be seen by user, # ideally all should be reported. diff --git a/testing/test_runner.py b/testing/test_runner.py index aca1bd7ceb0..f53ad2d0820 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -28,7 +28,7 @@ def test_setup(self, pytester: Pytester) -> None: ss.prepare(item) ss.addfinalizer(values.pop, colitem=item) assert values - ss._pop_and_teardown() + ss.teardown_exact(None) assert not values def test_teardown_exact_stack_empty(self, pytester: Pytester) -> None: From 14d71b2c228c3ee92a1e7aa93a6ea64444f19697 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 13:55:49 +0200 Subject: [PATCH 0402/2846] runner: make sure SetupState._finalizers is always set for a node in the stack This makes the stack <-> _finalizers correspondence clearer. --- src/_pytest/runner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 8102a6019f2..b25438c2326 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -423,6 +423,7 @@ def prepare(self, colitem: Item) -> None: needed_collectors = colitem.listchain() for col in needed_collectors[len(self.stack) :]: self.stack.append(col) + self._finalizers.setdefault(col, []) try: col.setup() except TEST_OUTCOME as e: @@ -444,7 +445,7 @@ def teardown_exact(self, nextitem: Optional[Item]) -> None: break try: colitem = self.stack.pop() - finalizers = self._finalizers.pop(colitem, None) + finalizers = self._finalizers.pop(colitem) inner_exc = None while finalizers: fin = finalizers.pop() From bb3d43c9a6d16174a05058686b9460ceff911e5a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 13:10:43 +0200 Subject: [PATCH 0403/2846] runner: ensure item.teardown() is called even if a finalizer raised If one finalizer fails, all of the subsequent finalizers still run, so the `teardown()` method should behave the same. --- src/_pytest/runner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index b25438c2326..c221b42a045 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -446,6 +446,7 @@ def teardown_exact(self, nextitem: Optional[Item]) -> None: try: colitem = self.stack.pop() finalizers = self._finalizers.pop(colitem) + finalizers.insert(0, colitem.teardown) inner_exc = None while finalizers: fin = finalizers.pop() @@ -456,11 +457,10 @@ def teardown_exact(self, nextitem: Optional[Item]) -> None: # ideally all should be reported. if inner_exc is None: inner_exc = e - if inner_exc: - raise inner_exc - colitem.teardown() for colitem in self._finalizers: assert colitem in self.stack + if inner_exc: + raise inner_exc except TEST_OUTCOME as e: # XXX Only first exception will be seen by user, # ideally all should be reported. From 0d4121d24bf4c1efdee67a45b831ccdbdda78f2c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 13:06:50 +0200 Subject: [PATCH 0404/2846] runner: collapse exception handling in SetupState.teardown_exact() This is equivalent but simpler. --- src/_pytest/runner.py | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index c221b42a045..3aa0a6c4bf9 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -443,29 +443,20 @@ def teardown_exact(self, nextitem: Optional[Item]) -> None: while self.stack: if self.stack == needed_collectors[: len(self.stack)]: break - try: - colitem = self.stack.pop() - finalizers = self._finalizers.pop(colitem) - finalizers.insert(0, colitem.teardown) - inner_exc = None - while finalizers: - fin = finalizers.pop() - try: - fin() - except TEST_OUTCOME as e: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if inner_exc is None: - inner_exc = e - for colitem in self._finalizers: - assert colitem in self.stack - if inner_exc: - raise inner_exc - except TEST_OUTCOME as e: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if exc is None: - exc = e + colitem = self.stack.pop() + finalizers = self._finalizers.pop(colitem) + finalizers.insert(0, colitem.teardown) + while finalizers: + fin = finalizers.pop() + try: + fin() + except TEST_OUTCOME as e: + # XXX Only first exception will be seen by user, + # ideally all should be reported. + if exc is None: + exc = e + for colitem in self._finalizers: + assert colitem in self.stack if exc: raise exc if nextitem is None: From c83d030028806b119cb61824ed984524c9faad6d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 15:53:38 +0200 Subject: [PATCH 0405/2846] testing/test_runner: make SetupState tests use a proper SetupState Previously the tests (probably unintentionally) mixed a fresh SetupState and the generated item Session's SetupState, which led to some serious head scratching when prodding it a bit. --- testing/test_runner.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/testing/test_runner.py b/testing/test_runner.py index f53ad2d0820..f1038ce96bb 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -22,8 +22,8 @@ class TestSetupState: def test_setup(self, pytester: Pytester) -> None: - ss = runner.SetupState() item = pytester.getitem("def test_func(): pass") + ss = item.session._setupstate values = [1] ss.prepare(item) ss.addfinalizer(values.pop, colitem=item) @@ -33,7 +33,7 @@ def test_setup(self, pytester: Pytester) -> None: def test_teardown_exact_stack_empty(self, pytester: Pytester) -> None: item = pytester.getitem("def test_func(): pass") - ss = runner.SetupState() + ss = item.session._setupstate ss.prepare(item) ss.teardown_exact(None) ss.teardown_exact(None) @@ -47,9 +47,11 @@ def setup_module(mod): def test_func(): pass """ ) - ss = runner.SetupState() - pytest.raises(ValueError, lambda: ss.prepare(item)) - pytest.raises(ValueError, lambda: ss.prepare(item)) + ss = item.session._setupstate + with pytest.raises(ValueError): + ss.prepare(item) + with pytest.raises(ValueError): + ss.prepare(item) def test_teardown_multiple_one_fails(self, pytester: Pytester) -> None: r = [] @@ -64,7 +66,7 @@ def fin3(): r.append("fin3") item = pytester.getitem("def test_func(): pass") - ss = runner.SetupState() + ss = item.session._setupstate ss.prepare(item) ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) @@ -84,7 +86,7 @@ def fin2(): raise Exception("oops2") item = pytester.getitem("def test_func(): pass") - ss = runner.SetupState() + ss = item.session._setupstate ss.prepare(item) ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) @@ -102,7 +104,7 @@ def fin_module(): module_teardown.append("fin_module") item = pytester.getitem("def test_func(): pass") - ss = runner.SetupState() + ss = item.session._setupstate ss.addfinalizer(fin_module, item.listchain()[-2]) ss.addfinalizer(fin_func, item) ss.prepare(item) From 637300d13d69848226f0f6bbc24102dafdfd6357 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 16:05:54 +0200 Subject: [PATCH 0406/2846] testing: fix some tests to be more realistic Perform the operations in the order and context in which they can legally occur. --- testing/python/fixtures.py | 15 +++++++++++---- testing/test_runner.py | 7 ++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index d1297339645..3d78ebf5826 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -130,7 +130,8 @@ def test_funcarg_basic(self, pytester: Pytester) -> None: pytester.copy_example() item = pytester.getitem(Path("test_funcarg_basic.py")) assert isinstance(item, Function) - item._request._fillfixtures() + # Execute's item's setup, which fills fixtures. + item.session._setupstate.prepare(item) del item.funcargs["request"] assert len(get_public_names(item.funcargs)) == 2 assert item.funcargs["some"] == "test_func" @@ -809,18 +810,25 @@ def test_getfixturevalue(self, pytester: Pytester) -> None: item = pytester.getitem( """ import pytest - values = [2] + @pytest.fixture - def something(request): return 1 + def something(request): + return 1 + + values = [2] @pytest.fixture def other(request): return values.pop() + def test_func(something): pass """ ) assert isinstance(item, Function) req = item._request + # Execute item's setup. + item.session._setupstate.prepare(item) + with pytest.raises(pytest.FixtureLookupError): req.getfixturevalue("notexists") val = req.getfixturevalue("something") @@ -831,7 +839,6 @@ def test_func(something): pass assert val2 == 2 val2 = req.getfixturevalue("other") # see about caching assert val2 == 2 - item._request._fillfixtures() assert item.funcargs["something"] == 1 assert len(get_public_names(item.funcargs)) == 2 assert "request" in item.funcargs diff --git a/testing/test_runner.py b/testing/test_runner.py index f1038ce96bb..0e90ea9cca2 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -104,13 +104,14 @@ def fin_module(): module_teardown.append("fin_module") item = pytester.getitem("def test_func(): pass") + mod = item.listchain()[-2] ss = item.session._setupstate - ss.addfinalizer(fin_module, item.listchain()[-2]) - ss.addfinalizer(fin_func, item) ss.prepare(item) + ss.addfinalizer(fin_module, mod) + ss.addfinalizer(fin_func, item) with pytest.raises(Exception, match="oops1"): ss.teardown_exact(None) - assert module_teardown + assert module_teardown == ["fin_module"] class BaseFunctionalTests: From 6db082a4486923637b4f427c95ab379d81b78528 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 17:35:22 +0200 Subject: [PATCH 0407/2846] fixtures: make sure to properly setup stack for _fill_fixtures_impl This code is weird, dead, deprecated and will be removed in pytest 7, but for now some tests execute it, so fix it up in preparation for some changes. --- src/_pytest/fixtures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 43a40a86449..481bda8f497 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -372,6 +372,7 @@ def _fill_fixtures_impl(function: "Function") -> None: fi = fm.getfixtureinfo(function.parent, function.obj, None) function._fixtureinfo = fi request = function._request = FixtureRequest(function, _ispytest=True) + fm.session._setupstate.prepare(function) request._fillfixtures() # Prune out funcargs for jstests. newfuncargs = {} From 960ebae943927805091153bfe17677c5f3734198 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 14:13:39 +0200 Subject: [PATCH 0408/2846] runner: enable a commented assertion in SetupState.addfinalizer The assertion ensures that when `addfinalizer(finalizer, node)` is called, the node is in the stack. This then would ensure that the finalization is actually properly executed properly during the node's teardown. Anything else indicates something is wrong. Previous commits fixed all of the tests which previously failed this, so can be reenabeld now. --- src/_pytest/runner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 3aa0a6c4bf9..9759441bd1e 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -434,8 +434,8 @@ def addfinalizer(self, finalizer: Callable[[], object], colitem: Node) -> None: """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 :/ - self._finalizers.setdefault(colitem, []).append(finalizer) + assert colitem in self.stack, (colitem, self.stack) + self._finalizers[colitem].append(finalizer) def teardown_exact(self, nextitem: Optional[Item]) -> None: needed_collectors = nextitem and nextitem.listchain() or [] From 03c3a90c686c4cb0a146e4139125b30cba27075a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 21:48:03 +0200 Subject: [PATCH 0409/2846] runner: replace setdefault with an unconditional set The already-exists case is not supposed to happen. --- src/_pytest/runner.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 9759441bd1e..fe3590d448c 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -422,8 +422,10 @@ def prepare(self, colitem: Item) -> None: needed_collectors = colitem.listchain() for col in needed_collectors[len(self.stack) :]: + assert col not in self.stack + assert col not in self._finalizers self.stack.append(col) - self._finalizers.setdefault(col, []) + self._finalizers[col] = [] try: col.setup() except TEST_OUTCOME as e: From 1db78bec311b9ad161dd201a1796abf82feeb8a8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 21:50:38 +0200 Subject: [PATCH 0410/2846] runner: use insertion-ordered dict instead of stack, dict pair Since dicts are now ordered, we can use the finalizers dict itself as the dict, simplifying the code. --- src/_pytest/runner.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index fe3590d448c..5dbb26aef0b 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -406,8 +406,7 @@ class SetupState: """Shared state for setting up/tearing down test items or collectors.""" def __init__(self) -> None: - self.stack: List[Node] = [] - self._finalizers: Dict[Node, List[Callable[[], object]]] = {} + self.stack: Dict[Node, List[Callable[[], object]]] = {} _prepare_exc_key = StoreKey[Union[OutcomeException, Exception]]() @@ -423,9 +422,7 @@ def prepare(self, colitem: Item) -> None: needed_collectors = colitem.listchain() for col in needed_collectors[len(self.stack) :]: assert col not in self.stack - assert col not in self._finalizers - self.stack.append(col) - self._finalizers[col] = [] + self.stack[col] = [] try: col.setup() except TEST_OUTCOME as e: @@ -437,16 +434,15 @@ def addfinalizer(self, finalizer: Callable[[], object], colitem: Node) -> None: assert colitem and not isinstance(colitem, tuple) assert callable(finalizer) assert colitem in self.stack, (colitem, self.stack) - self._finalizers[colitem].append(finalizer) + self.stack[colitem].append(finalizer) def teardown_exact(self, nextitem: Optional[Item]) -> None: needed_collectors = nextitem and nextitem.listchain() or [] exc = None while self.stack: - if self.stack == needed_collectors[: len(self.stack)]: + if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: break - colitem = self.stack.pop() - finalizers = self._finalizers.pop(colitem) + colitem, finalizers = self.stack.popitem() finalizers.insert(0, colitem.teardown) while finalizers: fin = finalizers.pop() @@ -457,12 +453,10 @@ def teardown_exact(self, nextitem: Optional[Item]) -> None: # ideally all should be reported. if exc is None: exc = e - for colitem in self._finalizers: - assert colitem in self.stack if exc: raise exc if nextitem is None: - assert not self._finalizers + assert not self.stack def collect_one_node(collector: Collector) -> CollectReport: From 0d19aff562680321a4dab33b1623edb424896d24 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 22:03:52 +0200 Subject: [PATCH 0411/2846] runner: schedule node.teardown() call already at setup This is more elegant. --- 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 5dbb26aef0b..63f9227ecda 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -422,7 +422,7 @@ def prepare(self, colitem: Item) -> None: needed_collectors = colitem.listchain() for col in needed_collectors[len(self.stack) :]: assert col not in self.stack - self.stack[col] = [] + self.stack[col] = [col.teardown] try: col.setup() except TEST_OUTCOME as e: @@ -443,7 +443,6 @@ def teardown_exact(self, nextitem: Optional[Item]) -> None: if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: break colitem, finalizers = self.stack.popitem() - finalizers.insert(0, colitem.teardown) while finalizers: fin = finalizers.pop() try: From c30feeef8b12ff2a755ce0fc61a5ed1f59e83c0c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 Jan 2021 23:14:04 +0200 Subject: [PATCH 0412/2846] runner: add docstring to SetupState and improve variable naming a bit --- src/_pytest/fixtures.py | 4 +- src/_pytest/runner.py | 96 +++++++++++++++++++++++++++++++++++------ testing/test_runner.py | 2 +- 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 481bda8f497..269369642e3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -544,8 +544,8 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._addfinalizer(finalizer, scope=self.scope) def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None: - item = self._getscopeitem(scope) - item.addfinalizer(finalizer) + node = self._getscopeitem(scope) + node.addfinalizer(finalizer) def applymarker(self, marker: Union[str, MarkDecorator]) -> None: """Apply a marker to a single test function invocation. diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 63f9227ecda..7bb92cecf81 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -403,23 +403,86 @@ 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 + in a session. + + Suppose we have a collection tree as follows: + + + + + + + + The SetupState maintains a stack. The stack starts out empty: + + [] + + During the setup phase of item1, prepare(item1) is called. What it does + is: + + push session to stack, run session.setup() + push mod1 to stack, run mod1.setup() + push item1 to stack, run item1.setup() + + The stack is: + + [session, mod1, item1] + + While the stack is in this shape, it is allowed to add finalizers to + each of session, mod1, item1 using addfinalizer(). + + During the teardown phase of item1, teardown_exact(item2) is called, + where item2 is the next item to item1. What it does is: + + pop item1 from stack, run its teardowns + pop mod1 from stack, run its teardowns + + mod1 was popped because it ended its purpose with item1. The stack is: + + [session] + + During the setup phase of item2, prepare(item2) is called. What it does + is: + + push mod2 to stack, run mod2.setup() + push item2 to stack, run item2.setup() + + Stack: + + [session, mod2, item2] + + During the teardown phase of item2, teardown_exact(None) is called, + because item2 is the last item. What it does is: + + pop item2 from stack, run its teardowns + pop mod2 from stack, run its teardowns + pop session from stack, run its teardowns + + Stack: + + [] + + The end! + """ def __init__(self) -> None: + # Maps node -> the node's finalizers. + # The stack is in the dict insertion order. self.stack: Dict[Node, List[Callable[[], object]]] = {} _prepare_exc_key = StoreKey[Union[OutcomeException, Exception]]() - def prepare(self, colitem: Item) -> None: - """Setup objects along the collector chain to the test-method.""" - - # Check if the last collection node has raised an error. + def prepare(self, item: Item) -> None: + """Setup objects along the collector chain to the item.""" + # If a collector fails its setup, fail its entire subtree of items. + # The setup is not retried for each item - the same exception is used. for col in self.stack: prepare_exc = col._store.get(self._prepare_exc_key, None) if prepare_exc: raise prepare_exc - needed_collectors = colitem.listchain() + needed_collectors = item.listchain() for col in needed_collectors[len(self.stack) :]: assert col not in self.stack self.stack[col] = [col.teardown] @@ -429,20 +492,29 @@ def prepare(self, colitem: Item) -> None: col._store[self._prepare_exc_key] = e raise e - def addfinalizer(self, finalizer: Callable[[], object], colitem: Node) -> None: - """Attach a finalizer to the given colitem.""" - assert colitem and not isinstance(colitem, tuple) + def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: + """Attach a finalizer to the given node. + + The node must be currently active in the stack. + """ + assert node and not isinstance(node, tuple) assert callable(finalizer) - assert colitem in self.stack, (colitem, self.stack) - self.stack[colitem].append(finalizer) + assert node in self.stack, (node, self.stack) + self.stack[node].append(finalizer) def teardown_exact(self, nextitem: Optional[Item]) -> None: + """Teardown the current stack up until reaching nodes that nextitem + also descends from. + + When nextitem is None (meaning we're at the last item), the entire + stack is torn down. + """ needed_collectors = nextitem and nextitem.listchain() or [] exc = None while self.stack: if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: break - colitem, finalizers = self.stack.popitem() + node, finalizers = self.stack.popitem() while finalizers: fin = finalizers.pop() try: diff --git a/testing/test_runner.py b/testing/test_runner.py index 0e90ea9cca2..e3f2863079f 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -26,7 +26,7 @@ def test_setup(self, pytester: Pytester) -> None: ss = item.session._setupstate values = [1] ss.prepare(item) - ss.addfinalizer(values.pop, colitem=item) + ss.addfinalizer(values.pop, item) assert values ss.teardown_exact(None) assert not values From 48fb989a71674189bec2e732fefb8b35b89d58f5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 24 Jan 2021 14:45:49 +0200 Subject: [PATCH 0413/2846] runner: avoid using node's store in SetupState SetupState maintains its own state, so it can store the exception itself, instead of using the node's store, which is better avoided when possible. This also reduces the lifetime of the reference-cycle-inducing exception objects which is never a bad thing. --- src/_pytest/runner.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 7bb92cecf81..ae76a247271 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -36,7 +36,6 @@ from _pytest.outcomes import OutcomeException from _pytest.outcomes import Skipped from _pytest.outcomes import TEST_OUTCOME -from _pytest.store import StoreKey if TYPE_CHECKING: from typing_extensions import Literal @@ -467,29 +466,33 @@ class SetupState: """ def __init__(self) -> None: - # Maps node -> the node's finalizers. # The stack is in the dict insertion order. - self.stack: Dict[Node, List[Callable[[], object]]] = {} - - _prepare_exc_key = StoreKey[Union[OutcomeException, Exception]]() + self.stack: Dict[ + Node, + Tuple[ + # Node's finalizers. + List[Callable[[], object]], + # Node's exception, if its setup raised. + Optional[Union[OutcomeException, Exception]], + ], + ] = {} def prepare(self, item: Item) -> None: """Setup objects along the collector chain to the item.""" # If a collector fails its setup, fail its entire subtree of items. # The setup is not retried for each item - the same exception is used. - for col in self.stack: - prepare_exc = col._store.get(self._prepare_exc_key, None) + for col, (finalizers, prepare_exc) in self.stack.items(): if prepare_exc: raise prepare_exc needed_collectors = item.listchain() for col in needed_collectors[len(self.stack) :]: assert col not in self.stack - self.stack[col] = [col.teardown] + self.stack[col] = ([col.teardown], None) try: col.setup() except TEST_OUTCOME as e: - col._store[self._prepare_exc_key] = e + self.stack[col] = (self.stack[col][0], e) raise e def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: @@ -500,7 +503,7 @@ def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: assert node and not isinstance(node, tuple) assert callable(finalizer) assert node in self.stack, (node, self.stack) - self.stack[node].append(finalizer) + self.stack[node][0].append(finalizer) def teardown_exact(self, nextitem: Optional[Item]) -> None: """Teardown the current stack up until reaching nodes that nextitem @@ -514,7 +517,7 @@ def teardown_exact(self, nextitem: Optional[Item]) -> None: while self.stack: if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: break - node, finalizers = self.stack.popitem() + node, (finalizers, prepare_exc) = self.stack.popitem() while finalizers: fin = finalizers.pop() try: From 83ee1a1f3b940b82223a162c4a695a84669c0ea3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 03:16:50 +0000 Subject: [PATCH 0414/2846] build(deps): bump pytest-cov in /testing/plugins_integration Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.10.1 to 2.11.1. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.10.1...v2.11.1) 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 ae5c9a93fef..86c2a862c9b 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -2,7 +2,7 @@ anyio[curio,trio]==2.0.2 django==3.1.5 pytest-asyncio==0.14.0 pytest-bdd==4.0.2 -pytest-cov==2.10.1 +pytest-cov==2.11.1 pytest-django==4.1.0 pytest-flakes==4.0.3 pytest-html==3.1.1 From 2a890286f8487c8f3bdc5de90c2fd522da9fd6a7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 25 Jan 2021 11:52:23 -0300 Subject: [PATCH 0415/2846] Merge pull request #8275 from pytest-dev/release-6.2.2 Prepare release 6.2.2 (cherry picked from commit 8220eca963472e7918ef7e108bdc1cd8ed155a4a) --- changelog/8152.bugfix.rst | 1 - changelog/8249.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-6.2.2.rst | 21 +++++++++++++++++++++ doc/en/changelog.rst | 12 ++++++++++++ doc/en/example/parametrize.rst | 4 ++-- doc/en/getting-started.rst | 2 +- 7 files changed, 37 insertions(+), 5 deletions(-) delete mode 100644 changelog/8152.bugfix.rst delete mode 100644 changelog/8249.bugfix.rst create mode 100644 doc/en/announce/release-6.2.2.rst diff --git a/changelog/8152.bugfix.rst b/changelog/8152.bugfix.rst deleted file mode 100644 index d79a832de41..00000000000 --- a/changelog/8152.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed "()" being shown as a skip reason in the verbose test summary line when the reason is empty. diff --git a/changelog/8249.bugfix.rst b/changelog/8249.bugfix.rst deleted file mode 100644 index aa084c75738..00000000000 --- a/changelog/8249.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix the ``faulthandler`` plugin for occasions when running with ``twisted.logger`` and using ``pytest --capture=no``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index e7cac2a1c41..a7656c5ee26 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.2.2 release-6.2.1 release-6.2.0 release-6.1.2 diff --git a/doc/en/announce/release-6.2.2.rst b/doc/en/announce/release-6.2.2.rst new file mode 100644 index 00000000000..c3999c53860 --- /dev/null +++ b/doc/en/announce/release-6.2.2.rst @@ -0,0 +1,21 @@ +pytest-6.2.2 +======================================= + +pytest 6.2.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: + +* Adam Johnson +* Bruno Oliveira +* Chris NeJame +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 6d66ad1d8dc..3e854f59971 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.2.2 (2021-01-25) +========================= + +Bug Fixes +--------- + +- `#8152 `_: Fixed "()" being shown as a skip reason in the verbose test summary line when the reason is empty. + + +- `#8249 `_: Fix the ``faulthandler`` plugin for occasions when running with ``twisted.logger`` and using ``pytest --capture=no``. + + pytest 6.2.1 (2020-12-15) ========================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index a65ee5f2fd9..771c7e16f28 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -637,13 +637,13 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker: 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 + collecting ... collected 24 items / 21 deselected / 3 selected test_pytest_param_example.py::test_eval[1+7-8] PASSED [ 33%] test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%] test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%] - =============== 2 passed, 11 deselected, 1 xfailed in 0.12s ================ + =============== 2 passed, 21 deselected, 1 xfailed in 0.12s ================ As the result: diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 09410585dc7..1275dff902e 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.1 + pytest 6.2.2 .. _`simpletest`: From 781b73bb523c705d65151d959736b017328e7227 Mon Sep 17 00:00:00 2001 From: Christian Steinmeyer Date: Mon, 25 Jan 2021 16:02:59 +0100 Subject: [PATCH 0416/2846] Mention that class variables are shared between tests Close #8252 --- doc/en/getting-started.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 1275dff902e..28fd862cf3b 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -210,6 +210,8 @@ This is outlined below: FAILED test_class_demo.py::TestClassDemoInstance::test_two - assert 0 2 failed in 0.12s +Note that attributes added at class level are *class attributes*, so they will be shared between tests. + Request a unique temporary directory for functional tests -------------------------------------------------------------- From 33861098d9145d3441c317c9d9b2265654b53b05 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 25 Jan 2021 12:28:00 -0300 Subject: [PATCH 0417/2846] Only re-enable fauthandler during unconfigure if it was enabled before --- src/_pytest/faulthandler.py | 9 +++++---- testing/test_faulthandler.py | 37 +++++++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 9592de82d6d..c8eb0310128 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -12,6 +12,7 @@ fault_handler_stderr_key = StoreKey[TextIO]() +fault_handler_originally_enabled_key = StoreKey[bool]() def pytest_addoption(parser: Parser) -> None: @@ -27,6 +28,7 @@ def pytest_configure(config: Config) -> None: stderr_fd_copy = os.dup(get_stderr_fileno()) config._store[fault_handler_stderr_key] = open(stderr_fd_copy, "w") + config._store[fault_handler_originally_enabled_key] = faulthandler.is_enabled() faulthandler.enable(file=config._store[fault_handler_stderr_key]) @@ -38,10 +40,9 @@ def pytest_unconfigure(config: Config) -> None: if fault_handler_stderr_key in config._store: config._store[fault_handler_stderr_key].close() del config._store[fault_handler_stderr_key] - # Re-enable the faulthandler, attaching it to the default sys.stderr - # so we can see crashes after pytest has finished, usually during - # garbage collection during interpreter shutdown. - faulthandler.enable(file=get_stderr_fileno()) + if config._store.get(fault_handler_originally_enabled_key, False): + # Re-enable the faulthandler if it was originally enabled. + faulthandler.enable(file=get_stderr_fileno()) def get_stderr_fileno() -> int: diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index 411e841a31a..5b7911f21f8 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -19,22 +19,41 @@ def test_crash(): assert result.ret != 0 -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).""" +def setup_crashing_test(pytester: Pytester) -> None: pytester.makepyfile( """ - import faulthandler - import atexit - def test_ok(): - atexit.register(faulthandler._sigabrt) - """ + import faulthandler + import atexit + def test_ok(): + atexit.register(faulthandler._sigabrt) + """ ) - result = pytester.runpytest_subprocess() + + +def test_crash_during_shutdown_captured(pytester: Pytester) -> None: + """ + Re-enable faulthandler if pytest encountered it enabled during configure. + We should be able to then see crashes during interpreter shutdown. + """ + setup_crashing_test(pytester) + args = (sys.executable, "-Xfaulthandler", "-mpytest") + result = pytester.run(*args) result.stderr.fnmatch_lines(["*Fatal Python error*"]) assert result.ret != 0 +def test_crash_during_shutdown_not_captured(pytester: Pytester) -> None: + """ + Check that pytest leaves faulthandler disabled if it was not enabled during configure. + This prevents us from seeing crashes during interpreter shutdown (see #8260). + """ + setup_crashing_test(pytester) + args = (sys.executable, "-mpytest") + result = pytester.run(*args) + result.stderr.no_fnmatch_line("*Fatal Python error*") + assert result.ret != 0 + + def test_disabled(pytester: Pytester) -> None: """Test option to disable fault handler in the command line.""" pytester.makepyfile( From 6806091b9346e930a8029f4c07b66a987db9855a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 16:41:17 +0000 Subject: [PATCH 0418/2846] [pre-commit.ci] pre-commit autoupdate --- .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 c8e19b283f8..9130a79a06b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.790 + rev: v0.800 hooks: - id: mypy files: ^(src/|testing/) From dfe933cdb4a1885f98c0067701c06c34aa388006 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 25 Jan 2021 15:21:08 -0300 Subject: [PATCH 0419/2846] Remove mypy workaround after 0.800 update --- src/_pytest/threadexception.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/_pytest/threadexception.py b/src/_pytest/threadexception.py index d084dc6e6a2..b250a52346f 100644 --- a/src/_pytest/threadexception.py +++ b/src/_pytest/threadexception.py @@ -34,11 +34,10 @@ class catch_threading_exception: """ 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 + self.args: Optional["threading.ExceptHookArgs"] = None + self._old_hook: Optional[Callable[["threading.ExceptHookArgs"], Any]] = None - def _hook(self, args: "threading._ExceptHookArgs") -> None: + def _hook(self, args: "threading.ExceptHookArgs") -> None: self.args = args def __enter__(self) -> "catch_threading_exception": From 8bb3977cb6f8d422dd0d469d9d3c8dbb87704d4f Mon Sep 17 00:00:00 2001 From: Hong Xu Date: Tue, 26 Jan 2021 00:48:01 -0800 Subject: [PATCH 0420/2846] Doc: Move the module declaration to index.rst When the declaration stays in reference.rst, it creates duplicated "pytest" symbols such as `pytest.pytest.mark.filterwarnings`. --- doc/en/index.rst | 1 + doc/en/reference.rst | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index 58f6c1d86c7..7c4d9394de9 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -11,6 +11,7 @@ pytest: helps you write better programs ======================================= +.. module:: pytest The ``pytest`` framework makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 51c52b33ae9..bc6c5670a5c 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -3,8 +3,6 @@ API Reference ============= -.. module:: pytest - This page contains the full reference to pytest's API. .. contents:: From 56cea26445095c5be7376dd9a6693d2f976b24ed Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 27 Jan 2021 10:23:18 +0100 Subject: [PATCH 0421/2846] Doc: Fix typo --- doc/en/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/usage.rst b/doc/en/usage.rst index fbd3333dabc..0a26182d451 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -497,7 +497,7 @@ The plugins are automatically enabled for pytest runs, unless the ``-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` +The warnings may be silenced selectively using the :ref:`pytest.mark.filterwarnings ref` mark. The warning categories are :class:`pytest.PytestUnraisableExceptionWarning` and :class:`pytest.PytestUnhandledThreadExceptionWarning`. From 0b510bcc5173ae8f3f29750d2097c6ef07a5a142 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 29 Jan 2021 16:06:36 +0200 Subject: [PATCH 0422/2846] changelog: fix missing tick Messes with the rendering. --- changelog/8192.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/8192.bugfix.rst b/changelog/8192.bugfix.rst index 5b26ecbe45c..8920b200a21 100644 --- a/changelog/8192.bugfix.rst +++ b/changelog/8192.bugfix.rst @@ -1,3 +1,3 @@ -``testdir.makefile` now silently accepts values which don't start with ``.`` to maintain backward compatibility with older pytest versions. +``testdir.makefile`` now silently accepts values which don't start with ``.`` to maintain backward compatibility with older pytest versions. ``pytester.makefile`` now issues a clearer error if the ``.`` is missing in the ``ext`` argument. From beda7a8a31a690a50d98e14263fcb2348ecb8bd6 Mon Sep 17 00:00:00 2001 From: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Date: Fri, 29 Jan 2021 15:19:54 +0100 Subject: [PATCH 0423/2846] Add plugin list --- .github/workflows/update-plugin-list.yml | 33 + README.rst | 2 +- changelog/5105.doc.rst | 1 + doc/en/_templates/globaltoc.html | 1 + doc/en/_templates/links.html | 1 - doc/en/contents.rst | 1 + doc/en/index.rst | 2 +- doc/en/plugin_list.rst | 831 +++++++++++++++++++++++ doc/en/plugins.rst | 2 +- doc/en/writing_plugins.rst | 2 +- scripts/update-plugin-list.py | 86 +++ 11 files changed, 957 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/update-plugin-list.yml create mode 100644 changelog/5105.doc.rst create mode 100644 doc/en/plugin_list.rst create mode 100644 scripts/update-plugin-list.py diff --git a/.github/workflows/update-plugin-list.yml b/.github/workflows/update-plugin-list.yml new file mode 100644 index 00000000000..10b5cb99478 --- /dev/null +++ b/.github/workflows/update-plugin-list.yml @@ -0,0 +1,33 @@ +name: Update Plugin List + +on: + schedule: + # Run daily at midnight. + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + createPullRequest: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install packaging requests tabulate[widechars] + - name: Update Plugin List + run: python scripts/update-plugin-list.py + - name: Create Pull Request + uses: peter-evans/create-pull-request@2455e1596942c2902952003bbb574afbbe2ab2e6 + with: + commit-message: '[automated] Update plugin list' + branch: update-plugin-list/patch + delete-branch: true + branch-suffix: short-commit-hash + title: '[automated] Update plugin list' + body: '[automated] Update plugin list' diff --git a/README.rst b/README.rst index 778faf89e50..159bf1d4f75 100644 --- a/README.rst +++ b/README.rst @@ -93,7 +93,7 @@ Features - Python 3.6+ 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/changelog/5105.doc.rst b/changelog/5105.doc.rst new file mode 100644 index 00000000000..f0cc8bab7b4 --- /dev/null +++ b/changelog/5105.doc.rst @@ -0,0 +1 @@ +Add automatically generated :doc:`plugin_list`. The list is updated on a periodic schedule. diff --git a/doc/en/_templates/globaltoc.html b/doc/en/_templates/globaltoc.html index 4522eb2dec9..5fc1ea13e5b 100644 --- a/doc/en/_templates/globaltoc.html +++ b/doc/en/_templates/globaltoc.html @@ -7,6 +7,7 @@

{{ _('Table Of Contents') }}

  • API Reference
  • Examples
  • Customize
  • +
  • 3rd party plugins
  • Changelog
  • Contributing
  • Backwards Compatibility
  • diff --git a/doc/en/_templates/links.html b/doc/en/_templates/links.html index 6f27757a348..c253ecabfd2 100644 --- a/doc/en/_templates/links.html +++ b/doc/en/_templates/links.html @@ -2,7 +2,6 @@

    Useful Links

    diff --git a/doc/en/contents.rst b/doc/en/contents.rst index 58a08744ced..a439f1eda96 100644 --- a/doc/en/contents.rst +++ b/doc/en/contents.rst @@ -28,6 +28,7 @@ Full pytest documentation nose xunit_setup plugins + plugin_list writing_plugins logging reference diff --git a/doc/en/index.rst b/doc/en/index.rst index 7c4d9394de9..f74ef90a785 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -72,7 +72,7 @@ Features - Python 3.6+ and PyPy 3 -- Rich plugin architecture, with over 315+ `external plugins `_ and thriving community +- Rich plugin architecture, with over 315+ :doc:`external plugins ` and thriving community Documentation diff --git a/doc/en/plugin_list.rst b/doc/en/plugin_list.rst new file mode 100644 index 00000000000..927838ac942 --- /dev/null +++ b/doc/en/plugin_list.rst @@ -0,0 +1,831 @@ +Plugins List +============ + +PyPI projects that match "pytest-\*" are considered plugins and are listed +automatically. Packages classified as inactive are excluded. +This list contains 820 plugins. + +============================================================================================================== ======================================================================================================================================================================== ============== ===================== ============================================ +name summary last release status requires +============================================================================================================== ======================================================================================================================================================================== ============== ===================== ============================================ +`pytest-adaptavist `_ pytest plugin for generating test execution results within Jira Test Management (tm4j) Feb 05, 2020 N/A pytest (>=3.4.1) +`pytest-adf `_ Pytest plugin for writing Azure Data Factory integration tests Jun 03, 2020 4 - Beta pytest (>=3.5.0) +`pytest-aggreport `_ pytest plugin for pytest-repeat that generate aggregate report of the same test cases with additional statistics details. Jul 19, 2019 4 - Beta pytest (>=4.3.1) +`pytest-aiofiles `_ pytest fixtures for writing aiofiles tests with pyfakefs May 14, 2017 5 - Production/Stable N/A +`pytest-aiohttp `_ pytest plugin for aiohttp support Dec 05, 2017 N/A pytest +`pytest-aiohttp-client `_ Pytest `client` fixture for the Aiohttp Nov 01, 2020 N/A pytest (>=6) +`pytest-aioresponses `_ py.test integration for aioresponses Dec 21, 2020 4 - Beta pytest (>=3.5.0) +`pytest-aioworkers `_ A plugin to test aioworkers project with pytest Dec 04, 2019 4 - Beta pytest (>=3.5.0) +`pytest-airflow `_ pytest support for airflow. Apr 03, 2019 3 - Alpha pytest (>=4.4.0) +`pytest-alembic `_ A pytest plugin for verifying alembic migrations. Jul 13, 2020 N/A pytest (>=1.0) +`pytest-allclose `_ Pytest fixture extending Numpy's allclose function Jul 30, 2019 5 - Production/Stable pytest +`pytest-allure-adaptor `_ Plugin for py.test to generate allure xml reports Jan 10, 2018 N/A pytest (>=2.7.3) +`pytest-allure-adaptor2 `_ Plugin for py.test to generate allure xml reports Oct 14, 2020 N/A pytest (>=2.7.3) +`pytest-allure-dsl `_ pytest plugin to test case doc string dls instructions Oct 25, 2020 4 - Beta pytest +`pytest-alphamoon `_ Static code checks used at Alphamoon Nov 20, 2020 4 - Beta pytest (>=3.5.0) +`pytest-android `_ This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2. Feb 21, 2019 3 - Alpha pytest +`pytest-annotate `_ pytest-annotate: Generate PyAnnotate annotations from your pytest tests. Aug 23, 2019 3 - Alpha pytest (<6.0.0,>=3.2.0) +`pytest-ansible `_ Plugin for py.test to simplify calling ansible modules from tests or fixtures Oct 26, 2020 5 - Production/Stable pytest +`pytest-ansible-playbook `_ Pytest fixture which runs given ansible playbook file. Mar 08, 2019 4 - Beta N/A +`pytest-ansible-playbook-runner `_ Pytest fixture which runs given ansible playbook file. Dec 02, 2020 4 - Beta pytest (>=3.1.0) +`pytest-antilru `_ Bust functools.lru_cache when running pytest to avoid test pollution Apr 11, 2019 5 - Production/Stable pytest +`pytest-anything `_ Pytest fixtures to assert anything and something Apr 03, 2020 N/A N/A +`pytest-aoc `_ Downloads puzzle inputs for Advent of Code and synthesizes PyTest fixtures Dec 01, 2020 N/A pytest ; extra == 'dev' +`pytest-apistellar `_ apistellar plugin for pytest. Jun 18, 2019 N/A N/A +`pytest-appengine `_ AppEngine integration that works well with pytest-django Feb 27, 2017 N/A N/A +`pytest-appium `_ Pytest plugin for appium Dec 05, 2019 N/A N/A +`pytest-approvaltests `_ A plugin to use approvaltests with pytest Aug 02, 2019 4 - Beta N/A +`pytest-arraydiff `_ pytest plugin to help with comparing array output from tests Dec 06, 2018 4 - Beta pytest +`pytest-asgi-server `_ Convenient ASGI client/server fixtures for Pytest Dec 12, 2020 N/A pytest (>=5.4.1) +`pytest-asptest `_ test Answer Set Programming programs Apr 28, 2018 4 - Beta N/A +`pytest-assertutil `_ pytest-assertutil May 10, 2019 N/A N/A +`pytest-assert-utils `_ Useful assertion utilities for use with pytest Aug 25, 2020 3 - Alpha N/A +`pytest-assume `_ A pytest plugin that allows multiple failures per test Dec 08, 2020 N/A pytest (>=2.7) +`pytest-ast-back-to-python `_ A plugin for pytest devs to view how assertion rewriting recodes the AST Sep 29, 2019 4 - Beta N/A +`pytest-astropy `_ Meta-package containing dependencies for testing Jan 16, 2020 5 - Production/Stable pytest (>=4.6) +`pytest-astropy-header `_ pytest plugin to add diagnostic information to the header of the test output Dec 18, 2019 3 - Alpha pytest (>=2.8) +`pytest-ast-transformer `_ May 04, 2019 3 - Alpha pytest +`pytest-asyncio `_ Pytest support for asyncio. Jun 23, 2020 4 - Beta pytest (>=5.4.0) +`pytest-asyncio-cooperative `_ Run all your asynchronous tests cooperatively. Jan 03, 2021 4 - Beta N/A +`pytest-asyncio-network-simulator `_ pytest-asyncio-network-simulator: Plugin for pytest for simulator the network in tests Jul 31, 2018 3 - Alpha pytest (<3.7.0,>=3.3.2) +`pytest-async-mongodb `_ pytest plugin for async MongoDB Oct 18, 2017 5 - Production/Stable pytest (>=2.5.2) +`pytest-atomic `_ Skip rest of tests if previous test failed. Nov 24, 2018 4 - Beta N/A +`pytest-attrib `_ pytest plugin to select tests based on attributes similar to the nose-attrib plugin May 24, 2016 4 - Beta N/A +`pytest-austin `_ Austin plugin for pytest Oct 11, 2020 4 - Beta N/A +`pytest-autochecklog `_ automatically check condition and log all the checks Apr 25, 2015 4 - Beta N/A +`pytest-automock `_ Pytest plugin for automatical mocks creation Apr 22, 2020 N/A pytest ; extra == 'dev' +`pytest-auto-parametrize `_ pytest plugin: avoid repeating arguments in parametrize Oct 02, 2016 3 - Alpha N/A +`pytest-avoidance `_ Makes pytest skip tests that don not need rerunning May 23, 2019 4 - Beta pytest (>=3.5.0) +`pytest-aws `_ pytest plugin for testing AWS resource configurations Oct 04, 2017 4 - Beta N/A +`pytest-axe `_ pytest plugin for axe-selenium-python Nov 12, 2018 N/A pytest (>=3.0.0) +`pytest-azurepipelines `_ Formatting PyTest output for Azure Pipelines UI Jul 23, 2020 4 - Beta pytest (>=3.5.0) +`pytest-bandit `_ A bandit plugin for pytest Sep 25, 2019 4 - Beta pytest (>=3.5.0) +`pytest-base-url `_ pytest plugin for URL based testing Jun 19, 2020 5 - Production/Stable pytest (>=2.7.3) +`pytest-bdd `_ BDD for pytest Dec 07, 2020 6 - Mature pytest (>=4.3) +`pytest-bdd-splinter `_ Common steps for pytest bdd and splinter integration Aug 12, 2019 5 - Production/Stable pytest (>=4.0.0) +`pytest-bdd-web `_ A simple plugin to use with pytest Jan 02, 2020 4 - Beta pytest (>=3.5.0) +`pytest-bdd-wrappers `_ Feb 11, 2020 2 - Pre-Alpha N/A +`pytest-beakerlib `_ A pytest plugin that reports test results to the BeakerLib framework Mar 17, 2017 5 - Production/Stable pytest +`pytest-beds `_ Fixtures for testing Google Appengine (GAE) apps Jun 07, 2016 4 - Beta N/A +`pytest-bench `_ Benchmark utility that plugs into pytest. Jul 21, 2014 3 - Alpha N/A +`pytest-benchmark `_ A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer. See calibration and FAQ. Jan 10, 2020 5 - Production/Stable pytest (>=3.8) +`pytest-bigchaindb `_ A BigchainDB plugin for pytest. Jan 10, 2020 4 - Beta N/A +`pytest-black `_ A pytest plugin to enable format checking with black Oct 05, 2020 4 - Beta N/A +`pytest-black-multipy `_ Allow '--black' on older Pythons Jan 14, 2021 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' +`pytest-blame `_ A pytest plugin helps developers to debug by providing useful commits history. May 04, 2019 N/A pytest (>=4.4.0) +`pytest-blink1 `_ Pytest plugin to emit notifications via the Blink(1) RGB LED Jan 07, 2018 4 - Beta N/A +`pytest-blockage `_ Disable network requests during a test run. Feb 13, 2019 N/A pytest +`pytest-blocker `_ pytest plugin to mark a test as blocker and skip all other tests Sep 07, 2015 4 - Beta N/A +`pytest-board `_ Local continuous test runner with pytest and watchdog. Jan 20, 2019 N/A N/A +`pytest-bpdb `_ A py.test plug-in to enable drop to bpdb debugger on test failure. Jan 19, 2015 2 - Pre-Alpha N/A +`pytest-bravado `_ Pytest-bravado automatically generates from OpenAPI specification client fixtures. Jan 20, 2021 N/A N/A +`pytest-breed-adapter `_ A simple plugin to connect with breed-server Nov 07, 2018 4 - Beta pytest (>=3.5.0) +`pytest-briefcase `_ A pytest plugin for running tests on a Briefcase project. Jun 14, 2020 4 - Beta pytest (>=3.5.0) +`pytest-browser `_ A pytest plugin for console based browser test selection just after the collection phase Dec 10, 2016 3 - Alpha N/A +`pytest-browsermob-proxy `_ BrowserMob proxy plugin for py.test. Jun 11, 2013 4 - Beta N/A +`pytest-browserstack-local `_ ``py.test`` plugin to run ``BrowserStackLocal`` in background. Feb 09, 2018 N/A N/A +`pytest-bug `_ Pytest plugin for marking tests as a bug Jun 02, 2020 5 - Production/Stable pytest (>=3.6.0) +`pytest-bugzilla `_ py.test bugzilla integration plugin May 05, 2010 4 - Beta N/A +`pytest-bugzilla-notifier `_ A plugin that allows you to execute create, update, and read information from BugZilla bugs Jun 15, 2018 4 - Beta pytest (>=2.9.2) +`pytest-buildkite `_ Plugin for pytest that automatically publishes coverage and pytest report annotations to Buildkite. Jul 13, 2019 4 - Beta pytest (>=3.5.0) +`pytest-bwrap `_ Run your tests in Bubblewrap sandboxes Oct 26, 2018 3 - Alpha N/A +`pytest-cache `_ pytest plugin with mechanisms for caching across test runs Jun 04, 2013 3 - Alpha N/A +`pytest-cagoule `_ Pytest plugin to only run tests affected by changes Jan 01, 2020 3 - Alpha N/A +`pytest-camel-collect `_ Enable CamelCase-aware pytest class collection Aug 02, 2020 N/A pytest (>=2.9) +`pytest-canonical-data `_ A plugin which allows to compare results with canonical results, based on previous runs May 08, 2020 2 - Pre-Alpha pytest (>=3.5.0) +`pytest-caprng `_ A plugin that replays pRNG state on failure. May 02, 2018 4 - Beta N/A +`pytest-capture-deprecatedwarnings `_ pytest plugin to capture all deprecatedwarnings and put them in one file Apr 30, 2019 N/A N/A +`pytest-cases `_ Separate test code from test cases in pytest. Jan 25, 2021 5 - Production/Stable N/A +`pytest-cassandra `_ Cassandra CCM Test Fixtures for pytest Nov 04, 2017 1 - Planning N/A +`pytest-catchlog `_ py.test plugin to catch log messages. This is a fork of pytest-capturelog. Jan 24, 2016 4 - Beta pytest (>=2.6) +`pytest-catch-server `_ Pytest plugin with server for catching HTTP requests. Dec 12, 2019 5 - Production/Stable N/A +`pytest-celery `_ pytest-celery a shim pytest plugin to enable celery.contrib.pytest Aug 05, 2020 N/A N/A +`pytest-chalice `_ A set of py.test fixtures for AWS Chalice Jul 01, 2020 4 - Beta N/A +`pytest-change-report `_ turn . into √,turn F into x Sep 14, 2020 N/A pytest +`pytest-chdir `_ A pytest fixture for changing current working directory Jan 28, 2020 N/A pytest (>=5.0.0,<6.0.0) +`pytest-check `_ A pytest plugin that allows multiple failures per test. Dec 27, 2020 5 - Production/Stable N/A +`pytest-checkdocs `_ check the README when running tests Jan 01, 2021 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' +`pytest-checkipdb `_ plugin to check if there are ipdb debugs left Jul 22, 2020 5 - Production/Stable pytest (>=2.9.2) +`pytest-check-links `_ Check links in files Jul 29, 2020 N/A N/A +`pytest-check-mk `_ pytest plugin to test Check_MK checks Nov 19, 2015 4 - Beta pytest +`pytest-circleci `_ py.test plugin for CircleCI May 03, 2019 N/A N/A +`pytest-circleci-parallelized `_ Parallelize pytest across CircleCI workers. Mar 26, 2019 N/A N/A +`pytest-ckan `_ Backport of CKAN 2.9 pytest plugin and fixtures to CAKN 2.8 Apr 28, 2020 4 - Beta pytest +`pytest-clarity `_ A plugin providing an alternative, colourful diff output for failing assertions. Jan 23, 2020 3 - Alpha N/A +`pytest-cldf `_ Easy quality control for CLDF datasets using pytest May 06, 2019 N/A N/A +`pytest-click `_ Py.test plugin for Click Aug 29, 2020 5 - Production/Stable pytest (>=5.0) +`pytest-clld `_ May 06, 2020 N/A pytest (>=3.6) +`pytest-cloud `_ Distributed tests planner plugin for pytest testing framework. Oct 05, 2020 6 - Mature N/A +`pytest-cloudflare-worker `_ pytest plugin for testing cloudflare workers Oct 19, 2020 4 - Beta pytest (>=6.0.0) +`pytest-cobra `_ PyTest plugin for testing Smart Contracts for Ethereum blockchain. Jun 29, 2019 3 - Alpha pytest (<4.0.0,>=3.7.1) +`pytest-codecheckers `_ pytest plugin to add source code sanity checks (pep8 and friends) Feb 13, 2010 N/A N/A +`pytest-codegen `_ Automatically create pytest test signatures Aug 23, 2020 2 - Pre-Alpha N/A +`pytest-codestyle `_ pytest plugin to run pycodestyle Mar 23, 2020 3 - Alpha N/A +`pytest-collect-formatter `_ Formatter for pytest collect output Nov 19, 2020 5 - Production/Stable N/A +`pytest-colordots `_ Colorizes the progress indicators Oct 06, 2017 5 - Production/Stable N/A +`pytest-commander `_ An interactive GUI test runner for PyTest Nov 21, 2020 N/A pytest (>=5.0.0) +`pytest-common-subject `_ pytest framework for testing different aspects of a common method Nov 12, 2020 N/A pytest (>=3.6,<7) +`pytest-concurrent `_ Concurrently execute test cases with multithread, multiprocess and gevent Jan 12, 2019 4 - Beta pytest (>=3.1.1) +`pytest-config `_ Base configurations and utilities for developing your Python project test suite with pytest. Nov 07, 2014 5 - Production/Stable N/A +`pytest-confluence-report `_ Package stands for pytest plugin to upload results into Confluence page. Nov 06, 2020 N/A N/A +`pytest-console-scripts `_ Pytest plugin for testing console scripts Nov 20, 2020 4 - Beta N/A +`pytest-consul `_ pytest plugin with fixtures for testing consul aware apps Nov 24, 2018 3 - Alpha pytest +`pytest-contextfixture `_ Define pytest fixtures as context managers. Mar 12, 2013 4 - Beta N/A +`pytest-contexts `_ A plugin to run tests written with the Contexts framework using pytest Jul 23, 2018 4 - Beta N/A +`pytest-cookies `_ The pytest plugin for your Cookiecutter templates. 🍪 Feb 14, 2020 5 - Production/Stable pytest (<6.0.0,>=3.3.0) +`pytest-couchdbkit `_ py.test extension for per-test couchdb databases using couchdbkit Apr 17, 2012 N/A N/A +`pytest-count `_ count erros and send email Jan 12, 2018 4 - Beta N/A +`pytest-cov `_ Pytest plugin for measuring coverage. Jan 20, 2021 5 - Production/Stable pytest (>=4.6) +`pytest-cover `_ Pytest plugin for measuring coverage. Forked from `pytest-cov`. Aug 01, 2015 5 - Production/Stable N/A +`pytest-coverage `_ Jun 17, 2015 N/A N/A +`pytest-coverage-context `_ Coverage dynamic context support for PyTest, including sub-processes Jan 04, 2021 4 - Beta pytest (>=6.1.0) +`pytest-cov-exclude `_ Pytest plugin for excluding tests based on coverage data Apr 29, 2016 4 - Beta pytest (>=2.8.0,<2.9.0); extra == 'dev' +`pytest-cpp `_ Use pytest's runner to discover and execute C++ tests Dec 10, 2020 4 - Beta pytest (!=5.4.0,!=5.4.1) +`pytest-cram `_ Run cram tests with pytest. Aug 08, 2020 N/A N/A +`pytest-crate `_ Manages CrateDB instances during your integration tests May 28, 2019 3 - Alpha pytest (>=4.0) +`pytest-cricri `_ A Cricri plugin for pytest. Jan 27, 2018 N/A pytest +`pytest-crontab `_ add crontab task in crontab Dec 09, 2019 N/A N/A +`pytest-csv `_ CSV output for pytest. Jun 24, 2019 N/A pytest (>=4.4) +`pytest-curio `_ Pytest support for curio. Oct 07, 2020 N/A N/A +`pytest-curl-report `_ pytest plugin to generate curl command line report Dec 11, 2016 4 - Beta N/A +`pytest-custom-exit-code `_ Exit pytest test session with custom exit code in different scenarios Aug 07, 2019 4 - Beta pytest (>=4.0.2) +`pytest-custom-report `_ Configure the symbols displayed for test outcomes Jan 30, 2019 N/A pytest +`pytest-cython `_ A plugin for testing Cython extension modules Jan 26, 2021 4 - Beta pytest (>=2.7.3) +`pytest-darker `_ A pytest plugin for checking of modified code using Darker Aug 16, 2020 N/A pytest (>=6.0.1) ; extra == 'test' +`pytest-dash `_ pytest fixtures to run dash applications. Mar 18, 2019 N/A N/A +`pytest-data `_ Useful functions for managing data for pytest fixtures Nov 01, 2016 5 - Production/Stable N/A +`pytest-databricks `_ Pytest plugin for remote Databricks notebooks testing Jul 29, 2020 N/A pytest +`pytest-datadir `_ pytest plugin for test data directories and files Oct 22, 2019 5 - Production/Stable pytest (>=2.7.0) +`pytest-datadir-mgr `_ Manager for test data providing downloads, caching of generated files, and a context for temp directories. Jan 08, 2021 5 - Production/Stable pytest (>=6.0.1,<7.0.0) +`pytest-datadir-ng `_ Fixtures for pytest allowing test functions/methods to easily retrieve test resources from the local filesystem. Dec 25, 2019 5 - Production/Stable pytest +`pytest-data-file `_ Fixture "data" and "case_data" for test from yaml file Dec 04, 2019 N/A N/A +`pytest-datafiles `_ py.test plugin to create a 'tmpdir' containing predefined files/directories. Oct 07, 2018 5 - Production/Stable pytest (>=3.6) +`pytest-datafixtures `_ Data fixtures for pytest made simple Dec 05, 2020 5 - Production/Stable N/A +`pytest-dataplugin `_ A pytest plugin for managing an archive of test data. Sep 16, 2017 1 - Planning N/A +`pytest-datarecorder `_ A py.test plugin recording and comparing test output. Apr 20, 2020 5 - Production/Stable pytest +`pytest-datatest `_ A pytest plugin for test driven data-wrangling (this is the development version of datatest's pytest integration). Oct 15, 2020 4 - Beta pytest (>=3.3) +`pytest-db `_ Session scope fixture "db" for mysql query or change Dec 04, 2019 N/A N/A +`pytest-dbfixtures `_ Databases fixtures plugin for py.test. Dec 07, 2016 4 - Beta N/A +`pytest-dbt-adapter `_ A pytest plugin for testing dbt adapter plugins Jan 07, 2021 N/A pytest (<7,>=6) +`pytest-dbus-notification `_ D-BUS notifications for pytest results. Mar 05, 2014 5 - Production/Stable N/A +`pytest-deadfixtures `_ A simple plugin to list unused fixtures in pytest Jul 23, 2020 5 - Production/Stable N/A +`pytest-dependency `_ Manage dependencies of tests Feb 14, 2020 4 - Beta N/A +`pytest-depends `_ Tests that depend on other tests Apr 05, 2020 5 - Production/Stable pytest (>=3) +`pytest-deprecate `_ Mark tests as testing a deprecated feature with a warning note. Jul 01, 2019 N/A N/A +`pytest-describe `_ Describe-style plugin for pytest Apr 21, 2020 3 - Alpha pytest (>=2.6.0) +`pytest-describe-it `_ plugin for rich text descriptions Jul 19, 2019 4 - Beta pytest +`pytest-devpi-server `_ DevPI server fixture for py.test May 28, 2019 5 - Production/Stable pytest +`pytest-diamond `_ pytest plugin for diamond Aug 31, 2015 4 - Beta N/A +`pytest-dicom `_ pytest plugin to provide DICOM fixtures Dec 19, 2018 3 - Alpha pytest +`pytest-dictsdiff `_ Jul 26, 2019 N/A N/A +`pytest-diff `_ A simple plugin to use with pytest Mar 30, 2019 4 - Beta pytest (>=3.5.0) +`pytest-diffeo `_ Common py.test support for Diffeo packages Apr 08, 2016 3 - Alpha N/A +`pytest-disable `_ pytest plugin to disable a test and skip it from testrun Sep 10, 2015 4 - Beta N/A +`pytest-disable-plugin `_ Disable plugins per test Feb 28, 2019 4 - Beta pytest (>=3.5.0) +`pytest-discord `_ A pytest plugin to notify test results to a Discord channel. Aug 15, 2020 3 - Alpha pytest (!=6.0.0,<7,>=3.3.2) +`pytest-django `_ A Django plugin for pytest. Oct 22, 2020 5 - Production/Stable pytest (>=5.4.0) +`pytest-django-ahead `_ A Django plugin for pytest. Oct 27, 2016 5 - Production/Stable pytest (>=2.9) +`pytest-djangoapp `_ Nice pytest plugin to help you with Django pluggable application testing. Sep 21, 2020 4 - Beta N/A +`pytest-django-cache-xdist `_ A djangocachexdist plugin for pytest May 12, 2020 4 - Beta N/A +`pytest-django-casperjs `_ Integrate CasperJS with your django tests as a pytest fixture. Mar 15, 2015 2 - Pre-Alpha N/A +`pytest-django-dotenv `_ Pytest plugin used to setup environment variables with django-dotenv Nov 26, 2019 4 - Beta pytest (>=2.6.0) +`pytest-django-factories `_ Factories for your Django models that can be used as Pytest fixtures. Nov 12, 2020 4 - Beta N/A +`pytest-django-gcir `_ A Django plugin for pytest. Mar 06, 2018 5 - Production/Stable N/A +`pytest-django-haystack `_ Cleanup your Haystack indexes between tests Sep 03, 2017 5 - Production/Stable pytest (>=2.3.4) +`pytest-django-ifactory `_ A model instance factory for pytest-django Jan 13, 2021 3 - Alpha N/A +`pytest-django-lite `_ The bare minimum to integrate py.test with Django. Jan 30, 2014 N/A N/A +`pytest-django-model `_ A Simple Way to Test your Django Models Feb 14, 2019 4 - Beta N/A +`pytest-django-ordering `_ A pytest plugin for preserving the order in which Django runs tests. Jul 25, 2019 5 - Production/Stable pytest (>=2.3.0) +`pytest-django-queries `_ Generate performance reports from your django database performance tests. Sep 03, 2020 N/A N/A +`pytest-djangorestframework `_ A djangorestframework plugin for pytest Aug 11, 2019 4 - Beta N/A +`pytest-django-rq `_ A pytest plugin to help writing unit test for django-rq Apr 13, 2020 4 - Beta N/A +`pytest-django-sqlcounts `_ py.test plugin for reporting the number of SQLs executed per django testcase. Jun 16, 2015 4 - Beta N/A +`pytest-django-testing-postgresql `_ Use a temporary PostgreSQL database with pytest-django Dec 05, 2019 3 - Alpha N/A +`pytest-doc `_ A documentation plugin for py.test. Jun 28, 2015 5 - Production/Stable N/A +`pytest-docgen `_ An RST Documentation Generator for pytest-based test suites Apr 17, 2020 N/A N/A +`pytest-docker `_ Simple pytest fixtures for Docker and docker-compose based tests Sep 22, 2020 N/A pytest (<7.0,>=4.0) +`pytest-docker-butla `_ Jun 16, 2019 3 - Alpha N/A +`pytest-dockerc `_ Run, manage and stop Docker Compose project from Docker API Oct 09, 2020 5 - Production/Stable pytest (>=3.0) +`pytest-docker-compose `_ Manages Docker containers during your integration tests Jan 26, 2021 5 - Production/Stable pytest (>=3.3) +`pytest-docker-db `_ A plugin to use docker databases for pytests Apr 19, 2020 5 - Production/Stable pytest (>=3.1.1) +`pytest-docker-fixtures `_ pytest docker fixtures Sep 30, 2020 3 - Alpha N/A +`pytest-docker-pexpect `_ pytest plugin for writing functional tests with pexpect and docker Jan 14, 2019 N/A pytest +`pytest-docker-postgresql `_ A simple plugin to use with pytest Sep 24, 2019 4 - Beta pytest (>=3.5.0) +`pytest-docker-py `_ Easy to use, simple to extend, pytest plugin that minimally leverages docker-py. Nov 27, 2018 N/A pytest (==4.0.0) +`pytest-docker-registry-fixtures `_ Pytest fixtures for testing with docker registries. Jan 25, 2021 4 - Beta pytest +`pytest-docker-tools `_ Docker integration tests for pytest Jan 15, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) +`pytest-docs `_ Documentation tool for pytest Nov 11, 2018 4 - Beta pytest (>=3.5.0) +`pytest-docstyle `_ pytest plugin to run pydocstyle Mar 23, 2020 3 - Alpha N/A +`pytest-doctest-custom `_ A py.test plugin for customizing string representations of doctest results. Jul 25, 2016 4 - Beta N/A +`pytest-doctest-ellipsis-markers `_ Setup additional values for ELLIPSIS_MARKER for doctests Jan 12, 2018 4 - Beta N/A +`pytest-doctest-import `_ A simple pytest plugin to import names and add them to the doctest namespace. Nov 13, 2018 4 - Beta pytest (>=3.3.0) +`pytest-doctestplus `_ Pytest plugin with advanced doctest features. Jan 15, 2021 3 - Alpha pytest (>=4.6) +`pytest-doctest-ufunc `_ A plugin to run doctests in docstrings of Numpy ufuncs Aug 02, 2020 4 - Beta pytest (>=3.5.0) +`pytest-dolphin `_ Some extra stuff that we use ininternally Nov 30, 2016 4 - Beta pytest (==3.0.4) +`pytest-doorstop `_ A pytest plugin for adding test results into doorstop items. Jun 09, 2020 4 - Beta pytest (>=3.5.0) +`pytest-dotenv `_ A py.test plugin that parses environment files before running tests Jun 16, 2020 4 - Beta pytest (>=5.0.0) +`pytest-drf `_ A Django REST framework plugin for pytest. Nov 12, 2020 5 - Production/Stable pytest (>=3.6) +`pytest-drivings `_ Tool to allow webdriver automation to be ran locally or remotely Jan 13, 2021 N/A N/A +`pytest-drop-dup-tests `_ A Pytest plugin to drop duplicated tests during collection May 23, 2020 4 - Beta pytest (>=2.7) +`pytest-dump2json `_ A pytest plugin for dumping test results to json. Jun 29, 2015 N/A N/A +`pytest-dynamicrerun `_ A pytest plugin to rerun tests dynamically based off of test outcome and output. Aug 15, 2020 4 - Beta N/A +`pytest-dynamodb `_ DynamoDB fixtures for pytest Feb 20, 2020 5 - Production/Stable pytest (>=3.0.0) +`pytest-easy-addoption `_ pytest-easy-addoption: Easy way to work with pytest addoption Jan 22, 2020 N/A N/A +`pytest-easy-api `_ Simple API testing with pytest Mar 26, 2018 N/A N/A +`pytest-easyMPI `_ Package that supports mpi tests in pytest Oct 21, 2020 N/A N/A +`pytest-easyread `_ pytest plugin that makes terminal printouts of the reports easier to read Nov 17, 2017 N/A N/A +`pytest-ec2 `_ Pytest execution on EC2 instance Oct 22, 2019 3 - Alpha N/A +`pytest-echo `_ pytest plugin with mechanisms for echoing environment variables, package version and generic attributes Jan 08, 2020 5 - Production/Stable N/A +`pytest-elasticsearch `_ Elasticsearch process and client fixtures for py.test. Feb 19, 2020 5 - Production/Stable pytest (>=3.0.0) +`pytest-elements `_ Tool to help automate user interfaces Jan 13, 2021 N/A pytest (>=5.4,<6.0) +`pytest-elk-reporter `_ A simple plugin to use with pytest Jan 24, 2021 4 - Beta pytest (>=3.5.0) +`pytest-email `_ Send execution result email Jul 08, 2020 N/A pytest +`pytest-emoji `_ A pytest plugin that adds emojis to your test result report Feb 19, 2019 4 - Beta pytest (>=4.2.1) +`pytest-emoji-output `_ Pytest plugin to represent test output with emoji support Oct 03, 2020 4 - Beta N/A +`pytest-enabler `_ Enable installed pytest plugins Jan 19, 2021 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' +`pytest-enhancements `_ Improvements for pytest (rejected upstream) Oct 30, 2019 4 - Beta N/A +`pytest-env `_ py.test plugin that allows you to add environment variables. Jun 16, 2017 4 - Beta N/A +`pytest-envfiles `_ A py.test plugin that parses environment files before running tests Oct 08, 2015 3 - Alpha N/A +`pytest-env-info `_ Push information about the running pytest into envvars Nov 25, 2017 4 - Beta pytest (>=3.1.1) +`pytest-envraw `_ py.test plugin that allows you to add environment variables. Aug 27, 2020 4 - Beta pytest (>=2.6.0) +`pytest-envvars `_ Pytest plugin to validate use of envvars on your tests Jun 13, 2020 5 - Production/Stable pytest (>=3.0.0) +`pytest-env-yaml `_ Apr 02, 2019 N/A N/A +`pytest-eradicate `_ pytest plugin to check for commented out code Sep 08, 2020 N/A pytest (>=2.4.2) +`pytest-error-for-skips `_ Pytest plugin to treat skipped tests a test failure Dec 19, 2019 4 - Beta pytest (>=4.6) +`pytest-eth `_ PyTest plugin for testing Smart Contracts for Ethereum Virtual Machine (EVM). Aug 14, 2020 1 - Planning N/A +`pytest-ethereum `_ pytest-ethereum: Pytest library for ethereum projects. Jun 24, 2019 3 - Alpha pytest (==3.3.2); extra == 'dev' +`pytest-eucalyptus `_ Pytest Plugin for BDD Aug 13, 2019 N/A pytest (>=4.2.0) +`pytest-excel `_ pytest plugin for generating excel reports Oct 06, 2020 5 - Production/Stable N/A +`pytest-exceptional `_ Better exceptions Mar 16, 2017 4 - Beta N/A +`pytest-exception-script `_ Walk your code through exception script to check it's resiliency to failures. Aug 04, 2020 3 - Alpha pytest +`pytest-executable `_ pytest plugin for testing executables Aug 10, 2020 4 - Beta pytest (<6.1,>=4.3) +`pytest-expect `_ py.test plugin to store test expectations and mark tests based on them Apr 21, 2016 4 - Beta N/A +`pytest-expecter `_ Better testing with expecter and pytest. Jul 08, 2020 5 - Production/Stable N/A +`pytest-expectr `_ This plugin is used to expect multiple assert using pytest framework. Oct 05, 2018 N/A pytest (>=2.4.2) +`pytest-exploratory `_ Interactive console for pytest. Jan 20, 2021 N/A pytest (>=5.3) +`pytest-external-blockers `_ a special outcome for tests that are blocked for external reasons Oct 04, 2016 N/A N/A +`pytest-extra-durations `_ A pytest plugin to get durations on a per-function basis and per module basis. Apr 21, 2020 4 - Beta pytest (>=3.5.0) +`pytest-fabric `_ Provides test utilities to run fabric task tests by using docker containers Sep 12, 2018 5 - Production/Stable N/A +`pytest-factory `_ Use factories for test setup with py.test Sep 06, 2020 3 - Alpha pytest (>4.3) +`pytest-factoryboy `_ Factory Boy support for pytest. Dec 30, 2020 6 - Mature pytest (>=4.6) +`pytest-factoryboy-fixtures `_ Generates pytest fixtures that allow the use of type hinting Jun 25, 2020 N/A N/A +`pytest-factoryboy-state `_ Simple factoryboy random state management Dec 11, 2020 4 - Beta pytest (>=5.0) +`pytest-failed-to-verify `_ A pytest plugin that helps better distinguishing real test failures from setup flakiness. Aug 08, 2019 5 - Production/Stable pytest (>=4.1.0) +`pytest-faker `_ Faker integration with the pytest framework. Dec 19, 2016 6 - Mature N/A +`pytest-falcon `_ Pytest helpers for Falcon. Sep 07, 2016 4 - Beta N/A +`pytest-falcon-client `_ Pytest `client` fixture for the Falcon Framework Mar 19, 2019 N/A N/A +`pytest-fantasy `_ Pytest plugin for Flask Fantasy Framework Mar 14, 2019 N/A N/A +`pytest-fastapi `_ Dec 27, 2020 N/A N/A +`pytest-fastest `_ Use SCM and coverage to run only needed tests Mar 05, 2020 N/A N/A +`pytest-faulthandler `_ py.test plugin that activates the fault handler module for tests (dummy package) Jul 04, 2019 6 - Mature pytest (>=5.0) +`pytest-fauxfactory `_ Integration of fauxfactory into pytest. Dec 06, 2017 5 - Production/Stable pytest (>=3.2) +`pytest-figleaf `_ py.test figleaf coverage plugin Jan 18, 2010 5 - Production/Stable N/A +`pytest-filedata `_ easily load data from files Jan 17, 2019 4 - Beta N/A +`pytest-filemarker `_ A pytest plugin that runs marked tests when files change. Dec 01, 2020 N/A pytest +`pytest-filter-case `_ run test cases filter by mark Nov 05, 2020 N/A N/A +`pytest-filter-subpackage `_ Pytest plugin for filtering based on sub-packages Jan 09, 2020 3 - Alpha pytest (>=3.0) +`pytest-finer-verdicts `_ A pytest plugin to treat non-assertion failures as test errors. Jun 18, 2020 N/A pytest (>=5.4.3) +`pytest-firefox `_ pytest plugin to manipulate firefox Aug 08, 2017 3 - Alpha pytest (>=3.0.2) +`pytest-fixture-config `_ Fixture configuration utils for py.test May 28, 2019 5 - Production/Stable pytest +`pytest-fixture-marker `_ A pytest plugin to add markers based on fixtures used. Oct 11, 2020 5 - Production/Stable N/A +`pytest-fixture-order `_ pytest plugin to control fixture evaluation order Aug 25, 2020 N/A pytest (>=3.0) +`pytest-fixtures `_ Common fixtures for pytest May 01, 2019 5 - Production/Stable N/A +`pytest-fixture-tools `_ Plugin for pytest which provides tools for fixtures Aug 18, 2020 6 - Mature pytest +`pytest-flake8 `_ pytest plugin to check FLAKE8 requirements Dec 16, 2020 4 - Beta pytest (>=3.5) +`pytest-flake8dir `_ A pytest fixture for testing flake8 plugins. Dec 13, 2020 5 - Production/Stable pytest +`pytest-flakefinder `_ Runs tests multiple times to expose flakiness. Jul 28, 2020 4 - Beta pytest (>=2.7.1) +`pytest-flakes `_ pytest plugin to check source code with pyflakes Nov 28, 2020 5 - Production/Stable N/A +`pytest-flaptastic `_ Flaptastic py.test plugin Mar 17, 2019 N/A N/A +`pytest-flask `_ A set of py.test fixtures to test Flask applications. Nov 09, 2020 5 - Production/Stable pytest (>=5.2) +`pytest-flask-sqlalchemy `_ A pytest plugin for preserving test isolation in Flask-SQlAlchemy using database transactions. Apr 04, 2019 4 - Beta pytest (>=3.2.1) +`pytest-flask-sqlalchemy-transactions `_ Run tests in transactions using pytest, Flask, and SQLalchemy. Aug 02, 2018 4 - Beta pytest (>=3.2.1) +`pytest-focus `_ A pytest plugin that alerts user of failed test cases with screen notifications May 04, 2019 4 - Beta pytest +`pytest-forcefail `_ py.test plugin to make the test failing regardless of pytest.mark.xfail May 15, 2018 4 - Beta N/A +`pytest-forward-compatability `_ A name to avoid typosquating pytest-foward-compatibility Sep 06, 2020 N/A N/A +`pytest-forward-compatibility `_ A pytest plugin to shim pytest commandline options for fowards compatibility Sep 29, 2020 N/A N/A +`pytest-freezegun `_ Wrap tests with fixtures in freeze_time Jul 19, 2020 4 - Beta pytest (>=3.0.0) +`pytest-freeze-reqs `_ Check if requirement files are frozen Nov 14, 2019 N/A N/A +`pytest-func-cov `_ Pytest plugin for measuring function coverage May 24, 2020 3 - Alpha pytest (>=5) +`pytest-fxa `_ pytest plugin for Firefox Accounts Aug 28, 2018 5 - Production/Stable N/A +`pytest-fxtest `_ Oct 27, 2020 N/A N/A +`pytest-gc `_ The garbage collector plugin for py.test Feb 01, 2018 N/A N/A +`pytest-gcov `_ Uses gcov to measure test coverage of a C library Feb 01, 2018 3 - Alpha N/A +`pytest-gevent `_ Ensure that gevent is properly patched when invoking pytest Feb 25, 2020 N/A pytest +`pytest-gherkin `_ A flexible framework for executing BDD gherkin tests Jul 27, 2019 3 - Alpha pytest (>=5.0.0) +`pytest-ghostinspector `_ For finding/executing Ghost Inspector tests May 17, 2016 3 - Alpha N/A +`pytest-girder `_ A set of pytest fixtures for testing Girder applications. Jan 18, 2021 N/A N/A +`pytest-git `_ Git repository fixture for py.test May 28, 2019 5 - Production/Stable pytest +`pytest-gitcov `_ Pytest plugin for reporting on coverage of the last git commit. Jan 11, 2020 2 - Pre-Alpha N/A +`pytest-git-fixtures `_ Pytest fixtures for testing with git. Jan 25, 2021 4 - Beta pytest +`pytest-github `_ Plugin for py.test that associates tests with github issues using a marker. Mar 07, 2019 5 - Production/Stable N/A +`pytest-github-actions-annotate-failures `_ pytest plugin to annotate failed tests with a workflow command for GitHub Actions Oct 13, 2020 N/A pytest (>=4.0.0) +`pytest-gitignore `_ py.test plugin to ignore the same files as git Jul 17, 2015 4 - Beta N/A +`pytest-gnupg-fixtures `_ Pytest fixtures for testing with gnupg. Jan 12, 2021 4 - Beta pytest +`pytest-golden `_ Plugin for pytest that offloads expected outputs to data files Nov 23, 2020 N/A pytest (>=6.1.2,<7.0.0) +`pytest-graphql-schema `_ Get graphql schema as fixture for pytest Oct 18, 2019 N/A N/A +`pytest-greendots `_ Green progress dots Feb 08, 2014 3 - Alpha N/A +`pytest-growl `_ Growl notifications for pytest results. Jan 13, 2014 5 - Production/Stable N/A +`pytest-grpc `_ pytest plugin for grpc May 01, 2020 N/A pytest (>=3.6.0) +`pytest-hammertime `_ Display "🔨 " instead of "." for passed pytest tests. Jul 28, 2018 N/A pytest +`pytest-harvest `_ Store data created during your pytest tests execution, and retrieve it at the end of the session, e.g. for applicative benchmarking purposes. Dec 08, 2020 5 - Production/Stable N/A +`pytest-helm-chart `_ A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. Jun 15, 2020 4 - Beta pytest (>=5.4.2,<6.0.0) +`pytest-helm-charts `_ A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. Dec 22, 2020 4 - Beta pytest (>=6.1.2,<7.0.0) +`pytest-helper `_ Functions to help in using the pytest testing framework May 31, 2019 5 - Production/Stable N/A +`pytest-helpers `_ pytest helpers May 17, 2020 N/A pytest +`pytest-helpers-namespace `_ PyTest Helpers Namespace Jan 07, 2019 5 - Production/Stable pytest (>=2.9.1) +`pytest-hidecaptured `_ Hide captured output May 04, 2018 4 - Beta pytest (>=2.8.5) +`pytest-historic `_ Custom report to display pytest historical execution records Apr 08, 2020 N/A pytest +`pytest-historic-hook `_ Custom listener to store execution results into MYSQL DB, which is used for pytest-historic report Apr 08, 2020 N/A pytest +`pytest-homeassistant `_ A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A +`pytest-homeassistant-custom-component `_ Experimental package to automatically extract test plugins for Home Assistant custom components Jan 05, 2021 3 - Alpha pytest (==6.1.2) +`pytest-honors `_ Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A +`pytest-hoverfly-wrapper `_ Integrates the Hoverfly HTTP proxy into Pytest Oct 25, 2020 4 - Beta N/A +`pytest-html `_ pytest plugin for generating HTML reports Dec 13, 2020 5 - Production/Stable pytest (!=6.0.0,>=5.0) +`pytest-html-lee `_ optimized pytest plugin for generating HTML reports Jun 30, 2020 5 - Production/Stable pytest (>=5.0) +`pytest-html-profiling `_ Pytest plugin for generating HTML reports with per-test profiling and optionally call graph visualizations. Based on pytest-html by Dave Hunt. Feb 11, 2020 5 - Production/Stable pytest (>=3.0) +`pytest-html-reporter `_ Generates a static html report based on pytest framework Sep 28, 2020 N/A N/A +`pytest-html-thread `_ pytest plugin for generating HTML reports Dec 29, 2020 5 - Production/Stable N/A +`pytest-http `_ Fixture "http" for http requests Dec 05, 2019 N/A N/A +`pytest-httpbin `_ Easily test your HTTP library against a local copy of httpbin Feb 11, 2019 5 - Production/Stable N/A +`pytest-http-mocker `_ Pytest plugin for http mocking (via https://github.com/vilus/mocker) Oct 20, 2019 N/A N/A +`pytest-httpretty `_ A thin wrapper of HTTPretty for pytest Feb 16, 2014 3 - Alpha N/A +`pytest-httpserver `_ pytest-httpserver is a httpserver for pytest Oct 18, 2020 3 - Alpha pytest ; extra == 'dev' +`pytest-httpx `_ Send responses to httpx. Nov 25, 2020 5 - Production/Stable pytest (==6.*) +`pytest-hue `_ Visualise PyTest status via your Phillips Hue lights May 09, 2019 N/A N/A +`pytest-hypo-25 `_ help hypo module for pytest Jan 12, 2020 3 - Alpha N/A +`pytest-ibutsu `_ A plugin to sent pytest results to an Ibutsu server Dec 02, 2020 4 - Beta pytest +`pytest-icdiff `_ use icdiff for better error messages in pytest assertions Apr 08, 2020 4 - Beta N/A +`pytest-idapro `_ A pytest plugin for idapython. Allows a pytest setup to run tests outside and inside IDA in an automated manner by runnig pytest inside IDA and by mocking idapython api Nov 03, 2018 N/A N/A +`pytest-ignore-flaky `_ ignore failures from flaky tests (pytest plugin) Jan 14, 2019 5 - Production/Stable pytest (>=3.7) +`pytest-image-diff `_ Sep 03, 2020 3 - Alpha pytest +`pytest-incremental `_ an incremental test runner (pytest plugin) Dec 09, 2018 4 - Beta N/A +`pytest-influxdb `_ Plugin for influxdb and pytest integration. Sep 22, 2020 N/A N/A +`pytest-info-collector `_ pytest plugin to collect information from tests May 26, 2019 3 - Alpha N/A +`pytest-informative-node `_ display more node ininformation. Apr 25, 2019 4 - Beta N/A +`pytest-infrastructure `_ pytest stack validation prior to testing executing Apr 12, 2020 4 - Beta N/A +`pytest-inmanta `_ A py.test plugin providing fixtures to simplify inmanta modules testing. Oct 12, 2020 5 - Production/Stable N/A +`pytest-inmanta-extensions `_ Inmanta tests package Nov 25, 2020 5 - Production/Stable N/A +`pytest-Inomaly `_ A simple image diff plugin for pytest Feb 13, 2018 4 - Beta N/A +`pytest-insta `_ A practical snapshot testing plugin for pytest Nov 29, 2020 N/A pytest (>=6.0.2,<7.0.0) +`pytest-instafail `_ pytest plugin to show failures instantly Jun 14, 2020 4 - Beta pytest (>=2.9) +`pytest-instrument `_ pytest plugin to instrument tests Apr 05, 2020 5 - Production/Stable pytest (>=5.1.0) +`pytest-integration `_ Organizing pytests by integration or not Apr 16, 2020 N/A N/A +`pytest-interactive `_ A pytest plugin for console based interactive test selection just after the collection phase Nov 30, 2017 3 - Alpha N/A +`pytest-invenio `_ Pytest fixtures for Invenio. Dec 17, 2020 5 - Production/Stable pytest (<7,>=6) +`pytest-involve `_ Run tests covering a specific file or changeset Feb 02, 2020 4 - Beta pytest (>=3.5.0) +`pytest-ipdb `_ A py.test plug-in to enable drop to ipdb debugger on test failure. Sep 02, 2014 2 - Pre-Alpha N/A +`pytest-ipynb `_ THIS PROJECT IS ABANDONED Jan 29, 2019 3 - Alpha N/A +`pytest-isort `_ py.test plugin to check import ordering using isort Jan 13, 2021 5 - Production/Stable N/A +`pytest-it `_ Pytest plugin to display test reports as a plaintext spec, inspired by Rspec: https://github.com/mattduck/pytest-it. Jan 22, 2020 4 - Beta N/A +`pytest-iterassert `_ Nicer list and iterable assertion messages for pytest May 11, 2020 3 - Alpha N/A +`pytest-jasmine `_ Run jasmine tests from your pytest test suite Nov 04, 2017 1 - Planning N/A +`pytest-jest `_ A custom jest-pytest oriented Pytest reporter May 22, 2018 4 - Beta pytest (>=3.3.2) +`pytest-jira `_ py.test JIRA integration plugin, using markers Nov 29, 2019 N/A N/A +`pytest-jobserver `_ Limit parallel tests with posix jobserver. May 15, 2019 5 - Production/Stable pytest +`pytest-joke `_ Test failures are better served with humor. Oct 08, 2019 4 - Beta pytest (>=4.2.1) +`pytest-json `_ Generate JSON test reports Jan 18, 2016 4 - Beta N/A +`pytest-jsonlint `_ UNKNOWN Aug 04, 2016 N/A N/A +`pytest-json-report `_ A pytest plugin to report test results as JSON files Oct 23, 2020 4 - Beta pytest (>=4.2.0) +`pytest-kafka `_ Zookeeper, Kafka server, and Kafka consumer fixtures for Pytest Nov 01, 2019 N/A pytest +`pytest-kind `_ Kubernetes test support with KIND for pytest Jan 24, 2021 5 - Production/Stable N/A +`pytest-kivy `_ Kivy GUI tests fixtures using pytest Dec 21, 2020 4 - Beta pytest (>=3.6) +`pytest-knows `_ A pytest plugin that can automaticly skip test case based on dependence info calculated by trace Aug 22, 2014 N/A N/A +`pytest-konira `_ Run Konira DSL tests with py.test Oct 09, 2011 N/A N/A +`pytest-krtech-common `_ pytest krtech common library Nov 28, 2016 4 - Beta N/A +`pytest-kwparametrize `_ Alternate syntax for @pytest.mark.parametrize with test cases as dictionaries and default value fallbacks Jan 22, 2021 N/A pytest (>=6) +`pytest-lambda `_ Define pytest fixtures with lambda functions. Dec 28, 2020 3 - Alpha pytest (>=3.6,<7) +`pytest-lamp `_ Jan 06, 2017 3 - Alpha N/A +`pytest-layab `_ Pytest fixtures for layab. Oct 05, 2020 5 - Production/Stable N/A +`pytest-lazy-fixture `_ It helps to use fixtures in pytest.mark.parametrize Feb 01, 2020 4 - Beta pytest (>=3.2.5) +`pytest-ldap `_ python-ldap fixtures for pytest Aug 18, 2020 N/A pytest +`pytest-leaks `_ A pytest plugin to trace resource leaks. Nov 27, 2019 1 - Planning N/A +`pytest-level `_ Select tests of a given level or lower Oct 21, 2019 N/A pytest +`pytest-libfaketime `_ A python-libfaketime plugin for pytest. Dec 22, 2018 4 - Beta pytest (>=3.0.0) +`pytest-libiio `_ A pytest plugin to manage interfacing with libiio contexts Jan 09, 2021 4 - Beta N/A +`pytest-libnotify `_ Pytest plugin that shows notifications about the test run Nov 12, 2018 3 - Alpha pytest +`pytest-ligo `_ Jan 16, 2020 4 - Beta N/A +`pytest-lineno `_ A pytest plugin to show the line numbers of test functions Dec 04, 2020 N/A pytest +`pytest-lisa `_ Pytest plugin for organizing tests. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) +`pytest-listener `_ A simple network listener May 28, 2019 5 - Production/Stable pytest +`pytest-litf `_ A pytest plugin that stream output in LITF format Jan 18, 2021 4 - Beta pytest (>=3.1.1) +`pytest-live `_ Live results for pytest Mar 08, 2020 N/A pytest +`pytest-localftpserver `_ A PyTest plugin which provides an FTP fixture for your tests Jan 27, 2021 5 - Production/Stable pytest +`pytest-localserver `_ py.test plugin to test server connections locally. Nov 14, 2018 4 - Beta N/A +`pytest-localstack `_ Pytest plugin for AWS integration tests Aug 22, 2019 4 - Beta pytest (>=3.3.0) +`pytest-lockable `_ lockable resource plugin for pytest Oct 05, 2020 3 - Alpha pytest +`pytest-locker `_ Used to lock object during testing. Essentially changing assertions from being hard coded to asserting that nothing changed Aug 11, 2020 N/A pytest (>=5.4) +`pytest-logbook `_ py.test plugin to capture logbook log messages Nov 23, 2015 5 - Production/Stable pytest (>=2.8) +`pytest-logfest `_ Pytest plugin providing three logger fixtures with basic or full writing to log files Jul 21, 2019 4 - Beta pytest (>=3.5.0) +`pytest-logger `_ Plugin configuring handlers for loggers from Python logging module. Jul 25, 2019 4 - Beta pytest (>=3.2) +`pytest-logging `_ Configures logging and allows tweaking the log level with a py.test flag Nov 04, 2015 4 - Beta N/A +`pytest-log-report `_ Package for creating a pytest test run reprot Dec 26, 2019 N/A N/A +`pytest-manual-marker `_ pytest marker for marking manual tests Nov 28, 2018 3 - Alpha pytest +`pytest-markdown `_ Test your markdown docs with pytest Jan 15, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) +`pytest-marker-bugzilla `_ py.test bugzilla integration plugin, using markers Jan 09, 2020 N/A N/A +`pytest-markers-presence `_ A simple plugin to detect missed pytest tags and markers" Dec 21, 2020 4 - Beta pytest (>=6.0) +`pytest-markfiltration `_ UNKNOWN Nov 08, 2011 3 - Alpha N/A +`pytest-mark-no-py3 `_ pytest plugin and bowler codemod to help migrate tests to Python 3 May 17, 2019 N/A pytest +`pytest-marks `_ UNKNOWN Nov 23, 2012 3 - Alpha N/A +`pytest-matcher `_ Match test output against patterns stored in files Apr 23, 2020 5 - Production/Stable pytest (>=3.4) +`pytest-match-skip `_ Skip matching marks. Matches partial marks using wildcards. May 15, 2019 4 - Beta pytest (>=4.4.1) +`pytest-mat-report `_ this is report Jan 20, 2021 N/A N/A +`pytest-matrix `_ Provide tools for generating tests from combinations of fixtures. Jun 24, 2020 5 - Production/Stable pytest (>=5.4.3,<6.0.0) +`pytest-mccabe `_ pytest plugin to run the mccabe code complexity checker. Jul 22, 2020 3 - Alpha pytest (>=5.4.0) +`pytest-md `_ Plugin for generating Markdown reports for pytest results Jul 11, 2019 3 - Alpha pytest (>=4.2.1) +`pytest-md-report `_ A pytest plugin to make a test results report with Markdown table format. Aug 14, 2020 4 - Beta pytest (!=6.0.0,<7,>=3.3.2) +`pytest-memprof `_ Estimates memory consumption of test functions Mar 29, 2019 4 - Beta N/A +`pytest-menu `_ A pytest plugin for console based interactive test selection just after the collection phase Oct 04, 2017 3 - Alpha pytest (>=2.4.2) +`pytest-mercurial `_ pytest plugin to write integration tests for projects using Mercurial Python internals Nov 21, 2020 1 - Planning N/A +`pytest-messenger `_ Pytest to Slack reporting plugin Dec 16, 2020 5 - Production/Stable N/A +`pytest-metadata `_ pytest plugin for test session metadata Nov 27, 2020 5 - Production/Stable pytest (>=2.9.0) +`pytest-metrics `_ Custom metrics report for pytest Apr 04, 2020 N/A pytest +`pytest-mimesis `_ Mimesis integration with the pytest test runner Mar 21, 2020 5 - Production/Stable pytest (>=4.2) +`pytest-minecraft `_ A pytest plugin for running tests against Minecraft releases Sep 26, 2020 N/A pytest (>=6.0.1,<7.0.0) +`pytest-missing-fixtures `_ Pytest plugin that creates missing fixtures Oct 14, 2020 4 - Beta pytest (>=3.5.0) +`pytest-ml `_ Test your machine learning! May 04, 2019 4 - Beta N/A +`pytest-mocha `_ pytest plugin to display test execution output like a mochajs Apr 02, 2020 4 - Beta pytest (>=5.4.0) +`pytest-mock `_ Thin-wrapper around the mock package for easier use with pytest Jan 10, 2021 5 - Production/Stable pytest (>=5.0) +`pytest-mock-api `_ A mock API server with configurable routes and responses available as a fixture. Feb 13, 2019 1 - Planning pytest (>=4.0.0) +`pytest-mock-helper `_ Help you mock HTTP call and generate mock code Jan 24, 2018 N/A pytest +`pytest-mockito `_ Base fixtures for mockito Jul 11, 2018 4 - Beta N/A +`pytest-mockredis `_ An in-memory mock of a Redis server that runs in a separate thread. This is to be used for unit-tests that require a Redis database. Jan 02, 2018 2 - Pre-Alpha N/A +`pytest-mock-resources `_ A pytest plugin for easily instantiating reproducible mock resources. Oct 08, 2020 N/A pytest (>=1.0) +`pytest-mock-server `_ Mock server plugin for pytest Apr 06, 2020 4 - Beta N/A +`pytest-mockservers `_ A set of fixtures to test your requests to HTTP/UDP servers Mar 31, 2020 N/A pytest (>=4.3.0) +`pytest-modifyjunit `_ Utility for adding additional properties to junit xml for IDM QE Jan 10, 2019 N/A N/A +`pytest-modifyscope `_ pytest plugin to modify fixture scope Apr 12, 2020 N/A pytest +`pytest-molecule `_ PyTest Molecule Plugin :: discover and run molecule tests Jan 25, 2021 5 - Production/Stable N/A +`pytest-mongo `_ MongoDB process and client fixtures plugin for py.test. Jan 12, 2021 5 - Production/Stable pytest (>=3.0.0) +`pytest-mongodb `_ pytest plugin for MongoDB fixtures Dec 07, 2019 5 - Production/Stable pytest (>=2.5.2) +`pytest-monitor `_ Pytest plugin for analyzing resource usage. Nov 20, 2020 5 - Production/Stable pytest +`pytest-monkeyplus `_ pytest's monkeypatch subclass with extra functionalities Sep 18, 2012 5 - Production/Stable N/A +`pytest-monkeytype `_ pytest-monkeytype: Generate Monkeytype annotations from your pytest tests. Jul 29, 2020 4 - Beta N/A +`pytest-moto `_ Fixtures for integration tests of AWS services,uses moto mocking library. Aug 28, 2015 1 - Planning N/A +`pytest-mp `_ A test batcher for multiprocessed Pytest runs May 23, 2018 4 - Beta pytest +`pytest-mpi `_ pytest plugin to collect information from tests Jun 20, 2020 3 - Alpha N/A +`pytest-mpl `_ pytest plugin to help with testing figures output from Matplotlib Nov 05, 2020 4 - Beta pytest +`pytest-mproc `_ low-startup-overhead, scalable, distributed-testing pytest plugin Aug 08, 2020 4 - Beta N/A +`pytest-multihost `_ Utility for writing multi-host tests for pytest Apr 07, 2020 4 - Beta N/A +`pytest-multilog `_ Multi-process logs handling and other helpers for pytest Nov 15, 2020 N/A N/A +`pytest-mutagen `_ Add the mutation testing feature to pytest Jul 24, 2020 N/A pytest (>=5.4) +`pytest-mypy `_ Mypy static type checker plugin for Pytest Nov 14, 2020 4 - Beta pytest (>=3.5) +`pytest-mypyd `_ Mypy static type checker plugin for Pytest Aug 20, 2019 4 - Beta pytest (<4.7,>=2.8) ; python_version < "3.5" +`pytest-mypy-plugins `_ pytest plugin for writing tests for mypy plugins Oct 26, 2020 3 - Alpha pytest (>=6.0.0) +`pytest-mypy-testing `_ Pytest plugin to check mypy output. Apr 24, 2020 N/A pytest +`pytest-mysql `_ MySQL process and client fixtures for pytest Jul 21, 2020 5 - Production/Stable pytest (>=3.0.0) +`pytest-needle `_ pytest plugin for visual testing websites using selenium Dec 10, 2018 4 - Beta pytest (<5.0.0,>=3.0.0) +`pytest-neo `_ pytest-neo is a plugin for pytest that shows tests like screen of Matrix. Apr 23, 2019 3 - Alpha pytest (>=3.7.2) +`pytest-network `_ A simple plugin to disable network on socket level. May 07, 2020 N/A N/A +`pytest-nginx `_ nginx fixture for pytest Aug 12, 2017 5 - Production/Stable N/A +`pytest-nginx-iplweb `_ nginx fixture for pytest - iplweb temporary fork Mar 01, 2019 5 - Production/Stable N/A +`pytest-ngrok `_ Jan 22, 2020 3 - Alpha N/A +`pytest-ngsfixtures `_ pytest ngs fixtures Sep 06, 2019 2 - Pre-Alpha pytest (>=5.0.0) +`pytest-nice `_ A pytest plugin that alerts user of failed test cases with screen notifications May 04, 2019 4 - Beta pytest +`pytest-nocustom `_ Run all tests without custom markers May 04, 2019 5 - Production/Stable N/A +`pytest-nodev `_ Test-driven source code search for Python. Jul 21, 2016 4 - Beta pytest (>=2.8.1) +`pytest-notebook `_ A pytest plugin for testing Jupyter Notebooks Sep 16, 2020 4 - Beta pytest (>=3.5.0) +`pytest-notice `_ Send pytest execution result email Nov 05, 2020 N/A N/A +`pytest-notification `_ A pytest plugin for sending a desktop notification and playing a sound upon completion of tests Jun 19, 2020 N/A pytest (>=4) +`pytest-notifier `_ A pytest plugin to notify test result Jun 12, 2020 3 - Alpha pytest +`pytest-notimplemented `_ Pytest markers for not implemented features and tests. Aug 27, 2019 N/A pytest (>=5.1,<6.0) +`pytest-notion `_ A PyTest Reporter to send test runs to Notion.so Aug 07, 2019 N/A N/A +`pytest-nunit `_ A pytest plugin for generating NUnit3 test result XML output Aug 04, 2020 4 - Beta pytest (>=3.5.0) +`pytest-ochrus `_ pytest results data-base and HTML reporter Feb 21, 2018 4 - Beta N/A +`pytest-odoo `_ py.test plugin to run Odoo tests Aug 19, 2020 4 - Beta pytest (>=2.9) +`pytest-odoo-fixtures `_ Project description Jun 25, 2019 N/A N/A +`pytest-oerp `_ pytest plugin to test OpenERP modules Feb 28, 2012 3 - Alpha N/A +`pytest-ok `_ The ultimate pytest output plugin Apr 01, 2019 4 - Beta N/A +`pytest-only `_ Use @pytest.mark.only to run a single test Jan 19, 2020 N/A N/A +`pytest-oot `_ Run object-oriented tests in a simple format Sep 18, 2016 4 - Beta N/A +`pytest-openfiles `_ Pytest plugin for detecting inadvertent open file handles Apr 16, 2020 3 - Alpha pytest (>=4.6) +`pytest-opentmi `_ pytest plugin for publish results to opentmi Jun 10, 2020 5 - Production/Stable pytest (>=5.0) +`pytest-optional `_ include/exclude values of fixtures in pytest Oct 07, 2015 N/A N/A +`pytest-optional-tests `_ Easy declaration of optional tests (i.e., that are not run by default) Jul 09, 2019 4 - Beta pytest (>=4.5.0) +`pytest-orchestration `_ A pytest plugin for orchestrating tests Jul 18, 2019 N/A N/A +`pytest-order `_ pytest plugin to run your tests in a specific order Jan 27, 2021 4 - Beta pytest (>=3.7) +`pytest-ordering `_ pytest plugin to run your tests in a specific order Nov 14, 2018 4 - Beta pytest +`pytest-osxnotify `_ OS X notifications for py.test results. May 15, 2015 N/A N/A +`pytest-pact `_ A simple plugin to use with pytest Jan 07, 2019 4 - Beta N/A +`pytest-parallel `_ a pytest plugin for parallel and concurrent testing Apr 30, 2020 3 - Alpha pytest (>=3.0.0) +`pytest-param `_ pytest plugin to test all, first, last or random params Sep 11, 2016 4 - Beta pytest (>=2.6.0) +`pytest-paramark `_ Configure pytest fixtures using a combination of"parametrize" and markers Jan 10, 2020 4 - Beta pytest (>=4.5.0) +`pytest-parametrization `_ Simpler PyTest parametrization Jul 28, 2019 5 - Production/Stable N/A +`pytest-parametrize-cases `_ A more user-friendly way to write parametrized tests. Dec 12, 2020 N/A pytest (>=6.1.2,<7.0.0) +`pytest-parametrized `_ Pytest plugin for parametrizing tests with default iterables. Oct 19, 2020 5 - Production/Stable pytest +`pytest-parawtf `_ Finally spell paramete?ri[sz]e correctly Dec 03, 2018 4 - Beta pytest (>=3.6.0) +`pytest-pass `_ Check out https://github.com/elilutsky/pytest-pass Dec 04, 2019 N/A N/A +`pytest-paste-config `_ Allow setting the path to a paste config file Sep 18, 2013 3 - Alpha N/A +`pytest-pdb `_ pytest plugin which adds pdb helper commands related to pytest. Jul 31, 2018 N/A N/A +`pytest-peach `_ pytest plugin for fuzzing with Peach API Security Apr 12, 2019 4 - Beta pytest (>=2.8.7) +`pytest-pep257 `_ py.test plugin for pep257 Jul 09, 2016 N/A N/A +`pytest-pep8 `_ pytest plugin to check PEP8 requirements Apr 27, 2014 N/A N/A +`pytest-percent `_ Change the exit code of pytest test sessions when a required percent of tests pass. May 21, 2020 N/A pytest (>=5.2.0) +`pytest-performance `_ A simple plugin to ensure the execution of critical sections of code has not been impacted Sep 11, 2020 5 - Production/Stable pytest (>=3.7.0) +`pytest-pgsql `_ Pytest plugins and helpers for tests using a Postgres database. May 13, 2020 5 - Production/Stable pytest (>=3.0.0) +`pytest-picked `_ Run the tests related to the changed files Dec 23, 2020 N/A pytest (>=3.5.0) +`pytest-pigeonhole `_ Jun 25, 2018 5 - Production/Stable pytest (>=3.4) +`pytest-pikachu `_ Show surprise when tests are passing Sep 30, 2019 4 - Beta pytest +`pytest-pilot `_ Slice in your test base thanks to powerful markers. Oct 09, 2020 5 - Production/Stable N/A +`pytest-pings `_ 🦊 The pytest plugin for Firefox Telemetry 📊 Jun 29, 2019 3 - Alpha pytest (>=5.0.0) +`pytest-pinned `_ A simple pytest plugin for pinning tests Jan 21, 2021 4 - Beta pytest (>=3.5.0) +`pytest-pinpoint `_ A pytest plugin which runs SBFL algorithms to detect faults. Sep 25, 2020 N/A pytest (>=4.4.0) +`pytest-pipeline `_ Pytest plugin for functional testing of data analysispipelines Jan 24, 2017 3 - Alpha N/A +`pytest-platform-markers `_ Markers for pytest to skip tests on specific platforms Sep 09, 2019 4 - Beta pytest (>=3.6.0) +`pytest-play `_ pytest plugin that let you automate actions and assertions with test metrics reporting executing plain YAML files Jun 12, 2019 5 - Production/Stable N/A +`pytest-playbook `_ Pytest plugin for reading playbooks. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) +`pytest-playwright `_ A pytest wrapper with fixtures for Playwright to automate web browsers Jan 21, 2021 N/A pytest +`pytest-plt `_ Fixtures for quickly making Matplotlib plots in tests Aug 17, 2020 5 - Production/Stable pytest +`pytest-plugin-helpers `_ A plugin to help developing and testing other plugins Nov 23, 2019 4 - Beta pytest (>=3.5.0) +`pytest-plus `_ PyTest Plus Plugin :: extends pytest functionality Mar 19, 2020 5 - Production/Stable pytest (>=3.50) +`pytest-pmisc `_ Mar 21, 2019 5 - Production/Stable N/A +`pytest-pointers `_ Pytest plugin to define functions you test with special marks for better navigation and reports Dec 14, 2020 N/A N/A +`pytest-polarion-cfme `_ pytest plugin for collecting test cases and recording test results Nov 13, 2017 3 - Alpha N/A +`pytest-polarion-collect `_ pytest plugin for collecting polarion test cases data Jun 18, 2020 3 - Alpha pytest +`pytest-polecat `_ Provides Polecat pytest fixtures Aug 12, 2019 4 - Beta N/A +`pytest-ponyorm `_ PonyORM in Pytest Oct 31, 2018 N/A pytest (>=3.1.1) +`pytest-poo `_ Visualize your crappy tests Jul 14, 2013 5 - Production/Stable N/A +`pytest-poo-fail `_ Visualize your failed tests with poo Feb 12, 2015 5 - Production/Stable N/A +`pytest-pop `_ A pytest plugin to help with testing pop projects Aug 13, 2020 5 - Production/Stable pytest (>=5.4.0) +`pytest-postgres `_ Run PostgreSQL in Docker container in Pytest. Mar 22, 2020 N/A pytest +`pytest-postgresql `_ Postgresql fixtures and fixture factories for Pytest. Oct 29, 2020 5 - Production/Stable pytest (>=3.0.0) +`pytest-power `_ pytest plugin with powerful fixtures Dec 31, 2020 N/A pytest (>=5.4) +`pytest-pride `_ Minitest-style test colors Apr 02, 2016 3 - Alpha N/A +`pytest-print `_ pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) Oct 23, 2020 5 - Production/Stable pytest (>=3.0.0) +`pytest-profiling `_ Profiling plugin for py.test May 28, 2019 5 - Production/Stable pytest +`pytest-progress `_ pytest plugin for instant test progress status Oct 06, 2020 5 - Production/Stable N/A +`pytest-prometheus `_ Report test pass / failures to a Prometheus PushGateway Oct 03, 2017 N/A N/A +`pytest-prosper `_ Test helpers for Prosper projects Sep 24, 2018 N/A N/A +`pytest-pspec `_ A rspec format reporter for Python ptest Jun 02, 2020 4 - Beta pytest (>=3.0.0) +`pytest-pudb `_ Pytest PuDB debugger integration Oct 25, 2018 3 - Alpha pytest (>=2.0) +`pytest-purkinje `_ py.test plugin for purkinje test runner Oct 28, 2017 2 - Pre-Alpha N/A +`pytest-pycharm `_ Plugin for py.test to enter PyCharm debugger on uncaught exceptions Aug 13, 2020 5 - Production/Stable pytest (>=2.3) +`pytest-pycodestyle `_ pytest plugin to run pycodestyle Aug 10, 2020 3 - Alpha N/A +`pytest-pydev `_ py.test plugin to connect to a remote debug server with PyDev or PyCharm. Nov 15, 2017 3 - Alpha N/A +`pytest-pydocstyle `_ pytest plugin to run pydocstyle Aug 10, 2020 3 - Alpha N/A +`pytest-pylint `_ pytest plugin to check source code with pylint Nov 09, 2020 5 - Production/Stable pytest (>=5.4) +`pytest-pypi `_ Easily test your HTTP library against a local copy of pypi Mar 04, 2018 3 - Alpha N/A +`pytest-pypom-navigation `_ Core engine for cookiecutter-qa and pytest-play packages Feb 18, 2019 4 - Beta pytest (>=3.0.7) +`pytest-pyppeteer `_ A plugin to run pyppeteer in pytest. Nov 27, 2020 4 - Beta pytest (>=6.0.2) +`pytest-pyq `_ Pytest fixture "q" for pyq Mar 10, 2020 5 - Production/Stable N/A +`pytest-pyramid `_ pytest pyramid providing basic fixtures for testing pyramid applications with pytest test suite Jun 05, 2020 4 - Beta pytest +`pytest-pyramid-server `_ Pyramid server fixture for py.test May 28, 2019 5 - Production/Stable pytest +`pytest-pytestrail `_ Pytest plugin for interaction with TestRail Aug 27, 2020 4 - Beta pytest (>=3.8.0) +`pytest-pythonpath `_ pytest plugin for adding to the PYTHONPATH from command line or configs. Aug 22, 2018 5 - Production/Stable N/A +`pytest-qml `_ Run QML Tests with pytest Dec 02, 2020 4 - Beta pytest (>=6.0.0) +`pytest-qt `_ pytest support for PyQt and PySide applications Dec 07, 2019 5 - Production/Stable pytest (>=3.0.0) +`pytest-qt-app `_ QT app fixture for py.test Dec 23, 2015 5 - Production/Stable N/A +`pytest-quarantine `_ A plugin for pytest to manage expected test failures Nov 24, 2019 5 - Production/Stable pytest (>=4.6) +`pytest-quickcheck `_ pytest plugin to generate random data inspired by QuickCheck Nov 15, 2020 4 - Beta pytest (<6.0.0,>=4.0) +`pytest-rabbitmq `_ RabbitMQ process and client fixtures for pytest Jan 11, 2021 5 - Production/Stable pytest (>=3.0.0) +`pytest-race `_ Race conditions tester for pytest Nov 21, 2016 4 - Beta N/A +`pytest-rage `_ pytest plugin to implement PEP712 Oct 21, 2011 3 - Alpha N/A +`pytest-raises `_ An implementation of pytest.raises as a pytest.mark fixture Apr 23, 2020 N/A pytest (>=3.2.2) +`pytest-raisesregexp `_ Simple pytest plugin to look for regex in Exceptions Dec 18, 2015 N/A N/A +`pytest-raisin `_ Plugin enabling the use of exception instances with pytest.raises Jun 25, 2020 N/A pytest +`pytest-random `_ py.test plugin to randomize tests Apr 28, 2013 3 - Alpha N/A +`pytest-randomly `_ Pytest plugin to randomly order tests and control random.seed. Nov 16, 2020 5 - Production/Stable pytest +`pytest-randomness `_ Pytest plugin about random seed management May 30, 2019 3 - Alpha N/A +`pytest-random-num `_ Randomise the order in which pytest tests are run with some control over the randomness Oct 19, 2020 5 - Production/Stable N/A +`pytest-random-order `_ Randomise the order in which pytest tests are run with some control over the randomness Nov 30, 2018 5 - Production/Stable pytest (>=3.0.0) +`pytest-readme `_ Test your README.md file Dec 28, 2014 5 - Production/Stable N/A +`pytest-reana `_ Pytest fixtures for REANA. Nov 24, 2020 3 - Alpha N/A +`pytest-recording `_ A pytest plugin that allows you recording of network interactions via VCR.py Nov 25, 2020 4 - Beta pytest (>=3.5.0) +`pytest-recordings `_ Provides pytest plugins for reporting request/response traffic, screenshots, and more to ReportPortal Aug 13, 2020 N/A N/A +`pytest-redis `_ Redis fixtures and fixture factories for Pytest. Oct 15, 2019 5 - Production/Stable pytest (>=3.0.0) +`pytest-redmine `_ Pytest plugin for redmine Mar 19, 2018 1 - Planning N/A +`pytest-ref `_ A plugin to store reference files to ease regression testing Nov 23, 2019 4 - Beta pytest (>=3.5.0) +`pytest-reference-formatter `_ Conveniently run pytest with a dot-formatted test reference. Oct 01, 2019 4 - Beta N/A +`pytest-regressions `_ Easy to use fixtures to write regression tests. Jan 27, 2021 5 - Production/Stable pytest (>=3.5.0) +`pytest-regtest `_ pytest plugin for regression tests Sep 16, 2020 N/A N/A +`pytest-relaxed `_ Relaxed test discovery/organization for pytest Jun 14, 2019 5 - Production/Stable pytest (<5,>=3) +`pytest-remfiles `_ Pytest plugin to create a temporary directory with remote files Jul 01, 2019 5 - Production/Stable N/A +`pytest-remotedata `_ Pytest plugin for controlling remote data access. Jul 20, 2019 3 - Alpha pytest (>=3.1) +`pytest-remove-stale-bytecode `_ py.test plugin to remove stale byte code files. Mar 04, 2020 4 - Beta pytest +`pytest-reorder `_ Reorder tests depending on their paths and names. May 31, 2018 4 - Beta pytest +`pytest-repeat `_ pytest plugin for repeating tests Oct 31, 2020 5 - Production/Stable pytest (>=3.6) +`pytest-replay `_ Saves previous test runs and allow re-execute previous pytest runs to reproduce crashes or flaky tests Dec 09, 2020 4 - Beta pytest (>=3.0.0) +`pytest-repo-health `_ A pytest plugin to report on repository standards conformance Nov 03, 2020 3 - Alpha pytest +`pytest-report `_ Creates json report that is compatible with atom.io's linter message format May 11, 2016 4 - Beta N/A +`pytest-reporter `_ Generate Pytest reports with templates Nov 05, 2020 4 - Beta pytest +`pytest-reporter-html1 `_ A basic HTML report template for Pytest Nov 02, 2020 4 - Beta N/A +`pytest-reportinfra `_ Pytest plugin for reportinfra Aug 11, 2019 3 - Alpha N/A +`pytest-reporting `_ A plugin to report summarized results in a table format Oct 25, 2019 4 - Beta pytest (>=3.5.0) +`pytest-reportlog `_ Replacement for the --resultlog option, focused in simplicity and extensibility Dec 11, 2020 3 - Alpha pytest (>=5.2) +`pytest-report-me `_ A pytest plugin to generate report. Dec 31, 2020 N/A pytest +`pytest-report-parameters `_ pytest plugin for adding tests' parameters to junit report Jun 18, 2020 3 - Alpha pytest (>=2.4.2) +`pytest-reportportal `_ Agent for Reporting results of tests to the Report Portal Dec 14, 2020 N/A pytest (>=3.0.7) +`pytest-reqs `_ pytest plugin to check pinned requirements May 12, 2019 N/A pytest (>=2.4.2) +`pytest-requests `_ A simple plugin to use with pytest Jun 24, 2019 4 - Beta pytest (>=3.5.0) +`pytest-reraise `_ Make multi-threaded pytest test cases fail when they should Jun 03, 2020 5 - Production/Stable N/A +`pytest-rerun `_ Re-run only changed files in specified branch Jul 08, 2019 N/A pytest (>=3.6) +`pytest-rerunfailures `_ pytest plugin to re-run tests to eliminate flaky failures Sep 29, 2020 5 - Production/Stable pytest (>=5.0) +`pytest-resilient-circuits `_ Resilient Circuits fixtures for PyTest. Jan 21, 2021 N/A N/A +`pytest-resource `_ Load resource fixture plugin to use with pytest Nov 14, 2018 4 - Beta N/A +`pytest-resource-path `_ Provides path for uniform access to test resources in isolated directory Aug 18, 2020 5 - Production/Stable pytest (>=3.5.0) +`pytest-responsemock `_ Simplified requests calls mocking for pytest Oct 10, 2020 5 - Production/Stable N/A +`pytest-responses `_ py.test integration for responses Jan 29, 2019 N/A N/A +`pytest-restrict `_ Pytest plugin to restrict the test types allowed Dec 03, 2020 5 - Production/Stable pytest +`pytest-rethinkdb `_ A RethinkDB plugin for pytest. Jul 24, 2016 4 - Beta N/A +`pytest-reverse `_ Pytest plugin to reverse test order. Dec 27, 2020 5 - Production/Stable pytest +`pytest-ringo `_ pytest plugin to test webapplications using the Ringo webframework Sep 27, 2017 3 - Alpha N/A +`pytest-rng `_ Fixtures for seeding tests and making randomness reproducible Aug 08, 2019 5 - Production/Stable pytest +`pytest-roast `_ pytest plugin for ROAST configuration override and fixtures Jan 14, 2021 5 - Production/Stable pytest (<6) +`pytest-rotest `_ Pytest integration with rotest Sep 08, 2019 N/A pytest (>=3.5.0) +`pytest-rpc `_ Extend py.test for RPC OpenStack testing. Feb 22, 2019 4 - Beta pytest (~=3.6) +`pytest-rt `_ pytest data collector plugin for Testgr Jan 24, 2021 N/A N/A +`pytest-rts `_ Coverage-based regression test selection (RTS) plugin for pytest Dec 21, 2020 N/A pytest +`pytest-runfailed `_ implement a --failed option for pytest Mar 24, 2016 N/A N/A +`pytest-runner `_ Invoke py.test as distutils command with dependency resolution Oct 26, 2019 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' +`pytest-salt `_ Pytest Salt Plugin Jan 27, 2020 4 - Beta N/A +`pytest-salt-containers `_ A Pytest plugin that builds and creates docker containers Nov 09, 2016 4 - Beta N/A +`pytest-salt-factories `_ Pytest Salt Plugin Jan 19, 2021 4 - Beta pytest (>=6.1.1) +`pytest-salt-from-filenames `_ Simple PyTest Plugin For Salt's Test Suite Specifically Jan 29, 2019 4 - Beta pytest (>=4.1) +`pytest-salt-runtests-bridge `_ Simple PyTest Plugin For Salt's Test Suite Specifically Dec 05, 2019 4 - Beta pytest (>=4.1) +`pytest-sanic `_ a pytest plugin for Sanic Sep 24, 2020 N/A pytest (>=5.2) +`pytest-sanity `_ Dec 07, 2020 N/A N/A +`pytest-sa-pg `_ May 14, 2019 N/A N/A +`pytest-sbase `_ A complete web automation framework for end-to-end testing. Jan 27, 2021 5 - Production/Stable N/A +`pytest-scenario `_ pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A +`pytest-schema `_ 👍 Validate return values against a schema-like object in testing Aug 31, 2020 5 - Production/Stable pytest (>=3.5.0) +`pytest-securestore `_ An encrypted password store for use within pytest cases Jun 19, 2019 4 - Beta N/A +`pytest-select `_ A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) +`pytest-selenium `_ pytest plugin for Selenium Sep 19, 2020 5 - Production/Stable pytest (>=5.0.0) +`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Jan 27, 2021 5 - Production/Stable N/A +`pytest-selenium-enhancer `_ pytest plugin for Selenium Nov 26, 2020 5 - Production/Stable N/A +`pytest-selenium-pdiff `_ A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A +`pytest-send-email `_ Send pytest execution result email Dec 04, 2019 N/A N/A +`pytest-sentry `_ A pytest plugin to send testrun information to Sentry.io Dec 16, 2020 N/A N/A +`pytest-server-fixtures `_ Extensible server fixures for py.test May 28, 2019 5 - Production/Stable pytest +`pytest-serverless `_ Automatically mocks resources from serverless.yml in pytest using moto. Dec 26, 2020 4 - Beta N/A +`pytest-services `_ Services plugin for pytest testing framework Oct 30, 2020 6 - Mature N/A +`pytest-session2file `_ pytest-session2file (aka: pytest-session_to_file for v0.1.0 - v0.1.2) is a py.test plugin for capturing and saving to file the stdout of py.test. Jan 26, 2021 3 - Alpha pytest +`pytest-session-fixture-globalize `_ py.test plugin to make session fixtures behave as if written in conftest, even if it is written in some modules May 15, 2018 4 - Beta N/A +`pytest-session_to_file `_ pytest-session_to_file is a py.test plugin for capturing and saving to file the stdout of py.test. Oct 01, 2015 3 - Alpha N/A +`pytest-sftpserver `_ py.test plugin to locally test sftp server connections. Sep 16, 2019 4 - Beta N/A +`pytest-shard `_ Dec 11, 2020 4 - Beta pytest +`pytest-shell `_ A pytest plugin for testing shell scripts and line-based processes Jan 18, 2020 N/A N/A +`pytest-sheraf `_ Versatile ZODB abstraction layer - pytest fixtures Feb 11, 2020 N/A pytest +`pytest-sherlock `_ pytest plugin help to find coupled tests Jul 13, 2020 5 - Production/Stable pytest (>=3.5.1) +`pytest-shortcuts `_ Expand command-line shortcuts listed in pytest configuration Oct 29, 2020 4 - Beta pytest (>=3.5.0) +`pytest-shutil `_ A goodie-bag of unix shell and environment tools for py.test May 28, 2019 5 - Production/Stable pytest +`pytest-simple-plugin `_ Simple pytest plugin Nov 27, 2019 N/A N/A +`pytest-simple-settings `_ simple-settings plugin for pytest Nov 17, 2020 4 - Beta pytest +`pytest-single-file-logging `_ Allow for multiple processes to log to a single file May 05, 2016 4 - Beta pytest (>=2.8.1) +`pytest-skipper `_ A plugin that selects only tests with changes in execution path Mar 26, 2017 3 - Alpha pytest (>=3.0.6) +`pytest-skippy `_ Automatically skip tests that don't need to run! Jan 27, 2018 3 - Alpha pytest (>=2.3.4) +`pytest-slack `_ Pytest to Slack reporting plugin Dec 15, 2020 5 - Production/Stable N/A +`pytest-smartcollect `_ A plugin for collecting tests that touch changed code Oct 04, 2018 N/A pytest (>=3.5.0) +`pytest-smartcov `_ Smart coverage plugin for pytest. Sep 30, 2017 3 - Alpha N/A +`pytest-snail `_ Plugin for adding a marker to slow running tests. 🐌 Nov 04, 2019 3 - Alpha pytest (>=5.0.1) +`pytest-snapci `_ py.test plugin for Snap-CI Nov 12, 2015 N/A N/A +`pytest-snapshot `_ A plugin to enable snapshot testing with pytest. Jan 22, 2021 4 - Beta pytest (>=3.0.0) +`pytest-snmpserver `_ Sep 14, 2020 N/A N/A +`pytest-socket `_ Pytest Plugin to disable socket calls during tests May 31, 2020 4 - Beta pytest (>=3.6.3) +`pytest-soft-assertions `_ May 05, 2020 3 - Alpha pytest +`pytest-solr `_ Solr process and client fixtures for py.test. May 11, 2020 3 - Alpha pytest (>=3.0.0) +`pytest-sorter `_ A simple plugin to first execute tests that historically failed more Jul 23, 2020 4 - Beta pytest (>=3.1.1) +`pytest-sourceorder `_ Test-ordering plugin for pytest Apr 11, 2017 4 - Beta pytest +`pytest-spark `_ pytest plugin to run the tests with support of pyspark. Feb 23, 2020 4 - Beta pytest +`pytest-spawner `_ py.test plugin to spawn process and communicate with them. Jul 31, 2015 4 - Beta N/A +`pytest-spec `_ Library pytest-spec is a pytest plugin to display test execution output like a SPECIFICATION. Jan 14, 2021 N/A N/A +`pytest-sphinx `_ Doctest plugin for pytest with support for Sphinx-specific doctest-directives Aug 05, 2020 4 - Beta N/A +`pytest-spiratest `_ Exports unit tests as test runs in SpiraTest/Team/Plan Oct 30, 2020 N/A N/A +`pytest-splinter `_ Splinter plugin for pytest testing framework Dec 25, 2020 6 - Mature N/A +`pytest-split `_ Pytest plugin for splitting test suite based on test execution time Apr 07, 2020 1 - Planning N/A +`pytest-splitio `_ Split.io SDK integration for e2e tests Sep 22, 2020 N/A pytest (<7,>=5.0) +`pytest-split-tests `_ A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Forked from Mark Adams' original project pytest-test-groups. May 28, 2019 N/A pytest (>=2.5) +`pytest-splunk-addon `_ A Dynamic test tool for Splunk Apps and Add-ons Jan 05, 2021 N/A pytest (>5.4.0,<6.1) +`pytest-splunk-addon-ui-smartx `_ Library to support testing Splunk Add-on UX Jan 18, 2021 N/A N/A +`pytest-splunk-env `_ pytest fixtures for interaction with Splunk Enterprise and Splunk Cloud Oct 22, 2020 N/A pytest (>=6.1.1,<7.0.0) +`pytest-sqitch `_ sqitch for pytest Apr 06, 2020 4 - Beta N/A +`pytest-sqlalchemy `_ pytest plugin with sqlalchemy related fixtures Mar 13, 2018 3 - Alpha N/A +`pytest-sql-bigquery `_ Yet another SQL-testing framework for BigQuery provided by pytest plugin Dec 19, 2019 N/A pytest +`pytest-ssh `_ pytest plugin for ssh command run May 27, 2019 N/A pytest +`pytest-start-from `_ Start pytest run from a given point Apr 11, 2016 N/A N/A +`pytest-statsd `_ pytest plugin for reporting to graphite Nov 30, 2018 5 - Production/Stable pytest (>=3.0.0) +`pytest-stepfunctions `_ A small description Jul 07, 2020 4 - Beta pytest +`pytest-steps `_ Create step-wise / incremental tests in pytest. Apr 25, 2020 5 - Production/Stable N/A +`pytest-stepwise `_ Run a test suite one failing test at a time. Dec 01, 2015 4 - Beta N/A +`pytest-stoq `_ A plugin to pytest stoq Nov 04, 2020 4 - Beta N/A +`pytest-stress `_ A Pytest plugin that allows you to loop tests for a user defined amount of time. Dec 07, 2019 4 - Beta pytest (>=3.6.0) +`pytest-structlog `_ Structured logging assertions Jul 16, 2020 N/A pytest +`pytest-structmpd `_ provide structured temporary directory Oct 17, 2018 N/A N/A +`pytest-stub `_ Stub packages, modules and attributes. Apr 28, 2020 5 - Production/Stable N/A +`pytest-stubprocess `_ Provide stub implementations for subprocesses in Python tests Sep 17, 2018 3 - Alpha pytest (>=3.5.0) +`pytest-study `_ A pytest plugin to organize long run tests (named studies) without interfering the regular tests Sep 26, 2017 3 - Alpha pytest (>=2.0) +`pytest-subprocess `_ A plugin to fake subprocess for pytest Aug 22, 2020 5 - Production/Stable pytest (>=4.0.0) +`pytest-subtesthack `_ A hack to explicitly set up and tear down fixtures. Jan 31, 2016 N/A N/A +`pytest-subtests `_ unittest subTest() support and subtests fixture Dec 13, 2020 4 - Beta pytest (>=5.3.0) +`pytest-subunit `_ pytest-subunit is a plugin for py.test which outputs testsresult in subunit format. Aug 29, 2017 N/A N/A +`pytest-sugar `_ pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly). Jul 06, 2020 3 - Alpha N/A +`pytest-sugar-bugfix159 `_ Workaround for https://github.com/Frozenball/pytest-sugar/issues/159 Nov 07, 2018 5 - Production/Stable pytest (!=3.7.3,>=3.5); extra == 'testing' +`pytest-super-check `_ Pytest plugin to check your TestCase classes call super in setUp, tearDown, etc. Dec 13, 2020 5 - Production/Stable pytest +`pytest-svn `_ SVN repository fixture for py.test May 28, 2019 5 - Production/Stable pytest +`pytest-symbols `_ pytest-symbols is a pytest plugin that adds support for passing test environment symbols into pytest tests. Nov 20, 2017 3 - Alpha N/A +`pytest-tap `_ Test Anything Protocol (TAP) reporting plugin for pytest Nov 07, 2020 5 - Production/Stable pytest (>=3.0) +`pytest-target `_ Pytest plugin for remote target orchestration. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) +`pytest-tblineinfo `_ tblineinfo is a py.test plugin that insert the node id in the final py.test report when --tb=line option is used Dec 01, 2015 3 - Alpha pytest (>=2.0) +`pytest-teamcity-logblock `_ py.test plugin to introduce block structure in teamcity build log, if output is not captured May 15, 2018 4 - Beta N/A +`pytest-telegram `_ Pytest to Telegram reporting plugin Dec 10, 2020 5 - Production/Stable N/A +`pytest-tempdir `_ Predictable and repeatable tempdir support. Oct 11, 2019 4 - Beta pytest (>=2.8.1) +`pytest-terraform `_ A pytest plugin for using terraform fixtures Oct 20, 2020 N/A pytest (>=6.0.0,<6.1.0) +`pytest-terraform-fixture `_ generate terraform resources to use with pytest Nov 14, 2018 4 - Beta N/A +`pytest-testbook `_ A plugin to run tests written in Jupyter notebook Dec 11, 2016 3 - Alpha N/A +`pytest-testconfig `_ Test configuration plugin for pytest. Jan 11, 2020 4 - Beta pytest (>=3.5.0) +`pytest-testdirectory `_ A py.test plugin providing temporary directories in unit tests. Nov 06, 2018 5 - Production/Stable pytest +`pytest-testdox `_ A testdox format reporter for pytest Oct 13, 2020 5 - Production/Stable pytest (>=3.7.0) +`pytest-test-groups `_ A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Oct 25, 2016 5 - Production/Stable N/A +`pytest-testinfra `_ Test infrastructures Nov 12, 2020 5 - Production/Stable pytest (!=3.0.2) +`pytest-testlink-adaptor `_ pytest reporting plugin for testlink Dec 20, 2018 4 - Beta pytest (>=2.6) +`pytest-testmon `_ selects tests affected by changed files and methods Aug 05, 2020 4 - Beta N/A +`pytest-testobject `_ Plugin to use TestObject Suites with Pytest Sep 24, 2019 4 - Beta pytest (>=3.1.1) +`pytest-testrail `_ pytest plugin for creating TestRail runs and adding results Aug 27, 2020 N/A pytest (>=3.6) +`pytest-testrail2 `_ A small example package Nov 17, 2020 N/A pytest (>=5) +`pytest-testrail-api `_ Плагин Pytest, для интеграции с TestRail Dec 09, 2020 N/A pytest (>=5.5) +`pytest-testrail-client `_ pytest plugin for Testrail Sep 29, 2020 5 - Production/Stable N/A +`pytest-testrail-e2e `_ pytest plugin for creating TestRail runs and adding results Jun 11, 2020 N/A pytest (>=3.6) +`pytest-testrail-plugin `_ PyTest plugin for TestRail Apr 21, 2020 3 - Alpha pytest +`pytest-testrail-reporter `_ Sep 10, 2018 N/A N/A +`pytest-testslide `_ TestSlide fixture for pytest Jan 07, 2021 5 - Production/Stable pytest (~=6.2) +`pytest-test-this `_ Plugin for py.test to run relevant tests, based on naively checking if a test contains a reference to the symbol you supply Sep 15, 2019 2 - Pre-Alpha pytest (>=2.3) +`pytest-tesults `_ Tesults plugin for pytest May 18, 2020 5 - Production/Stable pytest (>=3.5.0) +`pytest-tezos `_ pytest-ligo Jan 16, 2020 4 - Beta N/A +`pytest-thawgun `_ Pytest plugin for time travel May 26, 2020 3 - Alpha N/A +`pytest-threadleak `_ Detects thread leaks Sep 08, 2017 4 - Beta N/A +`pytest-timeit `_ A pytest plugin to time test function runs Oct 13, 2016 4 - Beta N/A +`pytest-timeout `_ py.test plugin to abort hanging tests Jul 15, 2020 5 - Production/Stable pytest (>=3.6.0) +`pytest-timeouts `_ Linux-only Pytest plugin to control durations of various test case execution phases Sep 21, 2019 5 - Production/Stable N/A +`pytest-timer `_ A timer plugin for pytest Dec 13, 2020 N/A N/A +`pytest-tipsi-django `_ Oct 14, 2020 4 - Beta pytest (>=6.0.0) +`pytest-tipsi-testing `_ Better fixtures management. Various helpers Nov 04, 2020 4 - Beta pytest (>=3.3.0) +`pytest-tldr `_ A pytest plugin that limits the output to just the things you need. Aug 08, 2020 4 - Beta pytest (>=3.5.0) +`pytest-tm4j-reporter `_ Cloud Jira Test Management (TM4J) PyTest reporter plugin Sep 01, 2020 N/A pytest +`pytest-todo `_ A small plugin for the pytest testing framework, marking TODO comments as failure May 23, 2019 4 - Beta pytest +`pytest-tomato `_ Mar 01, 2019 5 - Production/Stable N/A +`pytest-toolbelt `_ This is just a collection of utilities for pytest, but don't really belong in pytest proper. Aug 12, 2019 3 - Alpha N/A +`pytest-toolbox `_ Numerous useful plugins for pytest. Apr 07, 2018 N/A pytest (>=3.5.0) +`pytest-tornado `_ A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Jun 17, 2020 5 - Production/Stable pytest (>=3.6) +`pytest-tornado5 `_ A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Nov 16, 2018 5 - Production/Stable pytest (>=3.6) +`pytest-tornado-yen3 `_ A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Oct 15, 2018 5 - Production/Stable N/A +`pytest-tornasync `_ py.test plugin for testing Python 3.5+ Tornado code Jul 15, 2019 3 - Alpha pytest (>=3.0) +`pytest-track `_ Oct 23, 2020 3 - Alpha pytest (>=3.0) +`pytest-translations `_ Test your translation files. Oct 26, 2020 5 - Production/Stable N/A +`pytest-travis-fold `_ Folds captured output sections in Travis CI build log Nov 29, 2017 4 - Beta pytest (>=2.6.0) +`pytest-trello `_ Plugin for py.test that integrates trello using markers Nov 20, 2015 5 - Production/Stable N/A +`pytest-trepan `_ Pytest plugin for trepan debugger. Jul 28, 2018 5 - Production/Stable N/A +`pytest-trialtemp `_ py.test plugin for using the same _trial_temp working directory as trial Jun 08, 2015 N/A N/A +`pytest-trio `_ Pytest plugin for trio Oct 16, 2020 N/A N/A +`pytest-tspwplib `_ A simple plugin to use with tspwplib Jan 08, 2021 4 - Beta pytest (>=3.5.0) +`pytest-tstcls `_ Test Class Base Mar 23, 2020 5 - Production/Stable N/A +`pytest-twisted `_ A twisted plugin for pytest. Sep 11, 2020 5 - Production/Stable pytest (>=2.3) +`pytest-typhoon-xray `_ Typhoon HIL plugin for pytest Jun 22, 2020 4 - Beta pytest (>=5.4.2) +`pytest-tytest `_ Typhoon HIL plugin for pytest May 25, 2020 4 - Beta pytest (>=5.4.2) +`pytest-ubersmith `_ Easily mock calls to ubersmith at the `requests` level. Apr 13, 2015 N/A N/A +`pytest-ui `_ Text User Interface for running python tests May 03, 2020 4 - Beta pytest +`pytest-unhandled-exception-exit-code `_ Plugin for py.test set a different exit code on uncaught exceptions Jun 22, 2020 5 - Production/Stable pytest (>=2.3) +`pytest-unittest-filter `_ A pytest plugin for filtering unittest-based test classes Jan 12, 2019 4 - Beta pytest (>=3.1.0) +`pytest-unmarked `_ Run only unmarked tests Aug 27, 2019 5 - Production/Stable N/A +`pytest-unordered `_ Test equality of unordered collections in pytest Nov 02, 2020 4 - Beta pytest (>=6.0.0) +`pytest-vagrant `_ A py.test plugin providing access to vagrant. Mar 23, 2020 5 - Production/Stable pytest +`pytest-valgrind `_ Mar 15, 2020 N/A N/A +`pytest-variables `_ pytest plugin for providing variables to tests/fixtures Oct 23, 2019 5 - Production/Stable pytest (>=2.4.2) +`pytest-vcr `_ Plugin for managing VCR.py cassettes Apr 26, 2019 5 - Production/Stable pytest (>=3.6.0) +`pytest-vcrpandas `_ Test from HTTP interactions to dataframe processed. Jan 12, 2019 4 - Beta pytest +`pytest-venv `_ py.test fixture for creating a virtual environment Aug 04, 2020 4 - Beta pytest +`pytest-verbose-parametrize `_ More descriptive output for parametrized py.test tests May 28, 2019 5 - Production/Stable pytest +`pytest-virtualenv `_ Virtualenv fixture for py.test May 28, 2019 5 - Production/Stable pytest +`pytest-voluptuous `_ Pytest plugin for asserting data against voluptuous schema. Jun 09, 2020 N/A pytest +`pytest-vscodedebug `_ A pytest plugin to easily enable debugging tests within Visual Studio Code Dec 04, 2020 4 - Beta N/A +`pytest-vts `_ pytest plugin for automatic recording of http stubbed tests Jun 05, 2019 N/A pytest (>=2.3) +`pytest-vw `_ pytest-vw makes your failing test cases succeed under CI tools scrutiny Oct 07, 2015 4 - Beta N/A +`pytest-vyper `_ Plugin for the vyper smart contract language. May 28, 2020 2 - Pre-Alpha N/A +`pytest-wa-e2e-plugin `_ Pytest plugin for testing whatsapp bots with end to end tests Feb 18, 2020 4 - Beta pytest (>=3.5.0) +`pytest-watch `_ Local continuous test runner with pytest and watchdog. May 20, 2018 N/A N/A +`pytest-wdl `_ Pytest plugin for testing WDL workflows. Nov 17, 2020 5 - Production/Stable N/A +`pytest-webdriver `_ Selenium webdriver fixture for py.test May 28, 2019 5 - Production/Stable pytest +`pytest-wetest `_ Welian API Automation test framework pytest plugin Nov 10, 2018 4 - Beta N/A +`pytest-whirlwind `_ Testing Tornado. Jun 12, 2020 N/A N/A +`pytest-wholenodeid `_ pytest addon for displaying the whole node id for failures Aug 26, 2015 4 - Beta pytest (>=2.0) +`pytest-winnotify `_ Windows tray notifications for py.test results. Apr 22, 2016 N/A N/A +`pytest-workflow `_ A pytest plugin for configuring workflow/pipeline tests using YAML files Dec 14, 2020 5 - Production/Stable pytest (>=5.4.0) +`pytest-xdist `_ pytest xdist plugin for distributed testing and loop-on-failing modes Dec 14, 2020 5 - Production/Stable pytest (>=6.0.0) +`pytest-xdist-debug-for-graingert `_ pytest xdist plugin for distributed testing and loop-on-failing modes Jul 24, 2019 5 - Production/Stable pytest (>=4.4.0) +`pytest-xdist-forked `_ forked from pytest-xdist Feb 10, 2020 5 - Production/Stable pytest (>=4.4.0) +`pytest-xfiles `_ Pytest fixtures providing data read from function, module or package related (x)files. Feb 27, 2018 N/A N/A +`pytest-xlog `_ Extended logging for test and decorators May 31, 2020 4 - Beta N/A +`pytest-xpara `_ An extended parametrizing plugin of pytest. Oct 30, 2017 3 - Alpha pytest +`pytest-xprocess `_ A pytest plugin for managing processes across test runs. Nov 26, 2020 4 - Beta pytest (>=2.8) +`pytest-xray `_ May 30, 2019 3 - Alpha N/A +`pytest-xrayjira `_ Mar 17, 2020 3 - Alpha pytest (==4.3.1) +`pytest-xray-server `_ Nov 29, 2020 3 - Alpha N/A +`pytest-xvfb `_ A pytest plugin to run Xvfb for tests. Jun 09, 2020 4 - Beta pytest (>=2.8.1) +`pytest-yaml `_ This plugin is used to load yaml output to your test using pytest framework. Oct 05, 2018 N/A pytest +`pytest-yamltree `_ Create or check file/directory trees described by YAML Mar 02, 2020 4 - Beta pytest (>=3.1.1) +`pytest-yamlwsgi `_ Run tests against wsgi apps defined in yaml May 11, 2010 N/A N/A +`pytest-yapf `_ Run yapf Jul 06, 2017 4 - Beta pytest (>=3.1.1) +`pytest-yapf3 `_ Validate your Python file format with yapf Aug 03, 2020 5 - Production/Stable pytest (>=5.4) +`pytest-yield `_ PyTest plugin to run tests concurrently, each `yield` switch context to other one Jan 23, 2019 N/A N/A +`pytest-zafira `_ A Zafira plugin for pytest Sep 18, 2019 5 - Production/Stable pytest (==4.1.1) +`pytest-zap `_ OWASP ZAP plugin for py.test. May 12, 2014 4 - Beta N/A +`pytest-zigzag `_ Extend py.test for RPC OpenStack testing. Feb 27, 2019 4 - Beta pytest (~=3.6) +============================================================================================================== ======================================================================================================================================================================== ============== ===================== ============================================ diff --git a/doc/en/plugins.rst b/doc/en/plugins.rst index 855b597392b..8090e7d18a4 100644 --- a/doc/en/plugins.rst +++ b/doc/en/plugins.rst @@ -58,7 +58,7 @@ Here is a little annotated list for some popular plugins: To see a complete list of all plugins with their latest testing status against different pytest and Python versions, please visit -`plugincompat `_. +:doc:`plugin_list`. You may also discover more plugins through a `pytest- pypi.org search`_. diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index e9806a6664d..92a3dd7dd48 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -122,7 +122,7 @@ you can copy from: * a custom collection example plugin: :ref:`yaml plugin` * builtin plugins which provide pytest's own functionality -* many `external plugins `_ providing additional features +* many :doc:`external plugins ` providing additional features All of these plugins implement :ref:`hooks ` and/or :ref:`fixtures ` to extend and add functionality. diff --git a/scripts/update-plugin-list.py b/scripts/update-plugin-list.py new file mode 100644 index 00000000000..f80e63127ee --- /dev/null +++ b/scripts/update-plugin-list.py @@ -0,0 +1,86 @@ +import datetime +import pathlib +import re + +import packaging.version +import requests +import tabulate + +FILE_HEAD = r"""Plugins List +============ + +PyPI projects that match "pytest-\*" are considered plugins and are listed +automatically. Packages classified as inactive are excluded. +""" +DEVELOPMENT_STATUS_CLASSIFIERS = ( + "Development Status :: 1 - Planning", + "Development Status :: 2 - Pre-Alpha", + "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", + "Development Status :: 6 - Mature", + "Development Status :: 7 - Inactive", +) + + +def iter_plugins(): + regex = r">([\d\w-]*)" + response = requests.get("https://pypi.org/simple") + for match in re.finditer(regex, response.text): + name = match.groups()[0] + if not name.startswith("pytest-"): + continue + response = requests.get(f"https://pypi.org/pypi/{name}/json") + if response.status_code == 404: + # Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but + # return 404 on the JSON API. Skip. + continue + response.raise_for_status() + info = response.json()["info"] + if "Development Status :: 7 - Inactive" in info["classifiers"]: + continue + for classifier in DEVELOPMENT_STATUS_CLASSIFIERS: + if classifier in info["classifiers"]: + status = classifier[22:] + break + else: + status = "N/A" + requires = "N/A" + if info["requires_dist"]: + for requirement in info["requires_dist"]: + if requirement == "pytest" or "pytest " in requirement: + requires = requirement + break + releases = response.json()["releases"] + for release in sorted(releases, key=packaging.version.parse, reverse=True): + if releases[release]: + release_date = datetime.date.fromisoformat( + releases[release][-1]["upload_time_iso_8601"].split("T")[0] + ) + last_release = release_date.strftime("%b %d, %Y") + break + name = f'`{info["name"]} <{info["project_url"]}>`_' + summary = info["summary"].replace("\n", "") + summary = re.sub(r"_\b", "", summary) + yield { + "name": name, + "summary": summary, + "last release": last_release, + "status": status, + "requires": requires, + } + + +def main(): + plugins = list(iter_plugins()) + plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst") + plugin_list = pathlib.Path("doc", "en", "plugin_list.rst") + with plugin_list.open("w") as f: + f.write(FILE_HEAD) + f.write(f"This list contains {len(plugins)} plugins.\n\n") + f.write(plugin_table) + f.write("\n") + + +if __name__ == "__main__": + main() From 07f0eb26b4da671659fefbfcce59d5d56e91f21a Mon Sep 17 00:00:00 2001 From: Hong Xu Date: Fri, 29 Jan 2021 07:54:06 -0800 Subject: [PATCH 0424/2846] Doc: Clarify pytester.run (#8294) Co-authored-by: Bruno Oliveira --- src/_pytest/pytester.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 8ca21d1c538..d428654de88 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1296,16 +1296,16 @@ def collect_by_name( def popen( self, - cmdargs, + cmdargs: Sequence[Union[str, "os.PathLike[str]"]], stdout: Union[int, TextIO] = subprocess.PIPE, stderr: Union[int, TextIO] = subprocess.PIPE, stdin=CLOSE_STDIN, **kw, ): - """Invoke subprocess.Popen. + """Invoke :py:class:`subprocess.Popen`. - Calls subprocess.Popen making sure the current working directory is - in the PYTHONPATH. + Calls :py:class:`subprocess.Popen` making sure the current working + directory is in the ``PYTHONPATH``. You probably want to use :py:meth:`run` instead. """ @@ -1340,21 +1340,30 @@ def run( ) -> RunResult: """Run a command with arguments. - Run a process using subprocess.Popen saving the stdout and stderr. + Run a process using :py:class:`subprocess.Popen` saving the stdout and + stderr. :param cmdargs: - The sequence of arguments to pass to `subprocess.Popen()`, with path-like objects - being converted to ``str`` automatically. + The sequence of arguments to pass to :py:class:`subprocess.Popen`, + with path-like objects being converted to :py:class:`str` + automatically. :param timeout: The period in seconds after which to timeout and raise :py:class:`Pytester.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. + Optional standard input. - :rtype: RunResult + - If it is :py:attr:`CLOSE_STDIN` (Default), then this method calls + :py:class:`subprocess.Popen` with ``stdin=subprocess.PIPE``, and + the standard input is closed immediately after the new command is + started. + + - If it is of type :py:class:`bytes`, these bytes are sent to the + standard input of the command. + + - Otherwise, it is passed through to :py:class:`subprocess.Popen`. + For further information in this case, consult the document of the + ``stdin`` parameter in :py:class:`subprocess.Popen`. """ __tracebackhide__ = True From afea19079786761ee167e2bb4e0c90e3f44ba544 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 29 Jan 2021 20:40:43 +0200 Subject: [PATCH 0425/2846] Remove some no longer needed type-ignores --- src/_pytest/cacheprovider.py | 2 +- src/_pytest/monkeypatch.py | 2 +- src/_pytest/pytester.py | 10 +++------- src/_pytest/python.py | 3 +-- src/_pytest/python_api.py | 2 +- .../example_scripts/unittest/test_unittest_asyncio.py | 2 +- testing/test_assertrewrite.py | 4 ++-- 7 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 480319c03b4..585cebf6c9d 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -415,7 +415,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) # type: ignore[no-any-return] + return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) def pytest_sessionfinish(self) -> None: config = self.config diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index ffef87173a8..2c432065625 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: # type: ignore[unreachable] + if not isinstance(import_path, str) or "." not in import_path: raise TypeError(f"must be absolute import path string, not {import_path!r}") module, attr = import_path.rsplit(".", 1) target = resolve(module) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d428654de88..4fe6e288b43 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1096,7 +1096,7 @@ def pytest_configure(x, config: Config) -> None: class reprec: # type: ignore pass - reprec.ret = ret # type: ignore + reprec.ret = ret # Typically we reraise keyboard interrupts from the child run # because it's our user requesting interruption of the testing. @@ -1263,9 +1263,7 @@ def getmodulecol( Whether to also write an ``__init__.py`` file to the same directory to ensure it is a package. """ - # TODO: Remove type ignore in next mypy release (> 0.790). - # https://github.com/python/typeshed/pull/4582 - if isinstance(source, os.PathLike): # type: ignore[misc] + if isinstance(source, os.PathLike): path = self.path.joinpath(source) assert not withinit, "not supported for paths" else: @@ -1367,10 +1365,8 @@ 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 # type: ignore[misc] + os.fspath(arg) if isinstance(arg, os.PathLike) else arg for arg in cmdargs ) p1 = self.path.joinpath("stdout") p2 = self.path.joinpath("stderr") diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 29ebd176bbb..3d903ff9b0b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -139,8 +139,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: 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] + metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) def pytest_configure(config: Config) -> None: diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 81ce4f89539..7e0c86479d4 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -713,7 +713,7 @@ def raises( else: excepted_exceptions = expected_exception for exc in excepted_exceptions: - if not isinstance(exc, type) or not issubclass(exc, BaseException): # type: ignore[unreachable] + if not isinstance(exc, type) or not issubclass(exc, BaseException): 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/testing/example_scripts/unittest/test_unittest_asyncio.py b/testing/example_scripts/unittest/test_unittest_asyncio.py index bbc752de5c1..1cd2168604c 100644 --- a/testing/example_scripts/unittest/test_unittest_asyncio.py +++ b/testing/example_scripts/unittest/test_unittest_asyncio.py @@ -1,5 +1,5 @@ from typing import List -from unittest import IsolatedAsyncioTestCase # type: ignore +from unittest import IsolatedAsyncioTestCase teardowns: List[None] = [] diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index ffe18260f90..cba03406e86 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() # type: ignore[unreachable] + assert False or x() assert ( getmsg(f7, {"x": x}) @@ -471,7 +471,7 @@ def f1() -> None: assert getmsg(f1) == "assert ((3 % 2) and False)" def f2() -> None: - assert False or 4 % 2 # type: ignore[unreachable] + assert False or 4 % 2 assert getmsg(f2) == "assert (False or (4 % 2))" From f0f19aa8d960dc71eaa00a0f01d3459a9626bbea Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Jan 2021 21:08:37 +0100 Subject: [PATCH 0426/2846] doc: Point out two-argument form of monkeypatch.setattr See the "for convenience" part here: https://docs.pytest.org/en/stable/reference.html#pytest.MonkeyPatch.setattr --- doc/en/monkeypatch.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index 9480f008f7c..4964ee54815 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -16,6 +16,7 @@ functionality in tests: .. code-block:: python monkeypatch.setattr(obj, name, value, raising=True) + monkeypatch.setattr("somemodule.obj.name", value, raising=True) monkeypatch.delattr(obj, name, raising=True) monkeypatch.setitem(mapping, name, value) monkeypatch.delitem(obj, name, raising=True) From de06f468ed2bb64864f68e05a219e4a0f3986428 Mon Sep 17 00:00:00 2001 From: bluetech Date: Sat, 30 Jan 2021 00:30:28 +0000 Subject: [PATCH 0427/2846] [automated] Update plugin list --- doc/en/plugin_list.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/en/plugin_list.rst b/doc/en/plugin_list.rst index 927838ac942..a4ab7df46bc 100644 --- a/doc/en/plugin_list.rst +++ b/doc/en/plugin_list.rst @@ -3,7 +3,7 @@ Plugins List PyPI projects that match "pytest-\*" are considered plugins and are listed automatically. Packages classified as inactive are excluded. -This list contains 820 plugins. +This list contains 821 plugins. ============================================================================================================== ======================================================================================================================================================================== ============== ===================== ============================================ name summary last release status requires @@ -546,6 +546,7 @@ name `pytest-poo `_ Visualize your crappy tests Jul 14, 2013 5 - Production/Stable N/A `pytest-poo-fail `_ Visualize your failed tests with poo Feb 12, 2015 5 - Production/Stable N/A `pytest-pop `_ A pytest plugin to help with testing pop projects Aug 13, 2020 5 - Production/Stable pytest (>=5.4.0) +`pytest-portion `_ Select a portion of the collected tests Jan 28, 2021 4 - Beta pytest (>=3.5.0) `pytest-postgres `_ Run PostgreSQL in Docker container in Pytest. Mar 22, 2020 N/A pytest `pytest-postgresql `_ Postgresql fixtures and fixture factories for Pytest. Oct 29, 2020 5 - Production/Stable pytest (>=3.0.0) `pytest-power `_ pytest plugin with powerful fixtures Dec 31, 2020 N/A pytest (>=5.4) @@ -633,7 +634,7 @@ name `pytest-rotest `_ Pytest integration with rotest Sep 08, 2019 N/A pytest (>=3.5.0) `pytest-rpc `_ Extend py.test for RPC OpenStack testing. Feb 22, 2019 4 - Beta pytest (~=3.6) `pytest-rt `_ pytest data collector plugin for Testgr Jan 24, 2021 N/A N/A -`pytest-rts `_ Coverage-based regression test selection (RTS) plugin for pytest Dec 21, 2020 N/A pytest +`pytest-rts `_ Coverage-based regression test selection (RTS) plugin for pytest Jan 29, 2021 N/A pytest `pytest-runfailed `_ implement a --failed option for pytest Mar 24, 2016 N/A N/A `pytest-runner `_ Invoke py.test as distutils command with dependency resolution Oct 26, 2019 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' `pytest-salt `_ Pytest Salt Plugin Jan 27, 2020 4 - Beta N/A @@ -644,13 +645,13 @@ name `pytest-sanic `_ a pytest plugin for Sanic Sep 24, 2020 N/A pytest (>=5.2) `pytest-sanity `_ Dec 07, 2020 N/A N/A `pytest-sa-pg `_ May 14, 2019 N/A N/A -`pytest-sbase `_ A complete web automation framework for end-to-end testing. Jan 27, 2021 5 - Production/Stable N/A +`pytest-sbase `_ A complete web automation framework for end-to-end testing. Jan 29, 2021 5 - Production/Stable N/A `pytest-scenario `_ pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A `pytest-schema `_ 👍 Validate return values against a schema-like object in testing Aug 31, 2020 5 - Production/Stable pytest (>=3.5.0) `pytest-securestore `_ An encrypted password store for use within pytest cases Jun 19, 2019 4 - Beta N/A `pytest-select `_ A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) `pytest-selenium `_ pytest plugin for Selenium Sep 19, 2020 5 - Production/Stable pytest (>=5.0.0) -`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Jan 27, 2021 5 - Production/Stable N/A +`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Jan 29, 2021 5 - Production/Stable N/A `pytest-selenium-enhancer `_ pytest plugin for Selenium Nov 26, 2020 5 - Production/Stable N/A `pytest-selenium-pdiff `_ A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A `pytest-send-email `_ Send pytest execution result email Dec 04, 2019 N/A N/A From c971f2f474df929dd06b5c4dae27514c7fb47e70 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 30 Jan 2021 13:01:41 +0200 Subject: [PATCH 0428/2846] ci: give pytest bot the credit for updating the plugin list --- .github/workflows/update-plugin-list.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/update-plugin-list.yml b/.github/workflows/update-plugin-list.yml index 10b5cb99478..3f4ec092bd9 100644 --- a/.github/workflows/update-plugin-list.yml +++ b/.github/workflows/update-plugin-list.yml @@ -26,6 +26,7 @@ jobs: uses: peter-evans/create-pull-request@2455e1596942c2902952003bbb574afbbe2ab2e6 with: commit-message: '[automated] Update plugin list' + author: 'pytest bot ' branch: update-plugin-list/patch delete-branch: true branch-suffix: short-commit-hash From 5c61d60b0d9328889799b8cf698841b61ff7035b Mon Sep 17 00:00:00 2001 From: pytest bot Date: Mon, 1 Feb 2021 00:32:44 +0000 Subject: [PATCH 0429/2846] [automated] Update plugin list --- doc/en/plugin_list.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/en/plugin_list.rst b/doc/en/plugin_list.rst index a4ab7df46bc..9c6b6f0aa41 100644 --- a/doc/en/plugin_list.rst +++ b/doc/en/plugin_list.rst @@ -34,7 +34,7 @@ name `pytest-apistellar `_ apistellar plugin for pytest. Jun 18, 2019 N/A N/A `pytest-appengine `_ AppEngine integration that works well with pytest-django Feb 27, 2017 N/A N/A `pytest-appium `_ Pytest plugin for appium Dec 05, 2019 N/A N/A -`pytest-approvaltests `_ A plugin to use approvaltests with pytest Aug 02, 2019 4 - Beta N/A +`pytest-approvaltests `_ A plugin to use approvaltests with pytest Jan 31, 2021 4 - Beta pytest (>=3.5.0) `pytest-arraydiff `_ pytest plugin to help with comparing array output from tests Dec 06, 2018 4 - Beta pytest `pytest-asgi-server `_ Convenient ASGI client/server fixtures for Pytest Dec 12, 2020 N/A pytest (>=5.4.1) `pytest-asptest `_ test Answer Set Programming programs Apr 28, 2018 4 - Beta N/A @@ -123,7 +123,7 @@ name `pytest-codestyle `_ pytest plugin to run pycodestyle Mar 23, 2020 3 - Alpha N/A `pytest-collect-formatter `_ Formatter for pytest collect output Nov 19, 2020 5 - Production/Stable N/A `pytest-colordots `_ Colorizes the progress indicators Oct 06, 2017 5 - Production/Stable N/A -`pytest-commander `_ An interactive GUI test runner for PyTest Nov 21, 2020 N/A pytest (>=5.0.0) +`pytest-commander `_ An interactive GUI test runner for PyTest Jan 31, 2021 N/A pytest (>=5.0.0) `pytest-common-subject `_ pytest framework for testing different aspects of a common method Nov 12, 2020 N/A pytest (>=3.6,<7) `pytest-concurrent `_ Concurrently execute test cases with multithread, multiprocess and gevent Jan 12, 2019 4 - Beta pytest (>=3.1.1) `pytest-config `_ Base configurations and utilities for developing your Python project test suite with pytest. Nov 07, 2014 5 - Production/Stable N/A @@ -339,7 +339,7 @@ name `pytest-homeassistant `_ A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A `pytest-homeassistant-custom-component `_ Experimental package to automatically extract test plugins for Home Assistant custom components Jan 05, 2021 3 - Alpha pytest (==6.1.2) `pytest-honors `_ Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A -`pytest-hoverfly-wrapper `_ Integrates the Hoverfly HTTP proxy into Pytest Oct 25, 2020 4 - Beta N/A +`pytest-hoverfly-wrapper `_ Integrates the Hoverfly HTTP proxy into Pytest Jan 31, 2021 4 - Beta N/A `pytest-html `_ pytest plugin for generating HTML reports Dec 13, 2020 5 - Production/Stable pytest (!=6.0.0,>=5.0) `pytest-html-lee `_ optimized pytest plugin for generating HTML reports Jun 30, 2020 5 - Production/Stable pytest (>=5.0) `pytest-html-profiling `_ Pytest plugin for generating HTML reports with per-test profiling and optionally call graph visualizations. Based on pytest-html by Dave Hunt. Feb 11, 2020 5 - Production/Stable pytest (>=3.0) @@ -548,7 +548,7 @@ name `pytest-pop `_ A pytest plugin to help with testing pop projects Aug 13, 2020 5 - Production/Stable pytest (>=5.4.0) `pytest-portion `_ Select a portion of the collected tests Jan 28, 2021 4 - Beta pytest (>=3.5.0) `pytest-postgres `_ Run PostgreSQL in Docker container in Pytest. Mar 22, 2020 N/A pytest -`pytest-postgresql `_ Postgresql fixtures and fixture factories for Pytest. Oct 29, 2020 5 - Production/Stable pytest (>=3.0.0) +`pytest-postgresql `_ Postgresql fixtures and fixture factories for Pytest. Jan 31, 2021 5 - Production/Stable pytest (>=3.0.0) `pytest-power `_ pytest plugin with powerful fixtures Dec 31, 2020 N/A pytest (>=5.4) `pytest-pride `_ Minitest-style test colors Apr 02, 2016 3 - Alpha N/A `pytest-print `_ pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) Oct 23, 2020 5 - Production/Stable pytest (>=3.0.0) @@ -645,13 +645,13 @@ name `pytest-sanic `_ a pytest plugin for Sanic Sep 24, 2020 N/A pytest (>=5.2) `pytest-sanity `_ Dec 07, 2020 N/A N/A `pytest-sa-pg `_ May 14, 2019 N/A N/A -`pytest-sbase `_ A complete web automation framework for end-to-end testing. Jan 29, 2021 5 - Production/Stable N/A +`pytest-sbase `_ A complete web automation framework for end-to-end testing. Jan 31, 2021 5 - Production/Stable N/A `pytest-scenario `_ pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A `pytest-schema `_ 👍 Validate return values against a schema-like object in testing Aug 31, 2020 5 - Production/Stable pytest (>=3.5.0) `pytest-securestore `_ An encrypted password store for use within pytest cases Jun 19, 2019 4 - Beta N/A `pytest-select `_ A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) `pytest-selenium `_ pytest plugin for Selenium Sep 19, 2020 5 - Production/Stable pytest (>=5.0.0) -`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Jan 29, 2021 5 - Production/Stable N/A +`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Jan 31, 2021 5 - Production/Stable N/A `pytest-selenium-enhancer `_ pytest plugin for Selenium Nov 26, 2020 5 - Production/Stable N/A `pytest-selenium-pdiff `_ A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A `pytest-send-email `_ Send pytest execution result email Dec 04, 2019 N/A N/A From 1b2ca22e9bf6720445ca8eab928b44cd27b46c8f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 16:49:41 +0000 Subject: [PATCH 0430/2846] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9130a79a06b..2eed21fc883 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.9.1 + rev: v1.9.2 hooks: - id: blacken-docs additional_dependencies: [black==20.8b1] @@ -34,7 +34,7 @@ repos: - id: reorder-python-imports args: ['--application-directories=.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.7.4 + rev: v2.9.0 hooks: - id: pyupgrade args: [--py36-plus] From 3165b1e3238df1424431f754182c66fd6d18bee4 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Tue, 2 Feb 2021 00:31:11 +0000 Subject: [PATCH 0431/2846] [automated] Update plugin list --- doc/en/plugin_list.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/en/plugin_list.rst b/doc/en/plugin_list.rst index a4ab7df46bc..8747bf75e8a 100644 --- a/doc/en/plugin_list.rst +++ b/doc/en/plugin_list.rst @@ -34,7 +34,7 @@ name `pytest-apistellar `_ apistellar plugin for pytest. Jun 18, 2019 N/A N/A `pytest-appengine `_ AppEngine integration that works well with pytest-django Feb 27, 2017 N/A N/A `pytest-appium `_ Pytest plugin for appium Dec 05, 2019 N/A N/A -`pytest-approvaltests `_ A plugin to use approvaltests with pytest Aug 02, 2019 4 - Beta N/A +`pytest-approvaltests `_ A plugin to use approvaltests with pytest Jan 31, 2021 4 - Beta pytest (>=3.5.0) `pytest-arraydiff `_ pytest plugin to help with comparing array output from tests Dec 06, 2018 4 - Beta pytest `pytest-asgi-server `_ Convenient ASGI client/server fixtures for Pytest Dec 12, 2020 N/A pytest (>=5.4.1) `pytest-asptest `_ test Answer Set Programming programs Apr 28, 2018 4 - Beta N/A @@ -123,7 +123,7 @@ name `pytest-codestyle `_ pytest plugin to run pycodestyle Mar 23, 2020 3 - Alpha N/A `pytest-collect-formatter `_ Formatter for pytest collect output Nov 19, 2020 5 - Production/Stable N/A `pytest-colordots `_ Colorizes the progress indicators Oct 06, 2017 5 - Production/Stable N/A -`pytest-commander `_ An interactive GUI test runner for PyTest Nov 21, 2020 N/A pytest (>=5.0.0) +`pytest-commander `_ An interactive GUI test runner for PyTest Jan 31, 2021 N/A pytest (>=5.0.0) `pytest-common-subject `_ pytest framework for testing different aspects of a common method Nov 12, 2020 N/A pytest (>=3.6,<7) `pytest-concurrent `_ Concurrently execute test cases with multithread, multiprocess and gevent Jan 12, 2019 4 - Beta pytest (>=3.1.1) `pytest-config `_ Base configurations and utilities for developing your Python project test suite with pytest. Nov 07, 2014 5 - Production/Stable N/A @@ -337,9 +337,9 @@ name `pytest-historic `_ Custom report to display pytest historical execution records Apr 08, 2020 N/A pytest `pytest-historic-hook `_ Custom listener to store execution results into MYSQL DB, which is used for pytest-historic report Apr 08, 2020 N/A pytest `pytest-homeassistant `_ A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A -`pytest-homeassistant-custom-component `_ Experimental package to automatically extract test plugins for Home Assistant custom components Jan 05, 2021 3 - Alpha pytest (==6.1.2) +`pytest-homeassistant-custom-component `_ Experimental package to automatically extract test plugins for Home Assistant custom components Feb 01, 2021 3 - Alpha pytest (==6.2.2) `pytest-honors `_ Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A -`pytest-hoverfly-wrapper `_ Integrates the Hoverfly HTTP proxy into Pytest Oct 25, 2020 4 - Beta N/A +`pytest-hoverfly-wrapper `_ Integrates the Hoverfly HTTP proxy into Pytest Jan 31, 2021 4 - Beta N/A `pytest-html `_ pytest plugin for generating HTML reports Dec 13, 2020 5 - Production/Stable pytest (!=6.0.0,>=5.0) `pytest-html-lee `_ optimized pytest plugin for generating HTML reports Jun 30, 2020 5 - Production/Stable pytest (>=5.0) `pytest-html-profiling `_ Pytest plugin for generating HTML reports with per-test profiling and optionally call graph visualizations. Based on pytest-html by Dave Hunt. Feb 11, 2020 5 - Production/Stable pytest (>=3.0) @@ -548,7 +548,7 @@ name `pytest-pop `_ A pytest plugin to help with testing pop projects Aug 13, 2020 5 - Production/Stable pytest (>=5.4.0) `pytest-portion `_ Select a portion of the collected tests Jan 28, 2021 4 - Beta pytest (>=3.5.0) `pytest-postgres `_ Run PostgreSQL in Docker container in Pytest. Mar 22, 2020 N/A pytest -`pytest-postgresql `_ Postgresql fixtures and fixture factories for Pytest. Oct 29, 2020 5 - Production/Stable pytest (>=3.0.0) +`pytest-postgresql `_ Postgresql fixtures and fixture factories for Pytest. Jan 31, 2021 5 - Production/Stable pytest (>=3.0.0) `pytest-power `_ pytest plugin with powerful fixtures Dec 31, 2020 N/A pytest (>=5.4) `pytest-pride `_ Minitest-style test colors Apr 02, 2016 3 - Alpha N/A `pytest-print `_ pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) Oct 23, 2020 5 - Production/Stable pytest (>=3.0.0) @@ -645,13 +645,13 @@ name `pytest-sanic `_ a pytest plugin for Sanic Sep 24, 2020 N/A pytest (>=5.2) `pytest-sanity `_ Dec 07, 2020 N/A N/A `pytest-sa-pg `_ May 14, 2019 N/A N/A -`pytest-sbase `_ A complete web automation framework for end-to-end testing. Jan 29, 2021 5 - Production/Stable N/A +`pytest-sbase `_ A complete web automation framework for end-to-end testing. Jan 31, 2021 5 - Production/Stable N/A `pytest-scenario `_ pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A `pytest-schema `_ 👍 Validate return values against a schema-like object in testing Aug 31, 2020 5 - Production/Stable pytest (>=3.5.0) `pytest-securestore `_ An encrypted password store for use within pytest cases Jun 19, 2019 4 - Beta N/A `pytest-select `_ A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) `pytest-selenium `_ pytest plugin for Selenium Sep 19, 2020 5 - Production/Stable pytest (>=5.0.0) -`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Jan 29, 2021 5 - Production/Stable N/A +`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Jan 31, 2021 5 - Production/Stable N/A `pytest-selenium-enhancer `_ pytest plugin for Selenium Nov 26, 2020 5 - Production/Stable N/A `pytest-selenium-pdiff `_ A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A `pytest-send-email `_ Send pytest execution result email Dec 04, 2019 N/A N/A @@ -695,7 +695,7 @@ name `pytest-split `_ Pytest plugin for splitting test suite based on test execution time Apr 07, 2020 1 - Planning N/A `pytest-splitio `_ Split.io SDK integration for e2e tests Sep 22, 2020 N/A pytest (<7,>=5.0) `pytest-split-tests `_ A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Forked from Mark Adams' original project pytest-test-groups. May 28, 2019 N/A pytest (>=2.5) -`pytest-splunk-addon `_ A Dynamic test tool for Splunk Apps and Add-ons Jan 05, 2021 N/A pytest (>5.4.0,<6.1) +`pytest-splunk-addon `_ A Dynamic test tool for Splunk Apps and Add-ons Feb 01, 2021 N/A pytest (>5.4.0,<6.1) `pytest-splunk-addon-ui-smartx `_ Library to support testing Splunk Add-on UX Jan 18, 2021 N/A N/A `pytest-splunk-env `_ pytest fixtures for interaction with Splunk Enterprise and Splunk Cloud Oct 22, 2020 N/A pytest (>=6.1.1,<7.0.0) `pytest-sqitch `_ sqitch for pytest Apr 06, 2020 4 - Beta N/A From 100c8deab5fccda043b2087bd450d074c1f8756d Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Sat, 30 Jan 2021 19:50:56 +0000 Subject: [PATCH 0432/2846] Fix various typos in fixture docs --- doc/en/fixture.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index a629bb7d47f..ca44fbd8263 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -375,7 +375,7 @@ Fixtures are reusable ^^^^^^^^^^^^^^^^^^^^^ One of the things that makes pytest's fixture system so powerful, is that it -gives us the abilty to define a generic setup step that can reused over and +gives us the ability to define a generic setup step that can reused over and over, just like a normal function would be used. Two different tests can request the same fixture and have pytest give each test their own result from that fixture. @@ -902,7 +902,7 @@ attempt to tear them down as it normally would. 2. Adding finalizers directly ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -While yield fixtures are considered to be the cleaner and more straighforward +While yield fixtures are considered to be the cleaner and more straightforward option, there is another choice, and that is to add "finalizer" functions directly to the test's `request-context`_ object. It brings a similar result as yield fixtures, but requires a bit more verbosity. @@ -1026,7 +1026,7 @@ making one state-changing action each, and then bundling them together with their teardown code, as :ref:`the email examples above ` showed. The chance that a state-changing operation can fail but still modify state is -neglibible, as most of these operations tend to be `transaction`_-based (at +negligible, as most of these operations tend to be `transaction`_-based (at least at the level of testing where state could be left behind). So if we make sure that any successful state-changing action gets torn down by moving it to a separate fixture function and separating it from other, potentially failing @@ -1124,7 +1124,7 @@ never have been made. .. _`conftest.py`: .. _`conftest`: -Fixture availabiility +Fixture availability --------------------- Fixture availability is determined from the perspective of the test. A fixture @@ -1410,9 +1410,9 @@ pytest doesn't know where ``c`` should go in the case, so it should be assumed that it could go anywhere between ``g`` and ``b``. This isn't necessarily bad, but it's something to keep in mind. If the order -they execute in could affect the behavior a test is targetting, or could +they execute in could affect the behavior a test is targeting, or could otherwise influence the result of a test, then the order should be defined -explicitely in a way that allows pytest to linearize/"flatten" that order. +explicitly in a way that allows pytest to linearize/"flatten" that order. .. _`autouse order`: @@ -1506,7 +1506,7 @@ of what we've gone over so far. All that's needed is stepping up to a larger scope, then having the **act** step defined as an autouse fixture, and finally, making sure all the fixtures -are targetting that highler level scope. +are targeting that higher level scope. Let's pull :ref:`an example from above `, and tweak it a bit. Let's say that in addition to checking for a welcome message in the header, @@ -1777,7 +1777,7 @@ Parametrizing fixtures ----------------------------------------------------------------- Fixture functions can be parametrized in which case they will be called -multiple times, each time executing the set of dependent tests, i. e. the +multiple times, each time executing the set of dependent tests, i.e. the tests that depend on this fixture. Test functions usually do not need to be aware of their re-running. Fixture parametrization helps to write exhaustive functional tests for components which themselves can be From 298541f540a0b566002c8e6a01e7e093c6750158 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Sat, 30 Jan 2021 22:14:35 +0000 Subject: [PATCH 0433/2846] Add basic emaillib for tests in fixture.rst This will be used to power regendoc runs for later tests --- doc/en/fixture.rst | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index ca44fbd8263..38636ff7514 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -844,12 +844,42 @@ Once the test is finished, pytest will go back down the list of fixtures, but in the *reverse order*, taking each one that yielded, and running the code inside it that was *after* the ``yield`` statement. -As a simple example, let's say we want to test sending email from one user to -another. We'll have to first make each user, then send the email from one user -to the other, and finally assert that the other user received that message in -their inbox. If we want to clean up after the test runs, we'll likely have to -make sure the other user's mailbox is emptied before deleting that user, -otherwise the system may complain. +As a simple example, consider this basic email module: + +.. code-block:: python + + # content of emaillib.py + class MailAdminClient: + def create_user(self): + return MailUser() + + def delete_user(self, user): + # do some cleanup + pass + + + class MailUser: + def __init__(self): + self.inbox = [] + + def send_email(self, email, other): + other.inbox.append(email) + + def clear_mailbox(self): + self.mailbox.clear() + + + class Email: + def __init__(self, subject, body): + self.body = body + self.subject = subject + +Let's say we want to test sending email from one user to another. We'll have to +first make each user, then send the email from one user to the other, and +finally assert that the other user received that message in their inbox. If we +want to clean up after the test runs, we'll likely have to make sure the other +user's mailbox is emptied before deleting that user, otherwise the system may +complain. Here's what that might look like: From 1d895dd46cba716ae96ceb5688d52bdff5445168 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Thu, 4 Feb 2021 00:26:06 +0000 Subject: [PATCH 0434/2846] [automated] Update plugin list --- doc/en/plugin_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/plugin_list.rst b/doc/en/plugin_list.rst index 8747bf75e8a..6aae603a8a7 100644 --- a/doc/en/plugin_list.rst +++ b/doc/en/plugin_list.rst @@ -212,7 +212,7 @@ name `pytest-docker-pexpect `_ pytest plugin for writing functional tests with pexpect and docker Jan 14, 2019 N/A pytest `pytest-docker-postgresql `_ A simple plugin to use with pytest Sep 24, 2019 4 - Beta pytest (>=3.5.0) `pytest-docker-py `_ Easy to use, simple to extend, pytest plugin that minimally leverages docker-py. Nov 27, 2018 N/A pytest (==4.0.0) -`pytest-docker-registry-fixtures `_ Pytest fixtures for testing with docker registries. Jan 25, 2021 4 - Beta pytest +`pytest-docker-registry-fixtures `_ Pytest fixtures for testing with docker registries. Feb 03, 2021 4 - Beta pytest `pytest-docker-tools `_ Docker integration tests for pytest Jan 15, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) `pytest-docs `_ Documentation tool for pytest Nov 11, 2018 4 - Beta pytest (>=3.5.0) `pytest-docstyle `_ pytest plugin to run pydocstyle Mar 23, 2020 3 - Alpha N/A From b77f071befd67492776cefb632341639e98e30b6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 4 Feb 2021 11:40:28 +0100 Subject: [PATCH 0435/2846] doc: Remove past training --- doc/en/index.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index f74ef90a785..0361805d95a 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -1,11 +1,5 @@ :orphan: -.. sidebar:: Next Open Trainings - - - `Professional testing with Python `_, via Python Academy, February 1-3 2021, remote and Leipzig (Germany). **Early-bird discount available until January 15th**. - - Also see `previous talks and blogposts `_. - .. _features: pytest: helps you write better programs From 97cfd66806913250b3b26775da66ff75f6673b29 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Sat, 30 Jan 2021 22:23:04 +0000 Subject: [PATCH 0436/2846] Add regendoc runs for emaillib tests in fixture Also update these tests ensure they pass, and be explicit about the test file called in an existing test to avoid unintentional calls to the added tests --- doc/en/fixture.rst | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 38636ff7514..de1591d2d61 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -829,6 +829,8 @@ This system can be leveraged in two ways. 1. ``yield`` fixtures (recommended) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. regendoc: wipe + "Yield" fixtures ``yield`` instead of ``return``. With these fixtures, we can run some code and pass an object back to the requesting fixture/test, just like with the other fixtures. The only differences are: @@ -866,13 +868,13 @@ As a simple example, consider this basic email module: other.inbox.append(email) def clear_mailbox(self): - self.mailbox.clear() + self.inbox.clear() class Email: def __init__(self, subject, body): - self.body = body self.subject = subject + self.body = body Let's say we want to test sending email from one user to another. We'll have to first make each user, then send the email from one user to the other, and @@ -885,6 +887,7 @@ Here's what that might look like: .. code-block:: python + # content of test_emaillib.py import pytest from emaillib import Email, MailAdminClient @@ -899,17 +902,17 @@ Here's what that might look like: def sending_user(mail_admin): user = mail_admin.create_user() yield user - admin_client.delete_user(user) + mail_admin.delete_user(user) @pytest.fixture def receiving_user(mail_admin): user = mail_admin.create_user() yield user - admin_client.delete_user(user) + mail_admin.delete_user(user) - def test_email_received(sending_user, receiving_user, email): + def test_email_received(sending_user, receiving_user): email = Email(subject="Hey!", body="How's it going?") sending_user.send_email(email, receiving_user) assert email in receiving_user.inbox @@ -921,6 +924,10 @@ There is a risk that even having the order right on the teardown side of things doesn't guarantee a safe cleanup. That's covered in a bit more detail in :ref:`safe teardowns`. +.. code-block:: pytest + + $ pytest -q test_emaillib.py + Handling errors for yield fixture """"""""""""""""""""""""""""""""" @@ -952,6 +959,7 @@ Here's how the previous example would look using the ``addfinalizer`` method: .. code-block:: python + # content of test_emaillib.py import pytest from emaillib import Email, MailAdminClient @@ -966,7 +974,7 @@ Here's how the previous example would look using the ``addfinalizer`` method: def sending_user(mail_admin): user = mail_admin.create_user() yield user - admin_client.delete_user(user) + mail_admin.delete_user(user) @pytest.fixture @@ -974,7 +982,7 @@ Here's how the previous example would look using the ``addfinalizer`` method: user = mail_admin.create_user() def delete_user(): - admin_client.delete_user(user) + mail_admin.delete_user(user) request.addfinalizer(delete_user) return user @@ -986,7 +994,7 @@ Here's how the previous example would look using the ``addfinalizer`` method: sending_user.send_email(_email, receiving_user) def empty_mailbox(): - receiving_user.delete_email(_email) + receiving_user.clear_mailbox() request.addfinalizer(empty_mailbox) return _email @@ -999,6 +1007,10 @@ Here's how the previous example would look using the ``addfinalizer`` method: It's a bit longer than yield fixtures and a bit more complex, but it does offer some nuances for when you're in a pinch. +.. code-block:: pytest + + $ pytest -q test_emaillib.py + .. _`safe teardowns`: Safe teardowns @@ -1014,6 +1026,7 @@ above): .. code-block:: python + # content of test_emaillib.py import pytest from emaillib import Email, MailAdminClient @@ -1025,11 +1038,11 @@ above): sending_user = mail_admin.create_user() receiving_user = mail_admin.create_user() email = Email(subject="Hey!", body="How's it going?") - sending_user.send_emai(email, receiving_user) + sending_user.send_email(email, receiving_user) yield receiving_user, email - receiving_user.delete_email(email) - admin_client.delete_user(sending_user) - admin_client.delete_user(receiving_user) + receiving_user.clear_mailbox() + mail_admin.delete_user(sending_user) + mail_admin.delete_user(receiving_user) def test_email_received(setup): @@ -1046,6 +1059,10 @@ One option might be to go with the ``addfinalizer`` method instead of yield fixtures, but that might get pretty complex and difficult to maintain (and it wouldn't be compact anymore). +.. code-block:: pytest + + $ pytest -q test_emaillib.py + .. _`safe fixture structure`: Safe fixture structure @@ -1676,7 +1693,7 @@ again, nothing much has changed: .. code-block:: pytest - $ pytest -s -q --tb=no + $ pytest -s -q --tb=no test_module.py FFfinalizing (smtp.gmail.com) ========================= short test summary info ========================== From 709c211e68bf3a8aa452b61056b5294a21050dfb Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Thu, 4 Feb 2021 18:00:17 +0000 Subject: [PATCH 0437/2846] Run regendoc over fixture docs This is the result of running: $ cd doc/en && make regen REGENDOC_FILES=fixture.rst --- doc/en/fixture.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index de1591d2d61..d72a0e619b5 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -927,6 +927,8 @@ doesn't guarantee a safe cleanup. That's covered in a bit more detail in .. code-block:: pytest $ pytest -q test_emaillib.py + . [100%] + 1 passed in 0.12s Handling errors for yield fixture """"""""""""""""""""""""""""""""" @@ -1010,6 +1012,8 @@ does offer some nuances for when you're in a pinch. .. code-block:: pytest $ pytest -q test_emaillib.py + . [100%] + 1 passed in 0.12s .. _`safe teardowns`: @@ -1062,6 +1066,8 @@ wouldn't be compact anymore). .. code-block:: pytest $ pytest -q test_emaillib.py + . [100%] + 1 passed in 0.12s .. _`safe fixture structure`: @@ -1978,11 +1984,13 @@ Running the above tests results in the following test IDs being used: 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 + collected 11 items + + @@ -1994,7 +2002,7 @@ Running the above tests results in the following test IDs being used: - ======================= 10 tests collected in 0.12s ======================== + ======================= 11 tests collected in 0.12s ======================== .. _`fixture-parametrize-marks`: From 80c223474c98fd59a07776994e672e934866c7d5 Mon Sep 17 00:00:00 2001 From: Hong Xu Date: Thu, 4 Feb 2021 13:44:22 -0800 Subject: [PATCH 0438/2846] Type annotation polishing for symbols around Pytester.run (#8298) * Type annotation polishing for symbols around Pytester.run Hopefully these will help document readers understand pertinent methods and constants better. Following up #8294 * Use NOTSET instead of object --- src/_pytest/pytester.py | 42 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 4fe6e288b43..853dfbe9489 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -20,6 +20,7 @@ from typing import Callable from typing import Dict from typing import Generator +from typing import IO from typing import Iterable from typing import List from typing import Optional @@ -41,6 +42,8 @@ from _pytest._code import Source from _pytest.capture import _get_multicapture from _pytest.compat import final +from _pytest.compat import NOTSET +from _pytest.compat import NotSetType from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode @@ -66,6 +69,7 @@ if TYPE_CHECKING: + from typing_extensions import Final from typing_extensions import Literal import pexpect @@ -651,7 +655,7 @@ class Pytester: __test__ = False - CLOSE_STDIN = object + CLOSE_STDIN: "Final" = NOTSET class TimeoutExpired(Exception): pass @@ -1297,13 +1301,13 @@ def popen( cmdargs: Sequence[Union[str, "os.PathLike[str]"]], stdout: Union[int, TextIO] = subprocess.PIPE, stderr: Union[int, TextIO] = subprocess.PIPE, - stdin=CLOSE_STDIN, + stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN, **kw, ): """Invoke :py:class:`subprocess.Popen`. Calls :py:class:`subprocess.Popen` making sure the current working - directory is in the ``PYTHONPATH``. + directory is in ``PYTHONPATH``. You probably want to use :py:meth:`run` instead. """ @@ -1334,7 +1338,7 @@ def run( self, *cmdargs: Union[str, "os.PathLike[str]"], timeout: Optional[float] = None, - stdin=CLOSE_STDIN, + stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN, ) -> RunResult: """Run a command with arguments. @@ -1426,21 +1430,17 @@ def _dump_lines(self, lines, fp): def _getpytestargs(self) -> Tuple[str, ...]: return sys.executable, "-mpytest" - def runpython(self, script) -> RunResult: - """Run a python script using sys.executable as interpreter. - - :rtype: RunResult - """ + def runpython(self, script: "os.PathLike[str]") -> RunResult: + """Run a python script using sys.executable as interpreter.""" return self.run(sys.executable, script) - def runpython_c(self, command): - """Run python -c "command". - - :rtype: RunResult - """ + def runpython_c(self, command: str) -> RunResult: + """Run ``python -c "command"``.""" return self.run(sys.executable, "-c", command) - def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunResult: + def runpytest_subprocess( + self, *args: Union[str, "os.PathLike[str]"], 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 @@ -1454,8 +1454,6 @@ def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunRes :param timeout: The period in seconds after which to timeout and raise :py:class:`Pytester.TimeoutExpired`. - - :rtype: RunResult """ __tracebackhide__ = True p = make_numbered_dir(root=self.path, prefix="runpytest-") @@ -1529,9 +1527,9 @@ class Testdir: __test__ = False - CLOSE_STDIN = Pytester.CLOSE_STDIN - TimeoutExpired = Pytester.TimeoutExpired - Session = Pytester.Session + CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN + TimeoutExpired: "Final" = Pytester.TimeoutExpired + Session: "Final" = Pytester.Session def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None: check_ispytest(_ispytest) @@ -1695,8 +1693,8 @@ def collect_by_name( def popen( self, cmdargs, - stdout: Union[int, TextIO] = subprocess.PIPE, - stderr: Union[int, TextIO] = subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, stdin=CLOSE_STDIN, **kw, ): From d358a060add416e11b0e231cbfe9d97b02335ad0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 4 Feb 2021 11:52:13 +0200 Subject: [PATCH 0439/2846] config/argparsing: use proper deprecations instead of ad-hoc DeprecationWarning Proper for removing this in the next major pytest release. --- changelog/8315.deprecation.rst | 5 +++++ doc/en/deprecations.rst | 13 +++++++++++++ src/_pytest/config/argparsing.py | 22 ++++++---------------- src/_pytest/deprecated.py | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 changelog/8315.deprecation.rst diff --git a/changelog/8315.deprecation.rst b/changelog/8315.deprecation.rst new file mode 100644 index 00000000000..9b49d7c2f19 --- /dev/null +++ b/changelog/8315.deprecation.rst @@ -0,0 +1,5 @@ +Several behaviors of :meth:`Parser.addoption <_pytest.config.argparsing.Parser.addoption>` are now +scheduled for removal in pytest 7 (deprecated since pytest 2.4.0): + +- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead. +- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 0dcbd8ceb36..a3d7fd49a33 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 `. + +Backward compatibilities in ``Parser.addoption`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 2.4 + +Several behaviors of :meth:`Parser.addoption <_pytest.config.argparsing.Parser.addoption>` are now +scheduled for removal in pytest 7 (deprecated since pytest 2.4.0): + +- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead. +- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead. + + Raising ``unittest.SkipTest`` during collection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 5a09ea781e6..cf738cc2b8e 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -18,6 +18,9 @@ import _pytest._io from _pytest.compat import final from _pytest.config.exceptions import UsageError +from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT +from _pytest.deprecated import ARGUMENT_TYPE_STR +from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE if TYPE_CHECKING: from typing import NoReturn @@ -212,12 +215,7 @@ def __init__(self, *names: str, **attrs: Any) -> None: 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' - ' changed to "%(default)s" ', - DeprecationWarning, - stacklevel=3, - ) + warnings.warn(ARGUMENT_PERCENT_DEFAULT, stacklevel=3) try: typ = attrs["type"] except KeyError: @@ -227,11 +225,7 @@ def __init__(self, *names: str, **attrs: Any) -> None: if isinstance(typ, str): if typ == "choice": warnings.warn( - "`type` argument to addoption() is the string %r." - " For choices this is optional and can be omitted, " - " but when supplied should be a type (for example `str` or `int`)." - " (options: %s)" % (typ, names), - DeprecationWarning, + ARGUMENT_TYPE_STR_CHOICE.format(typ=typ, names=names), stacklevel=4, ) # argparse expects a type here take it from @@ -239,11 +233,7 @@ def __init__(self, *names: str, **attrs: Any) -> None: attrs["type"] = type(attrs["choices"][0]) else: warnings.warn( - "`type` argument to addoption() is the string %r, " - " but when supplied should be a type (for example `str` or `int`)." - " (options: %s)" % (typ, names), - DeprecationWarning, - stacklevel=4, + ARGUMENT_TYPE_STR.format(typ=typ, names=names), stacklevel=4 ) attrs["type"] = Argument._typ_map[typ] # Used in test_parseopt -> test_parse_defaultgetter. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index fa91f909769..5efc004ac94 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -69,6 +69,25 @@ "Use pytest.skip() instead." ) +ARGUMENT_PERCENT_DEFAULT = PytestDeprecationWarning( + 'pytest now uses argparse. "%default" should be changed to "%(default)s"', +) + +ARGUMENT_TYPE_STR_CHOICE = UnformattedWarning( + PytestDeprecationWarning, + "`type` argument to addoption() is the string {typ!r}." + " For choices this is optional and can be omitted, " + " but when supplied should be a type (for example `str` or `int`)." + " (options: {names})", +) + +ARGUMENT_TYPE_STR = UnformattedWarning( + PytestDeprecationWarning, + "`type` argument to addoption() is the string {typ!r}, " + " but when supplied should be a type (for example `str` or `int`)." + " (options: {names})", +) + # You want to make some `__init__` or function "private". # From 16af1a31fd137b7af2d4404af49406d2efa9880d Mon Sep 17 00:00:00 2001 From: pytest bot Date: Fri, 5 Feb 2021 00:24:50 +0000 Subject: [PATCH 0440/2846] [automated] Update plugin list --- doc/en/plugin_list.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/en/plugin_list.rst b/doc/en/plugin_list.rst index 6aae603a8a7..e43f313422a 100644 --- a/doc/en/plugin_list.rst +++ b/doc/en/plugin_list.rst @@ -349,7 +349,7 @@ name `pytest-httpbin `_ Easily test your HTTP library against a local copy of httpbin Feb 11, 2019 5 - Production/Stable N/A `pytest-http-mocker `_ Pytest plugin for http mocking (via https://github.com/vilus/mocker) Oct 20, 2019 N/A N/A `pytest-httpretty `_ A thin wrapper of HTTPretty for pytest Feb 16, 2014 3 - Alpha N/A -`pytest-httpserver `_ pytest-httpserver is a httpserver for pytest Oct 18, 2020 3 - Alpha pytest ; extra == 'dev' +`pytest-httpserver `_ pytest-httpserver is a httpserver for pytest Feb 04, 2021 3 - Alpha pytest ; extra == 'dev' `pytest-httpx `_ Send responses to httpx. Nov 25, 2020 5 - Production/Stable pytest (==6.*) `pytest-hue `_ Visualise PyTest status via your Phillips Hue lights May 09, 2019 N/A N/A `pytest-hypo-25 `_ help hypo module for pytest Jan 12, 2020 3 - Alpha N/A @@ -422,7 +422,7 @@ name `pytest-manual-marker `_ pytest marker for marking manual tests Nov 28, 2018 3 - Alpha pytest `pytest-markdown `_ Test your markdown docs with pytest Jan 15, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) `pytest-marker-bugzilla `_ py.test bugzilla integration plugin, using markers Jan 09, 2020 N/A N/A -`pytest-markers-presence `_ A simple plugin to detect missed pytest tags and markers" Dec 21, 2020 4 - Beta pytest (>=6.0) +`pytest-markers-presence `_ A simple plugin to detect missed pytest tags and markers" Feb 04, 2021 4 - Beta pytest (>=6.0) `pytest-markfiltration `_ UNKNOWN Nov 08, 2011 3 - Alpha N/A `pytest-mark-no-py3 `_ pytest plugin and bowler codemod to help migrate tests to Python 3 May 17, 2019 N/A pytest `pytest-marks `_ UNKNOWN Nov 23, 2012 3 - Alpha N/A @@ -645,13 +645,13 @@ name `pytest-sanic `_ a pytest plugin for Sanic Sep 24, 2020 N/A pytest (>=5.2) `pytest-sanity `_ Dec 07, 2020 N/A N/A `pytest-sa-pg `_ May 14, 2019 N/A N/A -`pytest-sbase `_ A complete web automation framework for end-to-end testing. Jan 31, 2021 5 - Production/Stable N/A +`pytest-sbase `_ A complete web automation framework for end-to-end testing. Feb 04, 2021 5 - Production/Stable N/A `pytest-scenario `_ pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A `pytest-schema `_ 👍 Validate return values against a schema-like object in testing Aug 31, 2020 5 - Production/Stable pytest (>=3.5.0) `pytest-securestore `_ An encrypted password store for use within pytest cases Jun 19, 2019 4 - Beta N/A `pytest-select `_ A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) `pytest-selenium `_ pytest plugin for Selenium Sep 19, 2020 5 - Production/Stable pytest (>=5.0.0) -`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Jan 31, 2021 5 - Production/Stable N/A +`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Feb 04, 2021 5 - Production/Stable N/A `pytest-selenium-enhancer `_ pytest plugin for Selenium Nov 26, 2020 5 - Production/Stable N/A `pytest-selenium-pdiff `_ A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A `pytest-send-email `_ Send pytest execution result email Dec 04, 2019 N/A N/A From e3c0fd3203fb13d9a43dc6cc6e45627e5c33234a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Feb 2021 23:14:08 -0300 Subject: [PATCH 0441/2846] Update plugin-list every Sunday instead of everyday Every day seems a bit excessive lately, let's make it less frequent. --- .github/workflows/update-plugin-list.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-plugin-list.yml b/.github/workflows/update-plugin-list.yml index 3f4ec092bd9..9b071aa3d4f 100644 --- a/.github/workflows/update-plugin-list.yml +++ b/.github/workflows/update-plugin-list.yml @@ -2,8 +2,9 @@ name: Update Plugin List on: schedule: - # Run daily at midnight. - - cron: '0 0 * * *' + # At 00:00 on Sunday. + # https://crontab.guru + - cron: '0 0 * * 0' workflow_dispatch: jobs: From c604f3f0c52b51fc61ba7d208d522c2614aaa10d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Feb 2021 17:47:37 +0100 Subject: [PATCH 0442/2846] doc: Remove confusing fixture sentence There is no previous `test_ehlo` example (it follows much later) - and the same thing is described further down in ""Requesting" fixtures" already. --- doc/en/fixture.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index d72a0e619b5..6ffd77920be 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -174,9 +174,6 @@ Back to fixtures "Fixtures", in the literal sense, are each of the **arrange** steps and data. They're everything that test needs to do its thing. -At a basic level, test functions request fixtures by declaring them as -arguments, as in the ``test_ehlo(smtp_connection):`` in the previous example. - In pytest, "fixtures" are functions you define that serve this purpose. But they don't have to be limited to just the **arrange** steps. They can provide the **act** step, as well, and this can be a powerful technique for designing more From bcfe253f5b5367d8537e011e3a9e56bae220d411 Mon Sep 17 00:00:00 2001 From: Pax <13646646+paxcodes@users.noreply.github.com> Date: Fri, 5 Feb 2021 12:03:58 -0800 Subject: [PATCH 0443/2846] Type annotation for request.param (#8319) --- src/_pytest/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 269369642e3..6a57fffd144 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -758,7 +758,7 @@ def __init__( self, request: "FixtureRequest", scope: "_Scope", - param, + param: Any, param_index: int, fixturedef: "FixtureDef[object]", *, From f674f6da9fec826bc8c4e08781e7309322fc12cc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 31 Jan 2021 01:49:25 +0200 Subject: [PATCH 0444/2846] runner: add a safety assert to SetupState.prepare This ensures that the teardown for the previous item was done properly for this (next) item, i.e. there are no leftover teardowns. --- src/_pytest/hookspec.py | 6 +++--- src/_pytest/runner.py | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 41c12a2ccd8..b0b8fd53d85 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -509,9 +509,9 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None: :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. + scheduled). This argument is used to perform exact teardowns, i.e. + calling just enough finalizers so that nextitem only needs to call + setup functions. """ diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index ae76a247271..124cf531a2d 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -479,15 +479,18 @@ def __init__(self) -> None: def prepare(self, item: Item) -> None: """Setup objects along the collector chain to the item.""" + needed_collectors = item.listchain() + # If a collector fails its setup, fail its entire subtree of items. # The setup is not retried for each item - the same exception is used. for col, (finalizers, prepare_exc) in self.stack.items(): + assert col in needed_collectors, "previous item was not torn down properly" if prepare_exc: raise prepare_exc - needed_collectors = item.listchain() for col in needed_collectors[len(self.stack) :]: assert col not in self.stack + # Push onto the stack. self.stack[col] = ([col.teardown], None) try: col.setup() From f42b68ccaa4a64b3f7ef1cfcff50b4f39b63ceb9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 31 Jan 2021 12:14:06 +0200 Subject: [PATCH 0445/2846] runner: rename SetupState.prepare -> setup This is the usual terminology we use, and matches better with `teardown_exact()` and `pytest_runtest_setup()`. --- src/_pytest/fixtures.py | 2 +- src/_pytest/runner.py | 22 +++++++++++----------- testing/python/fixtures.py | 6 +++--- testing/test_runner.py | 14 +++++++------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 269369642e3..40b482d0482 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -372,7 +372,7 @@ def _fill_fixtures_impl(function: "Function") -> None: fi = fm.getfixtureinfo(function.parent, function.obj, None) function._fixtureinfo = fi request = function._request = FixtureRequest(function, _ispytest=True) - fm.session._setupstate.prepare(function) + fm.session._setupstate.setup(function) request._fillfixtures() # Prune out funcargs for jstests. newfuncargs = {} diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 124cf531a2d..153b134fe79 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -151,7 +151,7 @@ def show_test_item(item: Item) -> None: def pytest_runtest_setup(item: Item) -> None: _update_current_test_var(item, "setup") - item.session._setupstate.prepare(item) + item.session._setupstate.setup(item) def pytest_runtest_call(item: Item) -> None: @@ -417,7 +417,7 @@ class SetupState: [] - During the setup phase of item1, prepare(item1) is called. What it does + During the setup phase of item1, setup(item1) is called. What it does is: push session to stack, run session.setup() @@ -441,7 +441,7 @@ class SetupState: [session] - During the setup phase of item2, prepare(item2) is called. What it does + During the setup phase of item2, setup(item2) is called. What it does is: push mod2 to stack, run mod2.setup() @@ -477,16 +477,16 @@ def __init__(self) -> None: ], ] = {} - def prepare(self, item: Item) -> None: + def setup(self, item: Item) -> None: """Setup objects along the collector chain to the item.""" needed_collectors = item.listchain() # If a collector fails its setup, fail its entire subtree of items. # The setup is not retried for each item - the same exception is used. - for col, (finalizers, prepare_exc) in self.stack.items(): + for col, (finalizers, exc) in self.stack.items(): assert col in needed_collectors, "previous item was not torn down properly" - if prepare_exc: - raise prepare_exc + if exc: + raise exc for col in needed_collectors[len(self.stack) :]: assert col not in self.stack @@ -494,9 +494,9 @@ def prepare(self, item: Item) -> None: self.stack[col] = ([col.teardown], None) try: col.setup() - except TEST_OUTCOME as e: - self.stack[col] = (self.stack[col][0], e) - raise e + except TEST_OUTCOME as exc: + self.stack[col] = (self.stack[col][0], exc) + raise exc def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: """Attach a finalizer to the given node. @@ -520,7 +520,7 @@ def teardown_exact(self, nextitem: Optional[Item]) -> None: while self.stack: if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: break - node, (finalizers, prepare_exc) = self.stack.popitem() + node, (finalizers, _) = self.stack.popitem() while finalizers: fin = finalizers.pop() try: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 3d78ebf5826..3d5099c5399 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -131,7 +131,7 @@ def test_funcarg_basic(self, pytester: Pytester) -> None: item = pytester.getitem(Path("test_funcarg_basic.py")) assert isinstance(item, Function) # Execute's item's setup, which fills fixtures. - item.session._setupstate.prepare(item) + item.session._setupstate.setup(item) del item.funcargs["request"] assert len(get_public_names(item.funcargs)) == 2 assert item.funcargs["some"] == "test_func" @@ -827,7 +827,7 @@ def test_func(something): pass req = item._request # Execute item's setup. - item.session._setupstate.prepare(item) + item.session._setupstate.setup(item) with pytest.raises(pytest.FixtureLookupError): req.getfixturevalue("notexists") @@ -855,7 +855,7 @@ def test_func(something): pass """ ) assert isinstance(item, Function) - item.session._setupstate.prepare(item) + item.session._setupstate.setup(item) item._request._fillfixtures() # successively check finalization calls parent = item.getparent(pytest.Module) diff --git a/testing/test_runner.py b/testing/test_runner.py index e3f2863079f..abb87c6d3d4 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -25,7 +25,7 @@ def test_setup(self, pytester: Pytester) -> None: item = pytester.getitem("def test_func(): pass") ss = item.session._setupstate values = [1] - ss.prepare(item) + ss.setup(item) ss.addfinalizer(values.pop, item) assert values ss.teardown_exact(None) @@ -34,7 +34,7 @@ def test_setup(self, pytester: Pytester) -> None: def test_teardown_exact_stack_empty(self, pytester: Pytester) -> None: item = pytester.getitem("def test_func(): pass") ss = item.session._setupstate - ss.prepare(item) + ss.setup(item) ss.teardown_exact(None) ss.teardown_exact(None) ss.teardown_exact(None) @@ -49,9 +49,9 @@ def test_func(): pass ) ss = item.session._setupstate with pytest.raises(ValueError): - ss.prepare(item) + ss.setup(item) with pytest.raises(ValueError): - ss.prepare(item) + ss.setup(item) def test_teardown_multiple_one_fails(self, pytester: Pytester) -> None: r = [] @@ -67,7 +67,7 @@ def fin3(): item = pytester.getitem("def test_func(): pass") ss = item.session._setupstate - ss.prepare(item) + ss.setup(item) ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) ss.addfinalizer(fin3, item) @@ -87,7 +87,7 @@ def fin2(): item = pytester.getitem("def test_func(): pass") ss = item.session._setupstate - ss.prepare(item) + ss.setup(item) ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) with pytest.raises(Exception) as err: @@ -106,7 +106,7 @@ def fin_module(): item = pytester.getitem("def test_func(): pass") mod = item.listchain()[-2] ss = item.session._setupstate - ss.prepare(item) + ss.setup(item) ss.addfinalizer(fin_module, mod) ss.addfinalizer(fin_func, item) with pytest.raises(Exception, match="oops1"): From 5822888d735e2cd617225686611275fa8fbafbea Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 31 Jan 2021 12:23:10 +0200 Subject: [PATCH 0446/2846] runner: add clarifying comments on why runtestprotocol re-inits the FixtureRequest --- src/_pytest/runner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 153b134fe79..e43dd2dc818 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -120,6 +120,8 @@ def runtestprotocol( ) -> List[TestReport]: hasrequest = hasattr(item, "_request") if hasrequest and not item._request: # type: ignore[attr-defined] + # This only happens if the item is re-run, as is done by + # pytest-rerunfailures. item._initrequest() # type: ignore[attr-defined] rep = call_and_report(item, "setup", log) reports = [rep] From 3c6bd7eb27cad576520ddae5d615df6b5ad47dc7 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sun, 7 Feb 2021 00:44:13 +0000 Subject: [PATCH 0447/2846] [automated] Update plugin list --- doc/en/plugin_list.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/en/plugin_list.rst b/doc/en/plugin_list.rst index e43f313422a..cb70b0dcfdf 100644 --- a/doc/en/plugin_list.rst +++ b/doc/en/plugin_list.rst @@ -3,7 +3,7 @@ Plugins List PyPI projects that match "pytest-\*" are considered plugins and are listed automatically. Packages classified as inactive are excluded. -This list contains 821 plugins. +This list contains 822 plugins. ============================================================================================================== ======================================================================================================================================================================== ============== ===================== ============================================ name summary last release status requires @@ -499,6 +499,7 @@ name `pytest-oot `_ Run object-oriented tests in a simple format Sep 18, 2016 4 - Beta N/A `pytest-openfiles `_ Pytest plugin for detecting inadvertent open file handles Apr 16, 2020 3 - Alpha pytest (>=4.6) `pytest-opentmi `_ pytest plugin for publish results to opentmi Jun 10, 2020 5 - Production/Stable pytest (>=5.0) +`pytest-operator `_ Fixtures for Operators Feb 04, 2021 N/A N/A `pytest-optional `_ include/exclude values of fixtures in pytest Oct 07, 2015 N/A N/A `pytest-optional-tests `_ Easy declaration of optional tests (i.e., that are not run by default) Jul 09, 2019 4 - Beta pytest (>=4.5.0) `pytest-orchestration `_ A pytest plugin for orchestrating tests Jul 18, 2019 N/A N/A @@ -630,7 +631,7 @@ name `pytest-reverse `_ Pytest plugin to reverse test order. Dec 27, 2020 5 - Production/Stable pytest `pytest-ringo `_ pytest plugin to test webapplications using the Ringo webframework Sep 27, 2017 3 - Alpha N/A `pytest-rng `_ Fixtures for seeding tests and making randomness reproducible Aug 08, 2019 5 - Production/Stable pytest -`pytest-roast `_ pytest plugin for ROAST configuration override and fixtures Jan 14, 2021 5 - Production/Stable pytest (<6) +`pytest-roast `_ pytest plugin for ROAST configuration override and fixtures Feb 05, 2021 5 - Production/Stable pytest (<6) `pytest-rotest `_ Pytest integration with rotest Sep 08, 2019 N/A pytest (>=3.5.0) `pytest-rpc `_ Extend py.test for RPC OpenStack testing. Feb 22, 2019 4 - Beta pytest (~=3.6) `pytest-rt `_ pytest data collector plugin for Testgr Jan 24, 2021 N/A N/A @@ -645,13 +646,13 @@ name `pytest-sanic `_ a pytest plugin for Sanic Sep 24, 2020 N/A pytest (>=5.2) `pytest-sanity `_ Dec 07, 2020 N/A N/A `pytest-sa-pg `_ May 14, 2019 N/A N/A -`pytest-sbase `_ A complete web automation framework for end-to-end testing. Feb 04, 2021 5 - Production/Stable N/A +`pytest-sbase `_ A complete web automation framework for end-to-end testing. Feb 06, 2021 5 - Production/Stable N/A `pytest-scenario `_ pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A `pytest-schema `_ 👍 Validate return values against a schema-like object in testing Aug 31, 2020 5 - Production/Stable pytest (>=3.5.0) `pytest-securestore `_ An encrypted password store for use within pytest cases Jun 19, 2019 4 - Beta N/A `pytest-select `_ A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) `pytest-selenium `_ pytest plugin for Selenium Sep 19, 2020 5 - Production/Stable pytest (>=5.0.0) -`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Feb 04, 2021 5 - Production/Stable N/A +`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Feb 06, 2021 5 - Production/Stable N/A `pytest-selenium-enhancer `_ pytest plugin for Selenium Nov 26, 2020 5 - Production/Stable N/A `pytest-selenium-pdiff `_ A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A `pytest-send-email `_ Send pytest execution result email Dec 04, 2019 N/A N/A From e8d7a7b843972399d457ab6a912855bac90e61fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 03:01:58 +0000 Subject: [PATCH 0448/2846] build(deps): bump anyio[curio,trio] in /testing/plugins_integration Bumps [anyio[curio,trio]](https://github.com/agronholm/anyio) from 2.0.2 to 2.1.0. - [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.2...2.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 86c2a862c9b..f92f62c6967 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,4 +1,4 @@ -anyio[curio,trio]==2.0.2 +anyio[curio,trio]==2.1.0 django==3.1.5 pytest-asyncio==0.14.0 pytest-bdd==4.0.2 From ef14f286a389c930f54fc4a424fba4e0a5730c1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 03:02:01 +0000 Subject: [PATCH 0449/2846] build(deps): bump django in /testing/plugins_integration Bumps [django](https://github.com/django/django) from 3.1.5 to 3.1.6. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.1.5...3.1.6) 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 86c2a862c9b..d9337f6e034 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.5 +django==3.1.6 pytest-asyncio==0.14.0 pytest-bdd==4.0.2 pytest-cov==2.11.1 From 6432fc23029c650d7244d9bd103cc7b7ea161a93 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 16:50:48 +0000 Subject: [PATCH 0450/2846] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2eed21fc883..6b2d09e814e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,12 +29,12 @@ repos: - flake8-typing-imports==1.9.0 - flake8-docstrings==1.5.0 - repo: https://github.com/asottile/reorder_python_imports - rev: v2.3.6 + rev: v2.4.0 hooks: - id: reorder-python-imports args: ['--application-directories=.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.9.0 + rev: v2.10.0 hooks: - id: pyupgrade args: [--py36-plus] @@ -43,7 +43,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.7.0 + rev: v1.7.1 hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy From 3d831225bb3e3934e592b9573ab665f4d2922e83 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Wed, 10 Feb 2021 22:17:39 +0000 Subject: [PATCH 0451/2846] Remove duplicate '>=' in setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 14fdb6df5c0..ab6b2fb9379 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,7 +54,7 @@ python_requires = >=3.6 package_dir = =src setup_requires = - setuptools>=>=42.0 + setuptools>=42.0 setuptools-scm>=3.4 zip_safe = no From a0ae5fd652786c18f3f9a6218abbf16a14449b45 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sun, 14 Feb 2021 00:45:06 +0000 Subject: [PATCH 0452/2846] [automated] Update plugin list --- doc/en/plugin_list.rst | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/doc/en/plugin_list.rst b/doc/en/plugin_list.rst index cb70b0dcfdf..c7c9fdd2957 100644 --- a/doc/en/plugin_list.rst +++ b/doc/en/plugin_list.rst @@ -3,7 +3,7 @@ Plugins List PyPI projects that match "pytest-\*" are considered plugins and are listed automatically. Packages classified as inactive are excluded. -This list contains 822 plugins. +This list contains 827 plugins. ============================================================================================================== ======================================================================================================================================================================== ============== ===================== ============================================ name summary last release status requires @@ -34,7 +34,7 @@ name `pytest-apistellar `_ apistellar plugin for pytest. Jun 18, 2019 N/A N/A `pytest-appengine `_ AppEngine integration that works well with pytest-django Feb 27, 2017 N/A N/A `pytest-appium `_ Pytest plugin for appium Dec 05, 2019 N/A N/A -`pytest-approvaltests `_ A plugin to use approvaltests with pytest Jan 31, 2021 4 - Beta pytest (>=3.5.0) +`pytest-approvaltests `_ A plugin to use approvaltests with pytest Feb 07, 2021 4 - Beta pytest (>=3.5.0) `pytest-arraydiff `_ pytest plugin to help with comparing array output from tests Dec 06, 2018 4 - Beta pytest `pytest-asgi-server `_ Convenient ASGI client/server fixtures for Pytest Dec 12, 2020 N/A pytest (>=5.4.1) `pytest-asptest `_ test Answer Set Programming programs Apr 28, 2018 4 - Beta N/A @@ -148,6 +148,7 @@ name `pytest-csv `_ CSV output for pytest. Jun 24, 2019 N/A pytest (>=4.4) `pytest-curio `_ Pytest support for curio. Oct 07, 2020 N/A N/A `pytest-curl-report `_ pytest plugin to generate curl command line report Dec 11, 2016 4 - Beta N/A +`pytest-custom-concurrency `_ Custom grouping concurrence for pytest Feb 08, 2021 N/A N/A `pytest-custom-exit-code `_ Exit pytest test session with custom exit code in different scenarios Aug 07, 2019 4 - Beta pytest (>=4.0.2) `pytest-custom-report `_ Configure the symbols displayed for test outcomes Jan 30, 2019 N/A pytest `pytest-cython `_ A plugin for testing Cython extension modules Jan 26, 2021 4 - Beta pytest (>=2.7.3) @@ -212,7 +213,7 @@ name `pytest-docker-pexpect `_ pytest plugin for writing functional tests with pexpect and docker Jan 14, 2019 N/A pytest `pytest-docker-postgresql `_ A simple plugin to use with pytest Sep 24, 2019 4 - Beta pytest (>=3.5.0) `pytest-docker-py `_ Easy to use, simple to extend, pytest plugin that minimally leverages docker-py. Nov 27, 2018 N/A pytest (==4.0.0) -`pytest-docker-registry-fixtures `_ Pytest fixtures for testing with docker registries. Feb 03, 2021 4 - Beta pytest +`pytest-docker-registry-fixtures `_ Pytest fixtures for testing with docker registries. Feb 10, 2021 4 - Beta pytest `pytest-docker-tools `_ Docker integration tests for pytest Jan 15, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) `pytest-docs `_ Documentation tool for pytest Nov 11, 2018 4 - Beta pytest (>=3.5.0) `pytest-docstyle `_ pytest plugin to run pydocstyle Mar 23, 2020 3 - Alpha N/A @@ -306,6 +307,7 @@ name `pytest-freezegun `_ Wrap tests with fixtures in freeze_time Jul 19, 2020 4 - Beta pytest (>=3.0.0) `pytest-freeze-reqs `_ Check if requirement files are frozen Nov 14, 2019 N/A N/A `pytest-func-cov `_ Pytest plugin for measuring function coverage May 24, 2020 3 - Alpha pytest (>=5) +`pytest-funparam `_ An alternative way to parametrize test cases Feb 13, 2021 4 - Beta pytest (>=4.6.0) `pytest-fxa `_ pytest plugin for Firefox Accounts Aug 28, 2018 5 - Production/Stable N/A `pytest-fxtest `_ Oct 27, 2020 N/A N/A `pytest-gc `_ The garbage collector plugin for py.test Feb 01, 2018 N/A N/A @@ -313,7 +315,7 @@ name `pytest-gevent `_ Ensure that gevent is properly patched when invoking pytest Feb 25, 2020 N/A pytest `pytest-gherkin `_ A flexible framework for executing BDD gherkin tests Jul 27, 2019 3 - Alpha pytest (>=5.0.0) `pytest-ghostinspector `_ For finding/executing Ghost Inspector tests May 17, 2016 3 - Alpha N/A -`pytest-girder `_ A set of pytest fixtures for testing Girder applications. Jan 18, 2021 N/A N/A +`pytest-girder `_ A set of pytest fixtures for testing Girder applications. Feb 11, 2021 N/A N/A `pytest-git `_ Git repository fixture for py.test May 28, 2019 5 - Production/Stable pytest `pytest-gitcov `_ Pytest plugin for reporting on coverage of the last git commit. Jan 11, 2020 2 - Pre-Alpha N/A `pytest-git-fixtures `_ Pytest fixtures for testing with git. Jan 25, 2021 4 - Beta pytest @@ -337,7 +339,7 @@ name `pytest-historic `_ Custom report to display pytest historical execution records Apr 08, 2020 N/A pytest `pytest-historic-hook `_ Custom listener to store execution results into MYSQL DB, which is used for pytest-historic report Apr 08, 2020 N/A pytest `pytest-homeassistant `_ A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A -`pytest-homeassistant-custom-component `_ Experimental package to automatically extract test plugins for Home Assistant custom components Feb 01, 2021 3 - Alpha pytest (==6.2.2) +`pytest-homeassistant-custom-component `_ Experimental package to automatically extract test plugins for Home Assistant custom components Feb 10, 2021 3 - Alpha pytest (==6.2.2) `pytest-honors `_ Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A `pytest-hoverfly-wrapper `_ Integrates the Hoverfly HTTP proxy into Pytest Jan 31, 2021 4 - Beta N/A `pytest-html `_ pytest plugin for generating HTML reports Dec 13, 2020 5 - Production/Stable pytest (!=6.0.0,>=5.0) @@ -353,7 +355,7 @@ name `pytest-httpx `_ Send responses to httpx. Nov 25, 2020 5 - Production/Stable pytest (==6.*) `pytest-hue `_ Visualise PyTest status via your Phillips Hue lights May 09, 2019 N/A N/A `pytest-hypo-25 `_ help hypo module for pytest Jan 12, 2020 3 - Alpha N/A -`pytest-ibutsu `_ A plugin to sent pytest results to an Ibutsu server Dec 02, 2020 4 - Beta pytest +`pytest-ibutsu `_ A plugin to sent pytest results to an Ibutsu server Feb 11, 2021 4 - Beta pytest `pytest-icdiff `_ use icdiff for better error messages in pytest assertions Apr 08, 2020 4 - Beta N/A `pytest-idapro `_ A pytest plugin for idapython. Allows a pytest setup to run tests outside and inside IDA in an automated manner by runnig pytest inside IDA and by mocking idapython api Nov 03, 2018 N/A N/A `pytest-ignore-flaky `_ ignore failures from flaky tests (pytest plugin) Jan 14, 2019 5 - Production/Stable pytest (>=3.7) @@ -381,6 +383,7 @@ name `pytest-jasmine `_ Run jasmine tests from your pytest test suite Nov 04, 2017 1 - Planning N/A `pytest-jest `_ A custom jest-pytest oriented Pytest reporter May 22, 2018 4 - Beta pytest (>=3.3.2) `pytest-jira `_ py.test JIRA integration plugin, using markers Nov 29, 2019 N/A N/A +`pytest-jira-xray `_ pytest plugin to integrate tests with JIRA XRAY Feb 12, 2021 3 - Alpha pytest `pytest-jobserver `_ Limit parallel tests with posix jobserver. May 15, 2019 5 - Production/Stable pytest `pytest-joke `_ Test failures are better served with humor. Oct 08, 2019 4 - Beta pytest (>=4.2.1) `pytest-json `_ Generate JSON test reports Jan 18, 2016 4 - Beta N/A @@ -457,7 +460,7 @@ name `pytest-molecule `_ PyTest Molecule Plugin :: discover and run molecule tests Jan 25, 2021 5 - Production/Stable N/A `pytest-mongo `_ MongoDB process and client fixtures plugin for py.test. Jan 12, 2021 5 - Production/Stable pytest (>=3.0.0) `pytest-mongodb `_ pytest plugin for MongoDB fixtures Dec 07, 2019 5 - Production/Stable pytest (>=2.5.2) -`pytest-monitor `_ Pytest plugin for analyzing resource usage. Nov 20, 2020 5 - Production/Stable pytest +`pytest-monitor `_ Pytest plugin for analyzing resource usage. Feb 07, 2021 5 - Production/Stable pytest `pytest-monkeyplus `_ pytest's monkeypatch subclass with extra functionalities Sep 18, 2012 5 - Production/Stable N/A `pytest-monkeytype `_ pytest-monkeytype: Generate Monkeytype annotations from your pytest tests. Jul 29, 2020 4 - Beta N/A `pytest-moto `_ Fixtures for integration tests of AWS services,uses moto mocking library. Aug 28, 2015 1 - Planning N/A @@ -499,7 +502,7 @@ name `pytest-oot `_ Run object-oriented tests in a simple format Sep 18, 2016 4 - Beta N/A `pytest-openfiles `_ Pytest plugin for detecting inadvertent open file handles Apr 16, 2020 3 - Alpha pytest (>=4.6) `pytest-opentmi `_ pytest plugin for publish results to opentmi Jun 10, 2020 5 - Production/Stable pytest (>=5.0) -`pytest-operator `_ Fixtures for Operators Feb 04, 2021 N/A N/A +`pytest-operator `_ Fixtures for Operators Feb 12, 2021 N/A N/A `pytest-optional `_ include/exclude values of fixtures in pytest Oct 07, 2015 N/A N/A `pytest-optional-tests `_ Easy declaration of optional tests (i.e., that are not run by default) Jul 09, 2019 4 - Beta pytest (>=4.5.0) `pytest-orchestration `_ A pytest plugin for orchestrating tests Jul 18, 2019 N/A N/A @@ -515,6 +518,7 @@ name `pytest-parametrized `_ Pytest plugin for parametrizing tests with default iterables. Oct 19, 2020 5 - Production/Stable pytest `pytest-parawtf `_ Finally spell paramete?ri[sz]e correctly Dec 03, 2018 4 - Beta pytest (>=3.6.0) `pytest-pass `_ Check out https://github.com/elilutsky/pytest-pass Dec 04, 2019 N/A N/A +`pytest-passrunner `_ Pytest plugin providing the 'run_on_pass' marker Feb 10, 2021 5 - Production/Stable pytest (>=4.6.0) `pytest-paste-config `_ Allow setting the path to a paste config file Sep 18, 2013 3 - Alpha N/A `pytest-pdb `_ pytest plugin which adds pdb helper commands related to pytest. Jul 31, 2018 N/A N/A `pytest-peach `_ pytest plugin for fuzzing with Peach API Security Apr 12, 2019 4 - Beta pytest (>=2.8.7) @@ -549,7 +553,7 @@ name `pytest-pop `_ A pytest plugin to help with testing pop projects Aug 13, 2020 5 - Production/Stable pytest (>=5.4.0) `pytest-portion `_ Select a portion of the collected tests Jan 28, 2021 4 - Beta pytest (>=3.5.0) `pytest-postgres `_ Run PostgreSQL in Docker container in Pytest. Mar 22, 2020 N/A pytest -`pytest-postgresql `_ Postgresql fixtures and fixture factories for Pytest. Jan 31, 2021 5 - Production/Stable pytest (>=3.0.0) +`pytest-postgresql `_ Postgresql fixtures and fixture factories for Pytest. Feb 11, 2021 5 - Production/Stable pytest (>=3.0.0) `pytest-power `_ pytest plugin with powerful fixtures Dec 31, 2020 N/A pytest (>=5.4) `pytest-pride `_ Minitest-style test colors Apr 02, 2016 3 - Alpha N/A `pytest-print `_ pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) Oct 23, 2020 5 - Production/Stable pytest (>=3.0.0) @@ -637,7 +641,7 @@ name `pytest-rt `_ pytest data collector plugin for Testgr Jan 24, 2021 N/A N/A `pytest-rts `_ Coverage-based regression test selection (RTS) plugin for pytest Jan 29, 2021 N/A pytest `pytest-runfailed `_ implement a --failed option for pytest Mar 24, 2016 N/A N/A -`pytest-runner `_ Invoke py.test as distutils command with dependency resolution Oct 26, 2019 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' +`pytest-runner `_ Invoke py.test as distutils command with dependency resolution Feb 12, 2021 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' `pytest-salt `_ Pytest Salt Plugin Jan 27, 2020 4 - Beta N/A `pytest-salt-containers `_ A Pytest plugin that builds and creates docker containers Nov 09, 2016 4 - Beta N/A `pytest-salt-factories `_ Pytest Salt Plugin Jan 19, 2021 4 - Beta pytest (>=6.1.1) @@ -646,19 +650,19 @@ name `pytest-sanic `_ a pytest plugin for Sanic Sep 24, 2020 N/A pytest (>=5.2) `pytest-sanity `_ Dec 07, 2020 N/A N/A `pytest-sa-pg `_ May 14, 2019 N/A N/A -`pytest-sbase `_ A complete web automation framework for end-to-end testing. Feb 06, 2021 5 - Production/Stable N/A +`pytest-sbase `_ A complete web automation framework for end-to-end testing. Feb 13, 2021 5 - Production/Stable N/A `pytest-scenario `_ pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A `pytest-schema `_ 👍 Validate return values against a schema-like object in testing Aug 31, 2020 5 - Production/Stable pytest (>=3.5.0) `pytest-securestore `_ An encrypted password store for use within pytest cases Jun 19, 2019 4 - Beta N/A `pytest-select `_ A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) `pytest-selenium `_ pytest plugin for Selenium Sep 19, 2020 5 - Production/Stable pytest (>=5.0.0) -`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Feb 06, 2021 5 - Production/Stable N/A +`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Feb 13, 2021 5 - Production/Stable N/A `pytest-selenium-enhancer `_ pytest plugin for Selenium Nov 26, 2020 5 - Production/Stable N/A `pytest-selenium-pdiff `_ A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A `pytest-send-email `_ Send pytest execution result email Dec 04, 2019 N/A N/A `pytest-sentry `_ A pytest plugin to send testrun information to Sentry.io Dec 16, 2020 N/A N/A `pytest-server-fixtures `_ Extensible server fixures for py.test May 28, 2019 5 - Production/Stable pytest -`pytest-serverless `_ Automatically mocks resources from serverless.yml in pytest using moto. Dec 26, 2020 4 - Beta N/A +`pytest-serverless `_ Automatically mocks resources from serverless.yml in pytest using moto. Feb 11, 2021 4 - Beta N/A `pytest-services `_ Services plugin for pytest testing framework Oct 30, 2020 6 - Mature N/A `pytest-session2file `_ pytest-session2file (aka: pytest-session_to_file for v0.1.0 - v0.1.2) is a py.test plugin for capturing and saving to file the stdout of py.test. Jan 26, 2021 3 - Alpha pytest `pytest-session-fixture-globalize `_ py.test plugin to make session fixtures behave as if written in conftest, even if it is written in some modules May 15, 2018 4 - Beta N/A @@ -691,12 +695,12 @@ name `pytest-spawner `_ py.test plugin to spawn process and communicate with them. Jul 31, 2015 4 - Beta N/A `pytest-spec `_ Library pytest-spec is a pytest plugin to display test execution output like a SPECIFICATION. Jan 14, 2021 N/A N/A `pytest-sphinx `_ Doctest plugin for pytest with support for Sphinx-specific doctest-directives Aug 05, 2020 4 - Beta N/A -`pytest-spiratest `_ Exports unit tests as test runs in SpiraTest/Team/Plan Oct 30, 2020 N/A N/A +`pytest-spiratest `_ Exports unit tests as test runs in SpiraTest/Team/Plan Feb 12, 2021 N/A N/A `pytest-splinter `_ Splinter plugin for pytest testing framework Dec 25, 2020 6 - Mature N/A `pytest-split `_ Pytest plugin for splitting test suite based on test execution time Apr 07, 2020 1 - Planning N/A `pytest-splitio `_ Split.io SDK integration for e2e tests Sep 22, 2020 N/A pytest (<7,>=5.0) `pytest-split-tests `_ A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Forked from Mark Adams' original project pytest-test-groups. May 28, 2019 N/A pytest (>=2.5) -`pytest-splunk-addon `_ A Dynamic test tool for Splunk Apps and Add-ons Feb 01, 2021 N/A pytest (>5.4.0,<6.1) +`pytest-splunk-addon `_ A Dynamic test tool for Splunk Apps and Add-ons Feb 10, 2021 N/A pytest (>5.4.0,<6.1) `pytest-splunk-addon-ui-smartx `_ Library to support testing Splunk Add-on UX Jan 18, 2021 N/A N/A `pytest-splunk-env `_ pytest fixtures for interaction with Splunk Enterprise and Splunk Cloud Oct 22, 2020 N/A pytest (>=6.1.1,<7.0.0) `pytest-sqitch `_ sqitch for pytest Apr 06, 2020 4 - Beta N/A @@ -708,7 +712,7 @@ name `pytest-stepfunctions `_ A small description Jul 07, 2020 4 - Beta pytest `pytest-steps `_ Create step-wise / incremental tests in pytest. Apr 25, 2020 5 - Production/Stable N/A `pytest-stepwise `_ Run a test suite one failing test at a time. Dec 01, 2015 4 - Beta N/A -`pytest-stoq `_ A plugin to pytest stoq Nov 04, 2020 4 - Beta N/A +`pytest-stoq `_ A plugin to pytest stoq Feb 09, 2021 4 - Beta N/A `pytest-stress `_ A Pytest plugin that allows you to loop tests for a user defined amount of time. Dec 07, 2019 4 - Beta pytest (>=3.6.0) `pytest-structlog `_ Structured logging assertions Jul 16, 2020 N/A pytest `pytest-structmpd `_ provide structured temporary directory Oct 17, 2018 N/A N/A @@ -795,6 +799,7 @@ name `pytest-vcrpandas `_ Test from HTTP interactions to dataframe processed. Jan 12, 2019 4 - Beta pytest `pytest-venv `_ py.test fixture for creating a virtual environment Aug 04, 2020 4 - Beta pytest `pytest-verbose-parametrize `_ More descriptive output for parametrized py.test tests May 28, 2019 5 - Production/Stable pytest +`pytest-vimqf `_ A simple pytest plugin that will shrink pytest output when specified, to fit vim quickfix window. Feb 08, 2021 4 - Beta pytest (>=6.2.2,<7.0.0) `pytest-virtualenv `_ Virtualenv fixture for py.test May 28, 2019 5 - Production/Stable pytest `pytest-voluptuous `_ Pytest plugin for asserting data against voluptuous schema. Jun 09, 2020 N/A pytest `pytest-vscodedebug `_ A pytest plugin to easily enable debugging tests within Visual Studio Code Dec 04, 2020 4 - Beta N/A @@ -810,7 +815,7 @@ name `pytest-wholenodeid `_ pytest addon for displaying the whole node id for failures Aug 26, 2015 4 - Beta pytest (>=2.0) `pytest-winnotify `_ Windows tray notifications for py.test results. Apr 22, 2016 N/A N/A `pytest-workflow `_ A pytest plugin for configuring workflow/pipeline tests using YAML files Dec 14, 2020 5 - Production/Stable pytest (>=5.4.0) -`pytest-xdist `_ pytest xdist plugin for distributed testing and loop-on-failing modes Dec 14, 2020 5 - Production/Stable pytest (>=6.0.0) +`pytest-xdist `_ pytest xdist plugin for distributed testing and loop-on-failing modes Feb 09, 2021 5 - Production/Stable pytest (>=6.0.0) `pytest-xdist-debug-for-graingert `_ pytest xdist plugin for distributed testing and loop-on-failing modes Jul 24, 2019 5 - Production/Stable pytest (>=4.4.0) `pytest-xdist-forked `_ forked from pytest-xdist Feb 10, 2020 5 - Production/Stable pytest (>=4.4.0) `pytest-xfiles `_ Pytest fixtures providing data read from function, module or package related (x)files. Feb 27, 2018 N/A N/A From 532543b4ef0d35eeac9e8c04a937ab7190ae5988 Mon Sep 17 00:00:00 2001 From: maskypy40 <60002142+maskypy40@users.noreply.github.com> Date: Wed, 17 Feb 2021 09:33:04 +0100 Subject: [PATCH 0453/2846] Remove empty lines from code-block In assert.rst at line 175 and further there is an example of an assert encountering comparisons. The code-block for this example starts with a comment (line 177) and then it has 2 empty lines. Comparing this example code (test_assert2.py) with the previously mentioned example code on the same page (i.e. test_assert1.py) you can see that there should not be 2 empty lines after the comment. These 2 empty lines are removed. --- doc/en/assert.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/en/assert.rst b/doc/en/assert.rst index b83e30e76db..e6a23bcf3ac 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -175,8 +175,6 @@ when it encounters comparisons. For example: .. code-block:: python # content of test_assert2.py - - def test_set_comparison(): set1 = set("1308") set2 = set("8035") From 565fe0fb7d19a5cb66fe0d11a273d38fad856d28 Mon Sep 17 00:00:00 2001 From: Vincent Poulailleau Date: Thu, 18 Feb 2021 15:27:39 +0100 Subject: [PATCH 0454/2846] Update number of plugins According to the source, there are 801 plugins now! --- doc/en/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index 0361805d95a..084725ec2c1 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -66,7 +66,7 @@ Features - Python 3.6+ and PyPy 3 -- Rich plugin architecture, with over 315+ :doc:`external plugins ` and thriving community +- Rich plugin architecture, with over 800+ :doc:`external plugins ` and thriving community Documentation From e6012612b9a7fe2ef6a6c5f46b95855d5977d438 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 19 Feb 2021 13:46:29 -0500 Subject: [PATCH 0455/2846] Remove a redundant paragraph doc This paragraph looks like it is a more verbose version of the sentence right above it. Removing it doesn't reduce the amount of information here but does make the section flow a little better. --- doc/en/fixture.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 6ffd77920be..028786f6523 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -228,12 +228,6 @@ At a basic level, test functions request fixtures by declaring them as arguments, as in the ``test_my_fruit_in_basket(my_fruit, fruit_basket):`` in the previous example. -At a basic level, pytest depends on a test to tell it what fixtures it needs, so -we have to build that information into the test itself. We have to make the test -"**request**" the fixtures it depends on, and to do this, we have to -list those fixtures as parameters in the test function's "signature" (which is -the ``def test_something(blah, stuff, more):`` line). - When pytest goes to run a test, it looks at the parameters in that test function's signature, and then searches for fixtures that have the same names as those parameters. Once pytest finds them, it runs those fixtures, captures what From 0e5e4e03e64c9a607bcaf40f8192ca3049f8db53 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Sat, 20 Feb 2021 18:01:42 +0000 Subject: [PATCH 0456/2846] Remove some unused 'tmpdir's --- testing/code/test_source.py | 4 +--- testing/python/collect.py | 2 +- testing/python/integration.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 083a7911f55..5f2c6b1ea54 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -286,9 +286,7 @@ def g(): assert lines == ["def f():", " def g():", " pass"] -def test_source_of_class_at_eof_without_newline( - tmpdir, _sys_snapshot, tmp_path: Path -) -> None: +def test_source_of_class_at_eof_without_newline(_sys_snapshot, tmp_path: Path) -> None: # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. source = Source( diff --git a/testing/python/collect.py b/testing/python/collect.py index c52fb017d0c..22ac6464b89 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -274,7 +274,7 @@ def test_function_as_object_instance_ignored(self, pytester: Pytester) -> None: pytester.makepyfile( """ class A(object): - def __call__(self, tmpdir): + def __call__(self): 0/0 test_a = A() diff --git a/testing/python/integration.py b/testing/python/integration.py index 8576fcee341..77ebfa23ef2 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -234,7 +234,7 @@ def mock_basename(path): @mock.patch("os.path.abspath") @mock.patch("os.path.normpath") @mock.patch("os.path.basename", new=mock_basename) - def test_someting(normpath, abspath, tmpdir): + def test_someting(normpath, abspath): abspath.return_value = "this" os.path.normpath(os.path.abspath("hello")) normpath.assert_any_call("this") From 09d4c5e30a54ea1a914d75d19e0762c53a264566 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Sat, 20 Feb 2021 18:05:43 +0000 Subject: [PATCH 0457/2846] Rename variables 'tmpdir'->'tmp_path' Rename this variables reflecting the migrations made with 3bcd316f0 and ed658d682 --- testing/test_collection.py | 50 ++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/testing/test_collection.py b/testing/test_collection.py index 3dd9283eced..cf34ef118ca 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -127,16 +127,16 @@ def test_foo(): class TestCollectFS: 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"): + tmp_path = pytester.path + ensure_file(tmp_path / "build" / "test_notfound.py") + ensure_file(tmp_path / "dist" / "test_notfound.py") + ensure_file(tmp_path / "_darcs" / "test_notfound.py") + ensure_file(tmp_path / "CVS" / "test_notfound.py") + ensure_file(tmp_path / "{arch}" / "test_notfound.py") + ensure_file(tmp_path / ".whatever" / "test_notfound.py") + ensure_file(tmp_path / ".bzr" / "test_notfound.py") + ensure_file(tmp_path / "normal" / "test_found.py") + for x in Path(str(tmp_path)).rglob("test_*.py"): x.write_text("def test_hello(): pass", "utf-8") result = pytester.runpytest("--collect-only") @@ -226,10 +226,12 @@ def test_custom_norecursedirs(self, pytester: Pytester) -> None: norecursedirs = mydir xyz* """ ) - 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") + tmp_path = pytester.path + ensure_file(tmp_path / "mydir" / "test_hello.py").write_text( + "def test_1(): pass" + ) + ensure_file(tmp_path / "xyz123" / "test_2.py").write_text("def test_2(): 0/0") + ensure_file(tmp_path / "xy" / "test_ok.py").write_text("def test_3(): pass") rec = pytester.inline_run() rec.assertoutcome(passed=1) rec = pytester.inline_run("xyz123/test_2.py") @@ -242,10 +244,10 @@ def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> No testpaths = gui uts """ ) - 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") + tmp_path = pytester.path + ensure_file(tmp_path / "env" / "test_1.py").write_text("def test_env(): pass") + ensure_file(tmp_path / "gui" / "test_2.py").write_text("def test_gui(): pass") + ensure_file(tmp_path / "uts" / "test_3.py").write_text("def test_uts(): pass") # executing from rootdir only tests from `testpaths` directories # are collected @@ -255,7 +257,7 @@ def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> No # check that explicitly passing directories in the command-line # collects the tests for dirname in ("env", "gui", "uts"): - items, reprec = pytester.inline_genitems(tmpdir.joinpath(dirname)) + items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname)) assert [x.name for x in items] == ["test_%s" % dirname] # changing cwd to each subdirectory and running pytest without @@ -628,9 +630,9 @@ def test_method(self): class Test_getinitialnodes: def test_global_file(self, pytester: Pytester) -> None: - tmpdir = pytester.path - x = ensure_file(tmpdir / "x.py") - with tmpdir.cwd(): + tmp_path = pytester.path + x = ensure_file(tmp_path / "x.py") + with tmp_path.cwd(): config = pytester.parseconfigure(x) col = pytester.getnode(config, x) assert isinstance(col, pytest.Module) @@ -645,8 +647,8 @@ def test_pkgfile(self, pytester: Pytester) -> None: The parent chain should match: Module -> Package -> Session. Session's parent should always be None. """ - tmpdir = pytester.path - subdir = tmpdir.joinpath("subdir") + tmp_path = pytester.path + subdir = tmp_path.joinpath("subdir") x = ensure_file(subdir / "x.py") ensure_file(subdir / "__init__.py") with subdir.cwd(): From 56421aed0127d034841fc3af8b5039c1b63fe983 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sun, 21 Feb 2021 00:46:01 +0000 Subject: [PATCH 0458/2846] [automated] Update plugin list --- doc/en/plugin_list.rst | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/doc/en/plugin_list.rst b/doc/en/plugin_list.rst index c7c9fdd2957..5d93204bcd4 100644 --- a/doc/en/plugin_list.rst +++ b/doc/en/plugin_list.rst @@ -3,7 +3,7 @@ Plugins List PyPI projects that match "pytest-\*" are considered plugins and are listed automatically. Packages classified as inactive are excluded. -This list contains 827 plugins. +This list contains 831 plugins. ============================================================================================================== ======================================================================================================================================================================== ============== ===================== ============================================ name summary last release status requires @@ -29,7 +29,7 @@ name `pytest-ansible-playbook `_ Pytest fixture which runs given ansible playbook file. Mar 08, 2019 4 - Beta N/A `pytest-ansible-playbook-runner `_ Pytest fixture which runs given ansible playbook file. Dec 02, 2020 4 - Beta pytest (>=3.1.0) `pytest-antilru `_ Bust functools.lru_cache when running pytest to avoid test pollution Apr 11, 2019 5 - Production/Stable pytest -`pytest-anything `_ Pytest fixtures to assert anything and something Apr 03, 2020 N/A N/A +`pytest-anything `_ Pytest fixtures to assert anything and something Feb 18, 2021 N/A N/A `pytest-aoc `_ Downloads puzzle inputs for Advent of Code and synthesizes PyTest fixtures Dec 01, 2020 N/A pytest ; extra == 'dev' `pytest-apistellar `_ apistellar plugin for pytest. Jun 18, 2019 N/A N/A `pytest-appengine `_ AppEngine integration that works well with pytest-django Feb 27, 2017 N/A N/A @@ -73,6 +73,7 @@ name `pytest-black `_ A pytest plugin to enable format checking with black Oct 05, 2020 4 - Beta N/A `pytest-black-multipy `_ Allow '--black' on older Pythons Jan 14, 2021 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' `pytest-blame `_ A pytest plugin helps developers to debug by providing useful commits history. May 04, 2019 N/A pytest (>=4.4.0) +`pytest-blender `_ Blender Pytest plugin. Feb 15, 2021 N/A pytest (==6.2.1) ; extra == 'dev' `pytest-blink1 `_ Pytest plugin to emit notifications via the Blink(1) RGB LED Jan 07, 2018 4 - Beta N/A `pytest-blockage `_ Disable network requests during a test run. Feb 13, 2019 N/A pytest `pytest-blocker `_ pytest plugin to mark a test as blocker and skip all other tests Sep 07, 2015 4 - Beta N/A @@ -95,7 +96,7 @@ name `pytest-canonical-data `_ A plugin which allows to compare results with canonical results, based on previous runs May 08, 2020 2 - Pre-Alpha pytest (>=3.5.0) `pytest-caprng `_ A plugin that replays pRNG state on failure. May 02, 2018 4 - Beta N/A `pytest-capture-deprecatedwarnings `_ pytest plugin to capture all deprecatedwarnings and put them in one file Apr 30, 2019 N/A N/A -`pytest-cases `_ Separate test code from test cases in pytest. Jan 25, 2021 5 - Production/Stable N/A +`pytest-cases `_ Separate test code from test cases in pytest. Feb 19, 2021 5 - Production/Stable N/A `pytest-cassandra `_ Cassandra CCM Test Fixtures for pytest Nov 04, 2017 1 - Planning N/A `pytest-catchlog `_ py.test plugin to catch log messages. This is a fork of pytest-capturelog. Jan 24, 2016 4 - Beta pytest (>=2.6) `pytest-catch-server `_ Pytest plugin with server for catching HTTP requests. Dec 12, 2019 5 - Production/Stable N/A @@ -157,7 +158,7 @@ name `pytest-data `_ Useful functions for managing data for pytest fixtures Nov 01, 2016 5 - Production/Stable N/A `pytest-databricks `_ Pytest plugin for remote Databricks notebooks testing Jul 29, 2020 N/A pytest `pytest-datadir `_ pytest plugin for test data directories and files Oct 22, 2019 5 - Production/Stable pytest (>=2.7.0) -`pytest-datadir-mgr `_ Manager for test data providing downloads, caching of generated files, and a context for temp directories. Jan 08, 2021 5 - Production/Stable pytest (>=6.0.1,<7.0.0) +`pytest-datadir-mgr `_ Manager for test data providing downloads, caching of generated files, and a context for temp directories. Feb 17, 2021 5 - Production/Stable pytest (>=6.0.1,<7.0.0) `pytest-datadir-ng `_ Fixtures for pytest allowing test functions/methods to easily retrieve test resources from the local filesystem. Dec 25, 2019 5 - Production/Stable pytest `pytest-data-file `_ Fixture "data" and "case_data" for test from yaml file Dec 04, 2019 N/A N/A `pytest-datafiles `_ py.test plugin to create a 'tmpdir' containing predefined files/directories. Oct 07, 2018 5 - Production/Stable pytest (>=3.6) @@ -183,7 +184,7 @@ name `pytest-diffeo `_ Common py.test support for Diffeo packages Apr 08, 2016 3 - Alpha N/A `pytest-disable `_ pytest plugin to disable a test and skip it from testrun Sep 10, 2015 4 - Beta N/A `pytest-disable-plugin `_ Disable plugins per test Feb 28, 2019 4 - Beta pytest (>=3.5.0) -`pytest-discord `_ A pytest plugin to notify test results to a Discord channel. Aug 15, 2020 3 - Alpha pytest (!=6.0.0,<7,>=3.3.2) +`pytest-discord `_ A pytest plugin to notify test results to a Discord channel. Feb 14, 2021 3 - Alpha pytest (!=6.0.0,<7,>=3.3.2) `pytest-django `_ A Django plugin for pytest. Oct 22, 2020 5 - Production/Stable pytest (>=5.4.0) `pytest-django-ahead `_ A Django plugin for pytest. Oct 27, 2016 5 - Production/Stable pytest (>=2.9) `pytest-djangoapp `_ Nice pytest plugin to help you with Django pluggable application testing. Sep 21, 2020 4 - Beta N/A @@ -213,7 +214,7 @@ name `pytest-docker-pexpect `_ pytest plugin for writing functional tests with pexpect and docker Jan 14, 2019 N/A pytest `pytest-docker-postgresql `_ A simple plugin to use with pytest Sep 24, 2019 4 - Beta pytest (>=3.5.0) `pytest-docker-py `_ Easy to use, simple to extend, pytest plugin that minimally leverages docker-py. Nov 27, 2018 N/A pytest (==4.0.0) -`pytest-docker-registry-fixtures `_ Pytest fixtures for testing with docker registries. Feb 10, 2021 4 - Beta pytest +`pytest-docker-registry-fixtures `_ Pytest fixtures for testing with docker registries. Feb 17, 2021 4 - Beta pytest `pytest-docker-tools `_ Docker integration tests for pytest Jan 15, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) `pytest-docs `_ Documentation tool for pytest Nov 11, 2018 4 - Beta pytest (>=3.5.0) `pytest-docstyle `_ pytest plugin to run pydocstyle Mar 23, 2020 3 - Alpha N/A @@ -351,7 +352,7 @@ name `pytest-httpbin `_ Easily test your HTTP library against a local copy of httpbin Feb 11, 2019 5 - Production/Stable N/A `pytest-http-mocker `_ Pytest plugin for http mocking (via https://github.com/vilus/mocker) Oct 20, 2019 N/A N/A `pytest-httpretty `_ A thin wrapper of HTTPretty for pytest Feb 16, 2014 3 - Alpha N/A -`pytest-httpserver `_ pytest-httpserver is a httpserver for pytest Feb 04, 2021 3 - Alpha pytest ; extra == 'dev' +`pytest-httpserver `_ pytest-httpserver is a httpserver for pytest Feb 14, 2021 3 - Alpha pytest ; extra == 'dev' `pytest-httpx `_ Send responses to httpx. Nov 25, 2020 5 - Production/Stable pytest (==6.*) `pytest-hue `_ Visualise PyTest status via your Phillips Hue lights May 09, 2019 N/A N/A `pytest-hypo-25 `_ help hypo module for pytest Jan 12, 2020 3 - Alpha N/A @@ -366,7 +367,7 @@ name `pytest-informative-node `_ display more node ininformation. Apr 25, 2019 4 - Beta N/A `pytest-infrastructure `_ pytest stack validation prior to testing executing Apr 12, 2020 4 - Beta N/A `pytest-inmanta `_ A py.test plugin providing fixtures to simplify inmanta modules testing. Oct 12, 2020 5 - Production/Stable N/A -`pytest-inmanta-extensions `_ Inmanta tests package Nov 25, 2020 5 - Production/Stable N/A +`pytest-inmanta-extensions `_ Inmanta tests package Jan 07, 2021 5 - Production/Stable N/A `pytest-Inomaly `_ A simple image diff plugin for pytest Feb 13, 2018 4 - Beta N/A `pytest-insta `_ A practical snapshot testing plugin for pytest Nov 29, 2020 N/A pytest (>=6.0.2,<7.0.0) `pytest-instafail `_ pytest plugin to show failures instantly Jun 14, 2020 4 - Beta pytest (>=2.9) @@ -452,7 +453,7 @@ name `pytest-mock-helper `_ Help you mock HTTP call and generate mock code Jan 24, 2018 N/A pytest `pytest-mockito `_ Base fixtures for mockito Jul 11, 2018 4 - Beta N/A `pytest-mockredis `_ An in-memory mock of a Redis server that runs in a separate thread. This is to be used for unit-tests that require a Redis database. Jan 02, 2018 2 - Pre-Alpha N/A -`pytest-mock-resources `_ A pytest plugin for easily instantiating reproducible mock resources. Oct 08, 2020 N/A pytest (>=1.0) +`pytest-mock-resources `_ A pytest plugin for easily instantiating reproducible mock resources. Feb 17, 2021 N/A pytest (>=1.0) `pytest-mock-server `_ Mock server plugin for pytest Apr 06, 2020 4 - Beta N/A `pytest-mockservers `_ A set of fixtures to test your requests to HTTP/UDP servers Mar 31, 2020 N/A pytest (>=4.3.0) `pytest-modifyjunit `_ Utility for adding additional properties to junit xml for IDM QE Jan 10, 2019 N/A N/A @@ -474,6 +475,7 @@ name `pytest-mypy `_ Mypy static type checker plugin for Pytest Nov 14, 2020 4 - Beta pytest (>=3.5) `pytest-mypyd `_ Mypy static type checker plugin for Pytest Aug 20, 2019 4 - Beta pytest (<4.7,>=2.8) ; python_version < "3.5" `pytest-mypy-plugins `_ pytest plugin for writing tests for mypy plugins Oct 26, 2020 3 - Alpha pytest (>=6.0.0) +`pytest-mypy-plugins-shim `_ Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy. Feb 14, 2021 N/A pytest (>=6.0.0) `pytest-mypy-testing `_ Pytest plugin to check mypy output. Apr 24, 2020 N/A pytest `pytest-mysql `_ MySQL process and client fixtures for pytest Jul 21, 2020 5 - Production/Stable pytest (>=3.0.0) `pytest-needle `_ pytest plugin for visual testing websites using selenium Dec 10, 2018 4 - Beta pytest (<5.0.0,>=3.0.0) @@ -502,11 +504,11 @@ name `pytest-oot `_ Run object-oriented tests in a simple format Sep 18, 2016 4 - Beta N/A `pytest-openfiles `_ Pytest plugin for detecting inadvertent open file handles Apr 16, 2020 3 - Alpha pytest (>=4.6) `pytest-opentmi `_ pytest plugin for publish results to opentmi Jun 10, 2020 5 - Production/Stable pytest (>=5.0) -`pytest-operator `_ Fixtures for Operators Feb 12, 2021 N/A N/A +`pytest-operator `_ Fixtures for Operators Feb 20, 2021 N/A N/A `pytest-optional `_ include/exclude values of fixtures in pytest Oct 07, 2015 N/A N/A `pytest-optional-tests `_ Easy declaration of optional tests (i.e., that are not run by default) Jul 09, 2019 4 - Beta pytest (>=4.5.0) `pytest-orchestration `_ A pytest plugin for orchestrating tests Jul 18, 2019 N/A N/A -`pytest-order `_ pytest plugin to run your tests in a specific order Jan 27, 2021 4 - Beta pytest (>=3.7) +`pytest-order `_ pytest plugin to run your tests in a specific order Feb 16, 2021 4 - Beta pytest (>=3.7) `pytest-ordering `_ pytest plugin to run your tests in a specific order Nov 14, 2018 4 - Beta pytest `pytest-osxnotify `_ OS X notifications for py.test results. May 15, 2015 N/A N/A `pytest-pact `_ A simple plugin to use with pytest Jan 07, 2019 4 - Beta N/A @@ -571,7 +573,7 @@ name `pytest-pylint `_ pytest plugin to check source code with pylint Nov 09, 2020 5 - Production/Stable pytest (>=5.4) `pytest-pypi `_ Easily test your HTTP library against a local copy of pypi Mar 04, 2018 3 - Alpha N/A `pytest-pypom-navigation `_ Core engine for cookiecutter-qa and pytest-play packages Feb 18, 2019 4 - Beta pytest (>=3.0.7) -`pytest-pyppeteer `_ A plugin to run pyppeteer in pytest. Nov 27, 2020 4 - Beta pytest (>=6.0.2) +`pytest-pyppeteer `_ A plugin to run pyppeteer in pytest. Feb 16, 2021 4 - Beta pytest (>=6.0.2) `pytest-pyq `_ Pytest fixture "q" for pyq Mar 10, 2020 5 - Production/Stable N/A `pytest-pyramid `_ pytest pyramid providing basic fixtures for testing pyramid applications with pytest test suite Jun 05, 2020 4 - Beta pytest `pytest-pyramid-server `_ Pyramid server fixture for py.test May 28, 2019 5 - Production/Stable pytest @@ -619,13 +621,13 @@ name `pytest-reportlog `_ Replacement for the --resultlog option, focused in simplicity and extensibility Dec 11, 2020 3 - Alpha pytest (>=5.2) `pytest-report-me `_ A pytest plugin to generate report. Dec 31, 2020 N/A pytest `pytest-report-parameters `_ pytest plugin for adding tests' parameters to junit report Jun 18, 2020 3 - Alpha pytest (>=2.4.2) -`pytest-reportportal `_ Agent for Reporting results of tests to the Report Portal Dec 14, 2020 N/A pytest (>=3.0.7) +`pytest-reportportal `_ Agent for Reporting results of tests to the Report Portal Feb 15, 2021 N/A pytest (>=3.0.7) `pytest-reqs `_ pytest plugin to check pinned requirements May 12, 2019 N/A pytest (>=2.4.2) `pytest-requests `_ A simple plugin to use with pytest Jun 24, 2019 4 - Beta pytest (>=3.5.0) `pytest-reraise `_ Make multi-threaded pytest test cases fail when they should Jun 03, 2020 5 - Production/Stable N/A `pytest-rerun `_ Re-run only changed files in specified branch Jul 08, 2019 N/A pytest (>=3.6) `pytest-rerunfailures `_ pytest plugin to re-run tests to eliminate flaky failures Sep 29, 2020 5 - Production/Stable pytest (>=5.0) -`pytest-resilient-circuits `_ Resilient Circuits fixtures for PyTest. Jan 21, 2021 N/A N/A +`pytest-resilient-circuits `_ Resilient Circuits fixtures for PyTest. Feb 19, 2021 N/A N/A `pytest-resource `_ Load resource fixture plugin to use with pytest Nov 14, 2018 4 - Beta N/A `pytest-resource-path `_ Provides path for uniform access to test resources in isolated directory Aug 18, 2020 5 - Production/Stable pytest (>=3.5.0) `pytest-responsemock `_ Simplified requests calls mocking for pytest Oct 10, 2020 5 - Production/Stable N/A @@ -639,30 +641,30 @@ name `pytest-rotest `_ Pytest integration with rotest Sep 08, 2019 N/A pytest (>=3.5.0) `pytest-rpc `_ Extend py.test for RPC OpenStack testing. Feb 22, 2019 4 - Beta pytest (~=3.6) `pytest-rt `_ pytest data collector plugin for Testgr Jan 24, 2021 N/A N/A -`pytest-rts `_ Coverage-based regression test selection (RTS) plugin for pytest Jan 29, 2021 N/A pytest +`pytest-rts `_ Coverage-based regression test selection (RTS) plugin for pytest Feb 15, 2021 N/A pytest `pytest-runfailed `_ implement a --failed option for pytest Mar 24, 2016 N/A N/A `pytest-runner `_ Invoke py.test as distutils command with dependency resolution Feb 12, 2021 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' `pytest-salt `_ Pytest Salt Plugin Jan 27, 2020 4 - Beta N/A `pytest-salt-containers `_ A Pytest plugin that builds and creates docker containers Nov 09, 2016 4 - Beta N/A -`pytest-salt-factories `_ Pytest Salt Plugin Jan 19, 2021 4 - Beta pytest (>=6.1.1) +`pytest-salt-factories `_ Pytest Salt Plugin Feb 19, 2021 4 - Beta pytest (>=6.1.1) `pytest-salt-from-filenames `_ Simple PyTest Plugin For Salt's Test Suite Specifically Jan 29, 2019 4 - Beta pytest (>=4.1) `pytest-salt-runtests-bridge `_ Simple PyTest Plugin For Salt's Test Suite Specifically Dec 05, 2019 4 - Beta pytest (>=4.1) `pytest-sanic `_ a pytest plugin for Sanic Sep 24, 2020 N/A pytest (>=5.2) `pytest-sanity `_ Dec 07, 2020 N/A N/A `pytest-sa-pg `_ May 14, 2019 N/A N/A -`pytest-sbase `_ A complete web automation framework for end-to-end testing. Feb 13, 2021 5 - Production/Stable N/A +`pytest-sbase `_ A complete web automation framework for end-to-end testing. Feb 19, 2021 5 - Production/Stable N/A `pytest-scenario `_ pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A `pytest-schema `_ 👍 Validate return values against a schema-like object in testing Aug 31, 2020 5 - Production/Stable pytest (>=3.5.0) `pytest-securestore `_ An encrypted password store for use within pytest cases Jun 19, 2019 4 - Beta N/A `pytest-select `_ A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) `pytest-selenium `_ pytest plugin for Selenium Sep 19, 2020 5 - Production/Stable pytest (>=5.0.0) -`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Feb 13, 2021 5 - Production/Stable N/A +`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Feb 19, 2021 5 - Production/Stable N/A `pytest-selenium-enhancer `_ pytest plugin for Selenium Nov 26, 2020 5 - Production/Stable N/A `pytest-selenium-pdiff `_ A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A `pytest-send-email `_ Send pytest execution result email Dec 04, 2019 N/A N/A `pytest-sentry `_ A pytest plugin to send testrun information to Sentry.io Dec 16, 2020 N/A N/A `pytest-server-fixtures `_ Extensible server fixures for py.test May 28, 2019 5 - Production/Stable pytest -`pytest-serverless `_ Automatically mocks resources from serverless.yml in pytest using moto. Feb 11, 2021 4 - Beta N/A +`pytest-serverless `_ Automatically mocks resources from serverless.yml in pytest using moto. Feb 20, 2021 4 - Beta N/A `pytest-services `_ Services plugin for pytest testing framework Oct 30, 2020 6 - Mature N/A `pytest-session2file `_ pytest-session2file (aka: pytest-session_to_file for v0.1.0 - v0.1.2) is a py.test plugin for capturing and saving to file the stdout of py.test. Jan 26, 2021 3 - Alpha pytest `pytest-session-fixture-globalize `_ py.test plugin to make session fixtures behave as if written in conftest, even if it is written in some modules May 15, 2018 4 - Beta N/A @@ -682,6 +684,7 @@ name `pytest-slack `_ Pytest to Slack reporting plugin Dec 15, 2020 5 - Production/Stable N/A `pytest-smartcollect `_ A plugin for collecting tests that touch changed code Oct 04, 2018 N/A pytest (>=3.5.0) `pytest-smartcov `_ Smart coverage plugin for pytest. Sep 30, 2017 3 - Alpha N/A +`pytest-smtp `_ Send email with pytest execution result Feb 20, 2021 N/A pytest `pytest-snail `_ Plugin for adding a marker to slow running tests. 🐌 Nov 04, 2019 3 - Alpha pytest (>=5.0.1) `pytest-snapci `_ py.test plugin for Snap-CI Nov 12, 2015 N/A N/A `pytest-snapshot `_ A plugin to enable snapshot testing with pytest. Jan 22, 2021 4 - Beta pytest (>=3.0.0) @@ -706,6 +709,7 @@ name `pytest-sqitch `_ sqitch for pytest Apr 06, 2020 4 - Beta N/A `pytest-sqlalchemy `_ pytest plugin with sqlalchemy related fixtures Mar 13, 2018 3 - Alpha N/A `pytest-sql-bigquery `_ Yet another SQL-testing framework for BigQuery provided by pytest plugin Dec 19, 2019 N/A pytest +`pytest-srcpaths `_ Add paths to sys.path Feb 18, 2021 N/A N/A `pytest-ssh `_ pytest plugin for ssh command run May 27, 2019 N/A pytest `pytest-start-from `_ Start pytest run from a given point Apr 11, 2016 N/A N/A `pytest-statsd `_ pytest plugin for reporting to graphite Nov 30, 2018 5 - Production/Stable pytest (>=3.0.0) From c9bb4c418f889820f786cbf8e63cbc057c653399 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Sun, 21 Feb 2021 13:10:00 +0000 Subject: [PATCH 0459/2846] fixup! Rename variables 'tmpdir'->'tmp_path' * Add some more of these * Also reintroduce+rename instances of fixture usages that were 'tmpdir'->'tmp_path' --- testing/python/collect.py | 2 +- testing/python/integration.py | 2 +- testing/test_conftest.py | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/testing/python/collect.py b/testing/python/collect.py index 22ac6464b89..4256851e254 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -274,7 +274,7 @@ def test_function_as_object_instance_ignored(self, pytester: Pytester) -> None: pytester.makepyfile( """ class A(object): - def __call__(self): + def __call__(self, tmp_path): 0/0 test_a = A() diff --git a/testing/python/integration.py b/testing/python/integration.py index 77ebfa23ef2..1ab2149ff07 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -234,7 +234,7 @@ def mock_basename(path): @mock.patch("os.path.abspath") @mock.patch("os.path.normpath") @mock.patch("os.path.basename", new=mock_basename) - def test_someting(normpath, abspath): + def test_someting(normpath, abspath, tmp_path): abspath.return_value = "this" os.path.normpath(os.path.abspath("hello")) normpath.assert_any_call("this") diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 80f2a6d0bc0..3497b7cc4fd 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -44,15 +44,15 @@ class TestConftestValueAccessGlobal: def basedir( self, request, tmp_path_factory: TempPathFactory ) -> Generator[Path, None, None]: - tmpdir = tmp_path_factory.mktemp("basedir", numbered=True) - tmpdir.joinpath("adir/b").mkdir(parents=True) - tmpdir.joinpath("adir/conftest.py").write_text("a=1 ; Directory = 3") - tmpdir.joinpath("adir/b/conftest.py").write_text("b=2 ; a = 1.5") + tmp_path = tmp_path_factory.mktemp("basedir", numbered=True) + tmp_path.joinpath("adir/b").mkdir(parents=True) + tmp_path.joinpath("adir/conftest.py").write_text("a=1 ; Directory = 3") + tmp_path.joinpath("adir/b/conftest.py").write_text("b=2 ; a = 1.5") if request.param == "inpackage": - tmpdir.joinpath("adir/__init__.py").touch() - tmpdir.joinpath("adir/b/__init__.py").touch() + tmp_path.joinpath("adir/__init__.py").touch() + tmp_path.joinpath("adir/b/__init__.py").touch() - yield tmpdir + yield tmp_path def test_basic_init(self, basedir: Path) -> None: conftest = PytestPluginManager() From 060cbef2604c7f0dd5122aca0e0cdd77c5b2c01f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 03:01:27 +0000 Subject: [PATCH 0460/2846] build(deps): bump django in /testing/plugins_integration Bumps [django](https://github.com/django/django) from 3.1.6 to 3.1.7. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.1.6...3.1.7) 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 6460e838535..730d1f028fe 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,5 +1,5 @@ anyio[curio,trio]==2.1.0 -django==3.1.6 +django==3.1.7 pytest-asyncio==0.14.0 pytest-bdd==4.0.2 pytest-cov==2.11.1 From e503e27579cb7a8bc7bb6efb252e6aada5a17ee0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 17:04:30 +0000 Subject: [PATCH 0461/2846] [pre-commit.ci] pre-commit autoupdate --- .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 6b2d09e814e..fed7ca83cbb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.800 + rev: v0.812 hooks: - id: mypy files: ^(src/|testing/) From 54a154c86f4806327081b80193cebca7934468d0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 23 Feb 2021 17:56:42 +0100 Subject: [PATCH 0462/2846] Allow Class.from_parent to forward custom parameters to the constructor Similarly to #7143, at work we have a project with a custom pytest.Class subclass, adding an additional argument to the constructor. All from_parent implementations in pytest accept and forward *kw, except Class (before this change) and DoctestItem - since I'm not familiar with doctest support, I've left the latter as-is. --- changelog/8367.bugfix.rst | 1 + src/_pytest/python.py | 4 ++-- testing/test_collection.py | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 changelog/8367.bugfix.rst diff --git a/changelog/8367.bugfix.rst b/changelog/8367.bugfix.rst new file mode 100644 index 00000000000..f4b03670108 --- /dev/null +++ b/changelog/8367.bugfix.rst @@ -0,0 +1 @@ +Fix ``Class.from_parent`` so it forwards extra keyword arguments to the constructor. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 726241cb5b9..944c395a84d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -763,9 +763,9 @@ class Class(PyCollector): """Collector for test methods.""" @classmethod - def from_parent(cls, parent, *, name, obj=None): + def from_parent(cls, parent, *, name, obj=None, **kw): """The public constructor.""" - return super().from_parent(name=name, parent=parent) + return super().from_parent(name=name, parent=parent, **kw) def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: if not safe_getattr(self.obj, "__test__", True): diff --git a/testing/test_collection.py b/testing/test_collection.py index 3dd9283eced..39538ae98cc 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1360,6 +1360,24 @@ def from_parent(cls, parent, *, fspath, x): assert collector.x == 10 +def test_class_from_parent(pytester: Pytester, request: FixtureRequest) -> None: + """Ensure Class.from_parent can forward custom arguments to the constructor.""" + + class MyCollector(pytest.Class): + def __init__(self, name, parent, x): + super().__init__(name, parent) + self.x = x + + @classmethod + def from_parent(cls, parent, *, name, x): + return super().from_parent(parent=parent, name=name, x=x) + + collector = MyCollector.from_parent( + parent=request.session, name="foo", x=10 + ) + assert collector.x == 10 + + class TestImportModeImportlib: def test_collect_duplicate_names(self, pytester: Pytester) -> None: """--import-mode=importlib can import modules with same names that are not in packages.""" From 3b7fc2c9c839148d19518af655a1d347351286b0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Feb 2021 17:02:45 +0000 Subject: [PATCH 0463/2846] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_collection.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/testing/test_collection.py b/testing/test_collection.py index 39538ae98cc..298c2dde1a0 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1372,9 +1372,7 @@ def __init__(self, name, parent, x): def from_parent(cls, parent, *, name, x): return super().from_parent(parent=parent, name=name, x=x) - collector = MyCollector.from_parent( - parent=request.session, name="foo", x=10 - ) + collector = MyCollector.from_parent(parent=request.session, name="foo", x=10) assert collector.x == 10 From 9d09d1991186d842dce4dc2ea023996b3c091fc8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 23 Feb 2021 18:03:10 +0100 Subject: [PATCH 0464/2846] Fix typo in changelog See #7143 --- doc/en/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 3e854f59971..967fca2098a 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -1028,7 +1028,7 @@ Bug Fixes - `#7110 `_: Fixed regression: ``asyncbase.TestCase`` tests are executed correctly again. -- `#7143 `_: Fix ``File.from_constructor`` so it forwards extra keyword arguments to the constructor. +- `#7143 `_: Fix ``File.from_parent`` so it forwards extra keyword arguments to the constructor. - `#7145 `_: Classes with broken ``__getattribute__`` methods are displayed correctly during failures. From 514f8e068080b374b470f6c9f349c48edfc0d3d8 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Wed, 24 Feb 2021 20:55:35 +0000 Subject: [PATCH 0465/2846] fixup! Rename variables 'tmpdir'->'tmp_path' --- testing/test_collection.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/testing/test_collection.py b/testing/test_collection.py index cf34ef118ca..9060ef36c2d 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -136,7 +136,7 @@ def test_ignored_certain_directories(self, pytester: Pytester) -> None: ensure_file(tmp_path / ".whatever" / "test_notfound.py") ensure_file(tmp_path / ".bzr" / "test_notfound.py") ensure_file(tmp_path / "normal" / "test_found.py") - for x in Path(str(tmp_path)).rglob("test_*.py"): + for x in tmp_path.rglob("test_*.py"): x.write_text("def test_hello(): pass", "utf-8") result = pytester.runpytest("--collect-only") @@ -632,8 +632,7 @@ class Test_getinitialnodes: def test_global_file(self, pytester: Pytester) -> None: tmp_path = pytester.path x = ensure_file(tmp_path / "x.py") - with tmp_path.cwd(): - config = pytester.parseconfigure(x) + config = pytester.parseconfigure(x) col = pytester.getnode(config, x) assert isinstance(col, pytest.Module) assert col.name == "x.py" From b7f2d7ca61d6169495e5780fffc252daaacd6583 Mon Sep 17 00:00:00 2001 From: Simon K Date: Thu, 25 Feb 2021 08:28:57 +0000 Subject: [PATCH 0466/2846] Fixed an issue where `getpass.getuser()` contained illegal characters for file directories (#8365) * retry writing pytest-of dir when invalid chars are in directory name * add unit tests for getbasetemp() and changelog * patch _basetemp & _given_basetemp for testing basetemp() * Tweak changelog for #8317, tidy up comments --- changelog/8317.bugfix.rst | 1 + src/_pytest/tmpdir.py | 7 ++++++- testing/test_tmpdir.py | 12 ++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 changelog/8317.bugfix.rst diff --git a/changelog/8317.bugfix.rst b/changelog/8317.bugfix.rst new file mode 100644 index 00000000000..7312880a11f --- /dev/null +++ b/changelog/8317.bugfix.rst @@ -0,0 +1 @@ +Fixed an issue where illegal directory characters derived from ``getpass.getuser()`` raised an ``OSError``. diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 29c7e19d7b4..47729ae5fee 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -115,7 +115,12 @@ def getbasetemp(self) -> Path: # use a sub-directory in the temproot to speed-up # make_numbered_dir() call rootdir = temproot.joinpath(f"pytest-of-{user}") - rootdir.mkdir(exist_ok=True) + try: + rootdir.mkdir(exist_ok=True) + except OSError: + # getuser() likely returned illegal characters for the platform, use unknown back off mechanism + rootdir = temproot.joinpath("pytest-of-unknown") + 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/test_tmpdir.py b/testing/test_tmpdir.py index d123287aa38..4dec9c59a3c 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -11,6 +11,7 @@ import pytest from _pytest import pathlib from _pytest.config import Config +from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import cleanup_numbered_dir from _pytest.pathlib import create_cleanup_lock from _pytest.pathlib import make_numbered_dir @@ -445,3 +446,14 @@ def test(tmp_path): # running a second time and ensure we don't crash result = pytester.runpytest("--basetemp=tmp") assert result.ret == 0 + + +def test_tmp_path_factory_handles_invalid_dir_characters( + tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch +) -> None: + monkeypatch.setattr("getpass.getuser", lambda: "os/<:*?;>agnostic") + # _basetemp / _given_basetemp are cached / set in parallel runs, patch them + monkeypatch.setattr(tmp_path_factory, "_basetemp", None) + monkeypatch.setattr(tmp_path_factory, "_given_basetemp", None) + p = tmp_path_factory.getbasetemp() + assert "pytest-of-unknown" in str(p) From 22c0dace3b67ac2aaf8d45f6f73ed9838c30e8eb Mon Sep 17 00:00:00 2001 From: Simon K Date: Thu, 25 Feb 2021 20:32:27 +0000 Subject: [PATCH 0467/2846] change istestfunction to callable() (#8374) --- src/_pytest/python.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 944c395a84d..40116ab9c5a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -384,10 +384,7 @@ def istestfunction(self, obj: object, name: str) -> bool: if isinstance(obj, staticmethod): # staticmethods need to be unwrapped. obj = safe_getattr(obj, "__func__", False) - return ( - safe_getattr(obj, "__call__", False) - and fixtures.getfixturemarker(obj) is None - ) + return callable(obj) and fixtures.getfixturemarker(obj) is None else: return False From a623b1b0861719a48d89f3ee7ac18250b7ea06ca Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sun, 28 Feb 2021 00:48:46 +0000 Subject: [PATCH 0468/2846] [automated] Update plugin list --- doc/en/plugin_list.rst | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/doc/en/plugin_list.rst b/doc/en/plugin_list.rst index 5d93204bcd4..f50b6033ff1 100644 --- a/doc/en/plugin_list.rst +++ b/doc/en/plugin_list.rst @@ -3,7 +3,7 @@ Plugins List PyPI projects that match "pytest-\*" are considered plugins and are listed automatically. Packages classified as inactive are excluded. -This list contains 831 plugins. +This list contains 833 plugins. ============================================================================================================== ======================================================================================================================================================================== ============== ===================== ============================================ name summary last release status requires @@ -11,6 +11,7 @@ name `pytest-adaptavist `_ pytest plugin for generating test execution results within Jira Test Management (tm4j) Feb 05, 2020 N/A pytest (>=3.4.1) `pytest-adf `_ Pytest plugin for writing Azure Data Factory integration tests Jun 03, 2020 4 - Beta pytest (>=3.5.0) `pytest-aggreport `_ pytest plugin for pytest-repeat that generate aggregate report of the same test cases with additional statistics details. Jul 19, 2019 4 - Beta pytest (>=4.3.1) +`pytest-aio `_ Pytest plugin for testing async python code Feb 27, 2021 4 - Beta pytest ; extra == 'tests' `pytest-aiofiles `_ pytest fixtures for writing aiofiles tests with pyfakefs May 14, 2017 5 - Production/Stable N/A `pytest-aiohttp `_ pytest plugin for aiohttp support Dec 05, 2017 N/A pytest `pytest-aiohttp-client `_ Pytest `client` fixture for the Aiohttp Nov 01, 2020 N/A pytest (>=6) @@ -59,7 +60,7 @@ name `pytest-aws `_ pytest plugin for testing AWS resource configurations Oct 04, 2017 4 - Beta N/A `pytest-axe `_ pytest plugin for axe-selenium-python Nov 12, 2018 N/A pytest (>=3.0.0) `pytest-azurepipelines `_ Formatting PyTest output for Azure Pipelines UI Jul 23, 2020 4 - Beta pytest (>=3.5.0) -`pytest-bandit `_ A bandit plugin for pytest Sep 25, 2019 4 - Beta pytest (>=3.5.0) +`pytest-bandit `_ A bandit plugin for pytest Feb 23, 2021 4 - Beta pytest (>=3.5.0) `pytest-base-url `_ pytest plugin for URL based testing Jun 19, 2020 5 - Production/Stable pytest (>=2.7.3) `pytest-bdd `_ BDD for pytest Dec 07, 2020 6 - Mature pytest (>=4.3) `pytest-bdd-splinter `_ Common steps for pytest bdd and splinter integration Aug 12, 2019 5 - Production/Stable pytest (>=4.0.0) @@ -105,7 +106,7 @@ name `pytest-change-report `_ turn . into √,turn F into x Sep 14, 2020 N/A pytest `pytest-chdir `_ A pytest fixture for changing current working directory Jan 28, 2020 N/A pytest (>=5.0.0,<6.0.0) `pytest-check `_ A pytest plugin that allows multiple failures per test. Dec 27, 2020 5 - Production/Stable N/A -`pytest-checkdocs `_ check the README when running tests Jan 01, 2021 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' +`pytest-checkdocs `_ check the README when running tests Feb 27, 2021 5 - Production/Stable pytest (>=4.6) ; extra == 'testing' `pytest-checkipdb `_ plugin to check if there are ipdb debugs left Jul 22, 2020 5 - Production/Stable pytest (>=2.9.2) `pytest-check-links `_ Check links in files Jul 29, 2020 N/A N/A `pytest-check-mk `_ pytest plugin to test Check_MK checks Nov 19, 2015 4 - Beta pytest @@ -152,6 +153,7 @@ name `pytest-custom-concurrency `_ Custom grouping concurrence for pytest Feb 08, 2021 N/A N/A `pytest-custom-exit-code `_ Exit pytest test session with custom exit code in different scenarios Aug 07, 2019 4 - Beta pytest (>=4.0.2) `pytest-custom-report `_ Configure the symbols displayed for test outcomes Jan 30, 2019 N/A pytest +`pytest-custom-scheduling `_ Custom grouping for pytest-xdist Feb 22, 2021 N/A N/A `pytest-cython `_ A plugin for testing Cython extension modules Jan 26, 2021 4 - Beta pytest (>=2.7.3) `pytest-darker `_ A pytest plugin for checking of modified code using Darker Aug 16, 2020 N/A pytest (>=6.0.1) ; extra == 'test' `pytest-dash `_ pytest fixtures to run dash applications. Mar 18, 2019 N/A N/A @@ -298,7 +300,7 @@ name `pytest-flakefinder `_ Runs tests multiple times to expose flakiness. Jul 28, 2020 4 - Beta pytest (>=2.7.1) `pytest-flakes `_ pytest plugin to check source code with pyflakes Nov 28, 2020 5 - Production/Stable N/A `pytest-flaptastic `_ Flaptastic py.test plugin Mar 17, 2019 N/A N/A -`pytest-flask `_ A set of py.test fixtures to test Flask applications. Nov 09, 2020 5 - Production/Stable pytest (>=5.2) +`pytest-flask `_ A set of py.test fixtures to test Flask applications. Feb 27, 2021 5 - Production/Stable pytest (>=5.2) `pytest-flask-sqlalchemy `_ A pytest plugin for preserving test isolation in Flask-SQlAlchemy using database transactions. Apr 04, 2019 4 - Beta pytest (>=3.2.1) `pytest-flask-sqlalchemy-transactions `_ Run tests in transactions using pytest, Flask, and SQLalchemy. Aug 02, 2018 4 - Beta pytest (>=3.2.1) `pytest-focus `_ A pytest plugin that alerts user of failed test cases with screen notifications May 04, 2019 4 - Beta pytest @@ -316,14 +318,14 @@ name `pytest-gevent `_ Ensure that gevent is properly patched when invoking pytest Feb 25, 2020 N/A pytest `pytest-gherkin `_ A flexible framework for executing BDD gherkin tests Jul 27, 2019 3 - Alpha pytest (>=5.0.0) `pytest-ghostinspector `_ For finding/executing Ghost Inspector tests May 17, 2016 3 - Alpha N/A -`pytest-girder `_ A set of pytest fixtures for testing Girder applications. Feb 11, 2021 N/A N/A +`pytest-girder `_ A set of pytest fixtures for testing Girder applications. Feb 25, 2021 N/A N/A `pytest-git `_ Git repository fixture for py.test May 28, 2019 5 - Production/Stable pytest `pytest-gitcov `_ Pytest plugin for reporting on coverage of the last git commit. Jan 11, 2020 2 - Pre-Alpha N/A `pytest-git-fixtures `_ Pytest fixtures for testing with git. Jan 25, 2021 4 - Beta pytest `pytest-github `_ Plugin for py.test that associates tests with github issues using a marker. Mar 07, 2019 5 - Production/Stable N/A `pytest-github-actions-annotate-failures `_ pytest plugin to annotate failed tests with a workflow command for GitHub Actions Oct 13, 2020 N/A pytest (>=4.0.0) `pytest-gitignore `_ py.test plugin to ignore the same files as git Jul 17, 2015 4 - Beta N/A -`pytest-gnupg-fixtures `_ Pytest fixtures for testing with gnupg. Jan 12, 2021 4 - Beta pytest +`pytest-gnupg-fixtures `_ Pytest fixtures for testing with gnupg. Feb 22, 2021 4 - Beta pytest `pytest-golden `_ Plugin for pytest that offloads expected outputs to data files Nov 23, 2020 N/A pytest (>=6.1.2,<7.0.0) `pytest-graphql-schema `_ Get graphql schema as fixture for pytest Oct 18, 2019 N/A N/A `pytest-greendots `_ Green progress dots Feb 08, 2014 3 - Alpha N/A @@ -356,7 +358,7 @@ name `pytest-httpx `_ Send responses to httpx. Nov 25, 2020 5 - Production/Stable pytest (==6.*) `pytest-hue `_ Visualise PyTest status via your Phillips Hue lights May 09, 2019 N/A N/A `pytest-hypo-25 `_ help hypo module for pytest Jan 12, 2020 3 - Alpha N/A -`pytest-ibutsu `_ A plugin to sent pytest results to an Ibutsu server Feb 11, 2021 4 - Beta pytest +`pytest-ibutsu `_ A plugin to sent pytest results to an Ibutsu server Feb 24, 2021 4 - Beta pytest `pytest-icdiff `_ use icdiff for better error messages in pytest assertions Apr 08, 2020 4 - Beta N/A `pytest-idapro `_ A pytest plugin for idapython. Allows a pytest setup to run tests outside and inside IDA in an automated manner by runnig pytest inside IDA and by mocking idapython api Nov 03, 2018 N/A N/A `pytest-ignore-flaky `_ ignore failures from flaky tests (pytest plugin) Jan 14, 2019 5 - Production/Stable pytest (>=3.7) @@ -369,7 +371,7 @@ name `pytest-inmanta `_ A py.test plugin providing fixtures to simplify inmanta modules testing. Oct 12, 2020 5 - Production/Stable N/A `pytest-inmanta-extensions `_ Inmanta tests package Jan 07, 2021 5 - Production/Stable N/A `pytest-Inomaly `_ A simple image diff plugin for pytest Feb 13, 2018 4 - Beta N/A -`pytest-insta `_ A practical snapshot testing plugin for pytest Nov 29, 2020 N/A pytest (>=6.0.2,<7.0.0) +`pytest-insta `_ A practical snapshot testing plugin for pytest Feb 25, 2021 N/A pytest (>=6.0.2,<7.0.0) `pytest-instafail `_ pytest plugin to show failures instantly Jun 14, 2020 4 - Beta pytest (>=2.9) `pytest-instrument `_ pytest plugin to instrument tests Apr 05, 2020 5 - Production/Stable pytest (>=5.1.0) `pytest-integration `_ Organizing pytests by integration or not Apr 16, 2020 N/A N/A @@ -417,7 +419,7 @@ name `pytest-localserver `_ py.test plugin to test server connections locally. Nov 14, 2018 4 - Beta N/A `pytest-localstack `_ Pytest plugin for AWS integration tests Aug 22, 2019 4 - Beta pytest (>=3.3.0) `pytest-lockable `_ lockable resource plugin for pytest Oct 05, 2020 3 - Alpha pytest -`pytest-locker `_ Used to lock object during testing. Essentially changing assertions from being hard coded to asserting that nothing changed Aug 11, 2020 N/A pytest (>=5.4) +`pytest-locker `_ Used to lock object during testing. Essentially changing assertions from being hard coded to asserting that nothing changed Feb 25, 2021 N/A pytest (>=5.4) `pytest-logbook `_ py.test plugin to capture logbook log messages Nov 23, 2015 5 - Production/Stable pytest (>=2.8) `pytest-logfest `_ Pytest plugin providing three logger fixtures with basic or full writing to log files Jul 21, 2019 4 - Beta pytest (>=3.5.0) `pytest-logger `_ Plugin configuring handlers for loggers from Python logging module. Jul 25, 2019 4 - Beta pytest (>=3.2) @@ -503,7 +505,7 @@ name `pytest-only `_ Use @pytest.mark.only to run a single test Jan 19, 2020 N/A N/A `pytest-oot `_ Run object-oriented tests in a simple format Sep 18, 2016 4 - Beta N/A `pytest-openfiles `_ Pytest plugin for detecting inadvertent open file handles Apr 16, 2020 3 - Alpha pytest (>=4.6) -`pytest-opentmi `_ pytest plugin for publish results to opentmi Jun 10, 2020 5 - Production/Stable pytest (>=5.0) +`pytest-opentmi `_ pytest plugin for publish results to opentmi Feb 26, 2021 5 - Production/Stable pytest (>=5.0) `pytest-operator `_ Fixtures for Operators Feb 20, 2021 N/A N/A `pytest-optional `_ include/exclude values of fixtures in pytest Oct 07, 2015 N/A N/A `pytest-optional-tests `_ Easy declaration of optional tests (i.e., that are not run by default) Jul 09, 2019 4 - Beta pytest (>=4.5.0) @@ -540,7 +542,7 @@ name `pytest-platform-markers `_ Markers for pytest to skip tests on specific platforms Sep 09, 2019 4 - Beta pytest (>=3.6.0) `pytest-play `_ pytest plugin that let you automate actions and assertions with test metrics reporting executing plain YAML files Jun 12, 2019 5 - Production/Stable N/A `pytest-playbook `_ Pytest plugin for reading playbooks. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) -`pytest-playwright `_ A pytest wrapper with fixtures for Playwright to automate web browsers Jan 21, 2021 N/A pytest +`pytest-playwright `_ A pytest wrapper with fixtures for Playwright to automate web browsers Feb 25, 2021 N/A pytest `pytest-plt `_ Fixtures for quickly making Matplotlib plots in tests Aug 17, 2020 5 - Production/Stable pytest `pytest-plugin-helpers `_ A plugin to help developing and testing other plugins Nov 23, 2019 4 - Beta pytest (>=3.5.0) `pytest-plus `_ PyTest Plus Plugin :: extends pytest functionality Mar 19, 2020 5 - Production/Stable pytest (>=3.50) @@ -555,7 +557,7 @@ name `pytest-pop `_ A pytest plugin to help with testing pop projects Aug 13, 2020 5 - Production/Stable pytest (>=5.4.0) `pytest-portion `_ Select a portion of the collected tests Jan 28, 2021 4 - Beta pytest (>=3.5.0) `pytest-postgres `_ Run PostgreSQL in Docker container in Pytest. Mar 22, 2020 N/A pytest -`pytest-postgresql `_ Postgresql fixtures and fixture factories for Pytest. Feb 11, 2021 5 - Production/Stable pytest (>=3.0.0) +`pytest-postgresql `_ Postgresql fixtures and fixture factories for Pytest. Feb 23, 2021 5 - Production/Stable pytest (>=3.0.0) `pytest-power `_ pytest plugin with powerful fixtures Dec 31, 2020 N/A pytest (>=5.4) `pytest-pride `_ Minitest-style test colors Apr 02, 2016 3 - Alpha N/A `pytest-print `_ pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) Oct 23, 2020 5 - Production/Stable pytest (>=3.0.0) @@ -575,7 +577,7 @@ name `pytest-pypom-navigation `_ Core engine for cookiecutter-qa and pytest-play packages Feb 18, 2019 4 - Beta pytest (>=3.0.7) `pytest-pyppeteer `_ A plugin to run pyppeteer in pytest. Feb 16, 2021 4 - Beta pytest (>=6.0.2) `pytest-pyq `_ Pytest fixture "q" for pyq Mar 10, 2020 5 - Production/Stable N/A -`pytest-pyramid `_ pytest pyramid providing basic fixtures for testing pyramid applications with pytest test suite Jun 05, 2020 4 - Beta pytest +`pytest-pyramid `_ pytest_pyramid - provides fixtures for testing pyramid applications with pytest test suite Feb 26, 2021 5 - Production/Stable pytest `pytest-pyramid-server `_ Pyramid server fixture for py.test May 28, 2019 5 - Production/Stable pytest `pytest-pytestrail `_ Pytest plugin for interaction with TestRail Aug 27, 2020 4 - Beta pytest (>=3.8.0) `pytest-pythonpath `_ pytest plugin for adding to the PYTHONPATH from command line or configs. Aug 22, 2018 5 - Production/Stable N/A @@ -649,16 +651,16 @@ name `pytest-salt-factories `_ Pytest Salt Plugin Feb 19, 2021 4 - Beta pytest (>=6.1.1) `pytest-salt-from-filenames `_ Simple PyTest Plugin For Salt's Test Suite Specifically Jan 29, 2019 4 - Beta pytest (>=4.1) `pytest-salt-runtests-bridge `_ Simple PyTest Plugin For Salt's Test Suite Specifically Dec 05, 2019 4 - Beta pytest (>=4.1) -`pytest-sanic `_ a pytest plugin for Sanic Sep 24, 2020 N/A pytest (>=5.2) +`pytest-sanic `_ a pytest plugin for Sanic Feb 27, 2021 N/A pytest (>=5.2) `pytest-sanity `_ Dec 07, 2020 N/A N/A `pytest-sa-pg `_ May 14, 2019 N/A N/A -`pytest-sbase `_ A complete web automation framework for end-to-end testing. Feb 19, 2021 5 - Production/Stable N/A +`pytest-sbase `_ A complete web automation framework for end-to-end testing. Feb 27, 2021 5 - Production/Stable N/A `pytest-scenario `_ pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A `pytest-schema `_ 👍 Validate return values against a schema-like object in testing Aug 31, 2020 5 - Production/Stable pytest (>=3.5.0) `pytest-securestore `_ An encrypted password store for use within pytest cases Jun 19, 2019 4 - Beta N/A `pytest-select `_ A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) `pytest-selenium `_ pytest plugin for Selenium Sep 19, 2020 5 - Production/Stable pytest (>=5.0.0) -`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Feb 19, 2021 5 - Production/Stable N/A +`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Feb 27, 2021 5 - Production/Stable N/A `pytest-selenium-enhancer `_ pytest plugin for Selenium Nov 26, 2020 5 - Production/Stable N/A `pytest-selenium-pdiff `_ A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A `pytest-send-email `_ Send pytest execution result email Dec 04, 2019 N/A N/A @@ -703,7 +705,7 @@ name `pytest-split `_ Pytest plugin for splitting test suite based on test execution time Apr 07, 2020 1 - Planning N/A `pytest-splitio `_ Split.io SDK integration for e2e tests Sep 22, 2020 N/A pytest (<7,>=5.0) `pytest-split-tests `_ A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Forked from Mark Adams' original project pytest-test-groups. May 28, 2019 N/A pytest (>=2.5) -`pytest-splunk-addon `_ A Dynamic test tool for Splunk Apps and Add-ons Feb 10, 2021 N/A pytest (>5.4.0,<6.1) +`pytest-splunk-addon `_ A Dynamic test tool for Splunk Apps and Add-ons Feb 26, 2021 N/A pytest (>5.4.0,<6.1) `pytest-splunk-addon-ui-smartx `_ Library to support testing Splunk Add-on UX Jan 18, 2021 N/A N/A `pytest-splunk-env `_ pytest fixtures for interaction with Splunk Enterprise and Splunk Cloud Oct 22, 2020 N/A pytest (>=6.1.1,<7.0.0) `pytest-sqitch `_ sqitch for pytest Apr 06, 2020 4 - Beta N/A @@ -778,7 +780,7 @@ name `pytest-tornado5 `_ A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Nov 16, 2018 5 - Production/Stable pytest (>=3.6) `pytest-tornado-yen3 `_ A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Oct 15, 2018 5 - Production/Stable N/A `pytest-tornasync `_ py.test plugin for testing Python 3.5+ Tornado code Jul 15, 2019 3 - Alpha pytest (>=3.0) -`pytest-track `_ Oct 23, 2020 3 - Alpha pytest (>=3.0) +`pytest-track `_ Feb 26, 2021 3 - Alpha pytest (>=3.0) `pytest-translations `_ Test your translation files. Oct 26, 2020 5 - Production/Stable N/A `pytest-travis-fold `_ Folds captured output sections in Travis CI build log Nov 29, 2017 4 - Beta pytest (>=2.6.0) `pytest-trello `_ Plugin for py.test that integrates trello using markers Nov 20, 2015 5 - Production/Stable N/A From 62ef87579647b7e97fba016f8300eb42d3fbe38d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Mar 2021 03:02:48 +0000 Subject: [PATCH 0469/2846] build(deps): bump anyio[curio,trio] in /testing/plugins_integration Bumps [anyio[curio,trio]](https://github.com/agronholm/anyio) from 2.1.0 to 2.2.0. - [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.1.0...2.2.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 730d1f028fe..712ac339ec1 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,4 +1,4 @@ -anyio[curio,trio]==2.1.0 +anyio[curio,trio]==2.2.0 django==3.1.7 pytest-asyncio==0.14.0 pytest-bdd==4.0.2 From 897b5a3bd6c4ede255770d9830ee119970bb1ea2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Mar 2021 03:02:50 +0000 Subject: [PATCH 0470/2846] build(deps): bump twisted in /testing/plugins_integration Bumps [twisted](https://github.com/twisted/twisted) from 20.3.0 to 21.2.0. - [Release notes](https://github.com/twisted/twisted/releases) - [Changelog](https://github.com/twisted/twisted/blob/twisted-21.2.0/NEWS.rst) - [Commits](https://github.com/twisted/twisted/compare/twisted-20.3.0...twisted-21.2.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 730d1f028fe..69011e651e5 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -11,5 +11,5 @@ pytest-rerunfailures==9.1.1 pytest-sugar==0.9.4 pytest-trio==0.7.0 pytest-twisted==1.13.2 -twisted==20.3.0 +twisted==21.2.0 pytest-xvfb==2.0.0 From decca74788d2f5d7b8fcac142579927346fc0a4b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Mar 2021 16:53:28 +0000 Subject: [PATCH 0471/2846] [pre-commit.ci] pre-commit autoupdate --- .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 fed7ca83cbb..b324b1f484e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,7 +43,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.7.1 + rev: v1.8.0 hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy From c14a9adba35ac675ce3e825d34d01d5bb51748c3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 4 Mar 2021 11:56:21 +0100 Subject: [PATCH 0472/2846] Fix skip signature (#8392) * Fix test_strict_and_skip The `--strict` argument was removed in #2552, but the removal wasn't actually correct - see #1472. * Fix argument handling in pytest.mark.skip See #8384 * Raise from None * Fix test name --- changelog/8384.bugfix.rst | 1 + doc/en/reference.rst | 2 +- src/_pytest/skipping.py | 13 +++++-------- testing/test_skipping.py | 18 +++++++++++++++++- 4 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 changelog/8384.bugfix.rst diff --git a/changelog/8384.bugfix.rst b/changelog/8384.bugfix.rst new file mode 100644 index 00000000000..3b70987490e --- /dev/null +++ b/changelog/8384.bugfix.rst @@ -0,0 +1 @@ +The ``@pytest.mark.skip`` decorator now correctly handles its arguments. When the ``reason`` argument is accidentally given both positional and as a keyword (e.g. because it was confused with ``skipif``), a ``TypeError`` now occurs. Before, such tests were silently skipped, and the positional argument ignored. Additionally, ``reason`` is now documented correctly as positional or keyword (rather than keyword-only). diff --git a/doc/en/reference.rst b/doc/en/reference.rst index bc6c5670a5c..9ad82b3e4b9 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -150,7 +150,7 @@ pytest.mark.skip Unconditionally skip a test function. -.. py:function:: pytest.mark.skip(*, reason=None) +.. py:function:: pytest.mark.skip(reason=None) :keyword str reason: Reason why the test function is being skipped. diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 1ad312919ca..7fe9783a4fa 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -161,7 +161,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, class Skip: """The result of evaluate_skip_marks().""" - reason = attr.ib(type=str) + reason = attr.ib(type=str, default="unconditional skip") def evaluate_skip_marks(item: Item) -> Optional[Skip]: @@ -184,13 +184,10 @@ def evaluate_skip_marks(item: Item) -> Optional[Skip]: 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) + try: + return Skip(*mark.args, **mark.kwargs) + except TypeError as e: + raise TypeError(str(e) + " - maybe you meant pytest.mark.skipif?") from None return None diff --git a/testing/test_skipping.py b/testing/test_skipping.py index fc66eb18e64..349de6e080f 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -861,9 +861,25 @@ def test_hello(): pass """ ) - result = pytester.runpytest("-rs") + result = pytester.runpytest("-rs", "--strict-markers") result.stdout.fnmatch_lines(["*unconditional skip*", "*1 skipped*"]) + def test_wrong_skip_usage(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + @pytest.mark.skip(False, reason="I thought this was skipif") + def test_hello(): + pass + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*TypeError: __init__() got multiple values for argument 'reason' - maybe you meant pytest.mark.skipif?" + ] + ) + class TestSkipif: def test_skipif_conditional(self, pytester: Pytester) -> None: From 19a2f7425ddec3b614da7c915e0cf8bb24b6906f Mon Sep 17 00:00:00 2001 From: Alexandros Tzannes Date: Thu, 4 Mar 2021 15:45:57 -0500 Subject: [PATCH 0473/2846] Merge pull request #8399 from atzannes/master closes #8394 Generated fixture names for unittest/xunit/nose should start with underscore --- changelog/8394.bugfix.rst | 1 + src/_pytest/python.py | 8 +++---- src/_pytest/unittest.py | 2 +- testing/test_nose.py | 44 +++++++++++++++++++++++++++++++++++++++ testing/test_unittest.py | 24 +++++++++++++++++++++ 5 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 changelog/8394.bugfix.rst diff --git a/changelog/8394.bugfix.rst b/changelog/8394.bugfix.rst new file mode 100644 index 00000000000..a0fb5bb71fd --- /dev/null +++ b/changelog/8394.bugfix.rst @@ -0,0 +1 @@ +Use private names for internal fixtures that handle classic setup/teardown so that they don't show up with the default ``--fixtures`` invocation (but they still show up with ``--fixtures -v``). diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 40116ab9c5a..c19d2ed4fb4 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -528,7 +528,7 @@ def _inject_setup_module_fixture(self) -> None: autouse=True, scope="module", # Use a unique name to speed up lookup. - name=f"xunit_setup_module_fixture_{self.obj.__name__}", + 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: @@ -557,7 +557,7 @@ def _inject_setup_function_fixture(self) -> None: autouse=True, scope="function", # Use a unique name to speed up lookup. - name=f"xunit_setup_function_fixture_{self.obj.__name__}", + 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: @@ -809,7 +809,7 @@ def _inject_setup_class_fixture(self) -> None: autouse=True, scope="class", # Use a unique name to speed up lookup. - name=f"xunit_setup_class_fixture_{self.obj.__qualname__}", + 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: @@ -838,7 +838,7 @@ def _inject_setup_method_fixture(self) -> None: autouse=True, scope="function", # Use a unique name to speed up lookup. - name=f"xunit_setup_method_fixture_{self.obj.__qualname__}", + name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}", ) def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]: method = request.function diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 719eb4e8823..3f88d7a9e2c 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -144,7 +144,7 @@ def cleanup(*args): scope=scope, autouse=True, # Use a unique name to speed up lookup. - name=f"unittest_{setup_name}_fixture_{obj.__qualname__}", + name=f"_unittest_{setup_name}_fixture_{obj.__qualname__}", ) def fixture(self, request: FixtureRequest) -> Generator[None, None, None]: if _is_skipped(self): diff --git a/testing/test_nose.py b/testing/test_nose.py index 13429afafd4..77f79b53b3c 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -211,6 +211,50 @@ def test_world(): result.stdout.fnmatch_lines(["*2 passed*"]) +def test_fixtures_nose_setup_issue8394(pytester: Pytester) -> None: + pytester.makepyfile( + """ + def setup_module(): + pass + + def teardown_module(): + pass + + def setup_function(func): + pass + + def teardown_function(func): + pass + + def test_world(): + pass + + class Test(object): + def setup_class(cls): + pass + + def teardown_class(cls): + pass + + def setup_method(self, meth): + pass + + def teardown_method(self, meth): + pass + + def test_method(self): pass + """ + ) + match = "*no docstring available*" + result = pytester.runpytest("--fixtures") + assert result.ret == 0 + result.stdout.no_fnmatch_line(match) + + result = pytester.runpytest("--fixtures", "-v") + assert result.ret == 0 + result.stdout.fnmatch_lines([match, match, match, match]) + + def test_nose_setup_ordering(pytester: Pytester) -> None: pytester.makepyfile( """ diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 69bafc26d61..d7f7737153d 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -302,6 +302,30 @@ def test_teareddown(): reprec.assertoutcome(passed=3) +def test_fixtures_setup_setUpClass_issue8394(pytester: Pytester) -> None: + pytester.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + pass + def test_func1(self): + pass + @classmethod + def tearDownClass(cls): + pass + """ + ) + result = pytester.runpytest("--fixtures") + assert result.ret == 0 + result.stdout.no_fnmatch_line("*no docstring available*") + + result = pytester.runpytest("--fixtures", "-v") + assert result.ret == 0 + result.stdout.fnmatch_lines(["*no docstring available*"]) + + def test_setup_class(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ From 7c792e96c68c0308ca7d6b913dd9fc82cf727012 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 5 Mar 2021 22:22:53 -0300 Subject: [PATCH 0474/2846] Add type annotations to the description instead of signature This configures Sphinx autodoc to include the type annotations along with the description of the function/method, instead of including it into the signature. Fix #8405 --- doc/en/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/en/conf.py b/doc/en/conf.py index e34ae6856f0..d9c1f3c2d3f 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -35,6 +35,7 @@ # sys.path.insert(0, os.path.abspath('.')) autodoc_member_order = "bysource" +autodoc_typehints = "description" todo_include_todos = 1 # -- General configuration ----------------------------------------------------- From 22dad53a248f50f50b5e000d63a8d3c798868d98 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 17 Jan 2021 21:20:29 +0100 Subject: [PATCH 0475/2846] implement Node.path as pathlib.Path * reorganize lastfailed node sort Co-authored-by: Bruno Oliveira --- changelog/8251.deprecation.rst | 1 + changelog/8251.feature.rst | 1 + doc/en/deprecations.rst | 10 +++ src/_pytest/cacheprovider.py | 13 ++-- src/_pytest/compat.py | 12 ++++ src/_pytest/deprecated.py | 8 +++ src/_pytest/doctest.py | 21 ++++--- src/_pytest/fixtures.py | 30 ++++++--- src/_pytest/main.py | 11 +++- src/_pytest/nodes.py | 85 ++++++++++++++++++++------ src/_pytest/pytester.py | 5 +- src/_pytest/python.py | 22 ++++--- testing/plugins_integration/pytest.ini | 1 + testing/python/collect.py | 6 +- testing/python/fixtures.py | 4 +- testing/test_collection.py | 32 +++++----- testing/test_mark.py | 3 +- testing/test_runner.py | 4 +- testing/test_terminal.py | 2 +- 19 files changed, 194 insertions(+), 77 deletions(-) create mode 100644 changelog/8251.deprecation.rst create mode 100644 changelog/8251.feature.rst diff --git a/changelog/8251.deprecation.rst b/changelog/8251.deprecation.rst new file mode 100644 index 00000000000..1d988bfc83b --- /dev/null +++ b/changelog/8251.deprecation.rst @@ -0,0 +1 @@ +Deprecate ``Node.fspath`` as we plan to move off `py.path.local `__ and switch to :mod:``pathlib``. diff --git a/changelog/8251.feature.rst b/changelog/8251.feature.rst new file mode 100644 index 00000000000..49aede797a0 --- /dev/null +++ b/changelog/8251.feature.rst @@ -0,0 +1 @@ +Implement ``Node.path`` as a ``pathlib.Path``. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index a3d7fd49a33..6ecb37b385a 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -19,6 +19,16 @@ Below is a complete list of all pytest features which are considered deprecated. :class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. +``Node.fspath`` in favor of ``pathlib`` and ``Node.path`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.3 + +As pytest tries to move off `py.path.local `__ we ported most of the node internals to :mod:`pathlib`. + +Pytest will provide compatibility for quite a while. + + Backward compatibilities in ``Parser.addoption`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 585cebf6c9d..03e20bea18c 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -218,14 +218,17 @@ def pytest_make_collect_report(self, collector: nodes.Collector): # Sort any lf-paths to the beginning. lf_paths = self.lfplugin._last_failed_paths + res.result = sorted( res.result, - key=lambda x: 0 if Path(str(x.fspath)) in lf_paths else 1, + # use stable sort to priorize last failed + key=lambda x: x.path in lf_paths, + reverse=True, ) return elif isinstance(collector, Module): - if Path(str(collector.fspath)) in self.lfplugin._last_failed_paths: + if collector.path in self.lfplugin._last_failed_paths: out = yield res = out.get_result() result = res.result @@ -246,7 +249,7 @@ def pytest_make_collect_report(self, collector: nodes.Collector): for x in result if x.nodeid in lastfailed # Include any passed arguments (not trivial to filter). - or session.isinitpath(x.fspath) + or session.isinitpath(x.path) # Keep all sub-collectors. or isinstance(x, nodes.Collector) ] @@ -266,7 +269,7 @@ def pytest_make_collect_report( # 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: + if collector.path not in self.lfplugin._last_failed_paths: self.lfplugin._skipped_files += 1 return CollectReport( @@ -415,7 +418,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.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return] def pytest_sessionfinish(self) -> None: config = self.config diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index b354fcb3f63..b9cbf85e04f 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -2,6 +2,7 @@ import enum import functools import inspect +import os import re import sys from contextlib import contextmanager @@ -18,6 +19,7 @@ from typing import Union import attr +import py from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -30,6 +32,16 @@ _T = TypeVar("_T") _S = TypeVar("_S") +#: constant to prepare valuing py.path.local replacements/lazy proxies later on +# intended for removal in pytest 8.0 or 9.0 + +LEGACY_PATH = py.path.local + + +def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH: + """Internal wrapper to prepare lazy proxies for py.path.local instances""" + return py.path.local(path) + # fmt: off # Singleton type for NOTSET, as described in: diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 5efc004ac94..c203eadc1ad 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -89,6 +89,12 @@ ) +NODE_FSPATH = UnformattedWarning( + PytestDeprecationWarning, + "{type}.fspath is deprecated and will be replaced by {type}.path.\n" + "see TODO;URL for details on replacing py.path.local with pathlib.Path", +) + # You want to make some `__init__` or function "private". # # def my_private_function(some, args): @@ -106,6 +112,8 @@ # # 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 255ca80b913..4942a8f793b 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -30,6 +30,7 @@ from _pytest._code.code import ReprFileLocation from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter +from _pytest.compat import legacy_path from _pytest.compat import safe_getattr from _pytest.config import Config from _pytest.config.argparsing import Parser @@ -128,10 +129,10 @@ def pytest_collect_file( config = parent.config if fspath.suffix == ".py": if config.option.doctestmodules and not _is_setup_py(fspath): - mod: DoctestModule = DoctestModule.from_parent(parent, fspath=path) + mod: DoctestModule = DoctestModule.from_parent(parent, path=fspath) return mod elif _is_doctest(config, fspath, parent): - txt: DoctestTextfile = DoctestTextfile.from_parent(parent, fspath=path) + txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=fspath) return txt return None @@ -378,7 +379,7 @@ def repr_failure( # type: ignore[override] def reportinfo(self): assert self.dtest is not None - return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name + return legacy_path(self.path), self.dtest.lineno, "[doctest] %s" % self.name def _get_flag_lookup() -> Dict[str, int]: @@ -425,9 +426,9 @@ def collect(self) -> Iterable[DoctestItem]: # 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) - name = self.fspath.basename + text = self.path.read_text(encoding) + filename = str(self.path) + name = self.path.name globs = {"__name__": "__main__"} optionflags = get_optionflags(self) @@ -534,16 +535,16 @@ def _find( self, tests, obj, name, module, source_lines, globs, seen ) - if self.fspath.basename == "conftest.py": + if self.path.name == "conftest.py": module = self.config.pluginmanager._importconftest( - Path(self.fspath), self.config.getoption("importmode") + self.path, self.config.getoption("importmode") ) else: try: - module = import_path(self.fspath) + module = import_path(self.path) except ImportError: if self.config.getvalue("doctest_ignore_import_errors"): - pytest.skip("unable to import module %r" % self.fspath) + pytest.skip("unable to import module %r" % self.path) else: raise # Uses internal doctest module parsing mechanism. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0521d736118..722400ff7aa 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -28,7 +28,6 @@ from typing import Union import attr -import py import _pytest from _pytest import nodes @@ -46,6 +45,8 @@ from _pytest.compat import getimfunc from _pytest.compat import getlocation from _pytest.compat import is_generator +from _pytest.compat import LEGACY_PATH +from _pytest.compat import legacy_path from _pytest.compat import NOTSET from _pytest.compat import safe_getattr from _pytest.config import _PluggyPlugin @@ -53,6 +54,7 @@ from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.deprecated import FILLFUNCARGS +from _pytest.deprecated import NODE_FSPATH from _pytest.deprecated import YIELD_FIXTURE from _pytest.mark import Mark from _pytest.mark import ParameterSet @@ -256,12 +258,12 @@ def get_parametrized_fixture_keys(item: nodes.Item, scopenum: int) -> Iterator[_ if scopenum == 0: # session key: _Key = (argname, param_index) elif scopenum == 1: # package - key = (argname, param_index, item.fspath.dirpath()) + key = (argname, param_index, item.path.parent) elif scopenum == 2: # module - key = (argname, param_index, item.fspath) + key = (argname, param_index, item.path) elif scopenum == 3: # class item_cls = item.cls # type: ignore[attr-defined] - key = (argname, param_index, item.fspath, item_cls) + key = (argname, param_index, item.path, item_cls) yield key @@ -519,12 +521,17 @@ def module(self): 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.""" + def fspath(self) -> LEGACY_PATH: + """(deprecated) The file system path of the test module which collected this test.""" + warnings.warn(NODE_FSPATH.format(type=type(self).__name__), stacklevel=2) + return legacy_path(self.path) + + @property + def path(self) -> Path: if self.scope not in ("function", "class", "module", "package"): 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 + return self._pyfuncitem.path # type: ignore @property def keywords(self) -> MutableMapping[str, Any]: @@ -1040,7 +1047,7 @@ def finish(self, request: SubRequest) -> None: if exc: raise exc finally: - hook = self._fixturemanager.session.gethookproxy(request.node.fspath) + hook = self._fixturemanager.session.gethookproxy(request.node.path) 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 @@ -1075,7 +1082,7 @@ def execute(self, request: SubRequest) -> _FixtureValue: self.finish(request) assert self.cached_result is None - hook = self._fixturemanager.session.gethookproxy(request.node.fspath) + hook = self._fixturemanager.session.gethookproxy(request.node.path) result = hook.pytest_fixture_setup(fixturedef=self, request=request) return result @@ -1623,6 +1630,11 @@ def parsefactories( self._holderobjseen.add(holderobj) autousenames = [] for name in dir(holderobj): + # ugly workaround for one of the fspath deprecated property of node + # todo: safely generalize + if isinstance(holderobj, nodes.Node) and name == "fspath": + continue + # The attribute can be an arbitrary descriptor, so the attribute # access below can raise. safe_getatt() ignores such exceptions. obj = safe_getattr(holderobj, name, None) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 5036601f9bb..3dc00fa691e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -464,7 +464,12 @@ class Session(nodes.FSCollector): def __init__(self, config: Config) -> None: super().__init__( - config.rootdir, parent=None, config=config, session=self, nodeid="" + path=config.rootpath, + fspath=config.rootdir, + parent=None, + config=config, + session=self, + nodeid="", ) self.testsfailed = 0 self.testscollected = 0 @@ -688,7 +693,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: if col: if isinstance(col[0], Package): pkg_roots[str(parent)] = col[0] - node_cache1[Path(col[0].fspath)] = [col[0]] + node_cache1[col[0].path] = [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. @@ -717,7 +722,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: continue for x in self._collectfile(path): - key2 = (type(x), Path(x.fspath)) + key2 = (type(x), x.path) if key2 in node_cache2: yield node_cache2[key2] else: diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 2a96d55ad05..47752d34c61 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -23,9 +23,12 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest.compat import cached_property +from _pytest.compat import LEGACY_PATH +from _pytest.compat import legacy_path from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH +from _pytest.deprecated import NODE_FSPATH from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords @@ -79,6 +82,26 @@ def iterparentnodeids(nodeid: str) -> Iterator[str]: pos = at + len(sep) +def _imply_path( + path: Optional[Path], fspath: Optional[LEGACY_PATH] +) -> Tuple[Path, LEGACY_PATH]: + if path is not None: + if fspath is not None: + if Path(fspath) != path: + raise ValueError( + f"Path({fspath!r}) != {path!r}\n" + "if both path and fspath are given they need to be equal" + ) + assert Path(fspath) == path, f"{fspath} != {path}" + else: + fspath = legacy_path(path) + return path, fspath + + else: + assert fspath is not None + return Path(fspath), fspath + + _NodeType = TypeVar("_NodeType", bound="Node") @@ -110,7 +133,7 @@ class Node(metaclass=NodeMeta): "parent", "config", "session", - "fspath", + "path", "_nodeid", "_store", "__dict__", @@ -123,6 +146,7 @@ def __init__( config: Optional[Config] = None, session: "Optional[Session]" = None, fspath: Optional[py.path.local] = None, + path: Optional[Path] = None, nodeid: Optional[str] = None, ) -> None: #: A unique name within the scope of the parent node. @@ -148,7 +172,7 @@ def __init__( self.session = parent.session #: Filesystem path where this node was collected from (can be None). - self.fspath = fspath or getattr(parent, "fspath", None) + self.path = _imply_path(path or getattr(parent, "path", None), fspath=fspath)[0] # The explicit annotation is to avoid publicly exposing NodeKeywords. #: Keywords/markers collected from all scopes. @@ -174,6 +198,17 @@ def __init__( # own use. Currently only intended for internal plugins. self._store = Store() + @property + def fspath(self): + """(deprecated) returns a py.path.local copy of self.path""" + warnings.warn(NODE_FSPATH.format(type=type(self).__name__), stacklevel=2) + return py.path.local(self.path) + + @fspath.setter + def fspath(self, value: py.path.local): + warnings.warn(NODE_FSPATH.format(type=type(self).__name__), stacklevel=2) + self.path = Path(value) + @classmethod def from_parent(cls, parent: "Node", **kw): """Public constructor for Nodes. @@ -195,7 +230,7 @@ def from_parent(cls, parent: "Node", **kw): @property def ihook(self): """fspath-sensitive hook proxy used to call pytest hooks.""" - return self.session.gethookproxy(self.fspath) + return self.session.gethookproxy(self.path) def __repr__(self) -> str: return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) @@ -476,9 +511,9 @@ def repr_failure( # type: ignore[override] return self._repr_failure_py(excinfo, style=tbstyle) def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: - if hasattr(self, "fspath"): + if hasattr(self, "path"): traceback = excinfo.traceback - ntraceback = traceback.cut(path=Path(self.fspath)) + ntraceback = traceback.cut(path=self.path) if ntraceback == traceback: ntraceback = ntraceback.cut(excludepath=tracebackcutdir) excinfo.traceback = ntraceback.filter() @@ -497,36 +532,52 @@ def _check_initialpaths_for_relpath( class FSCollector(Collector): def __init__( self, - fspath: py.path.local, + fspath: Optional[py.path.local], + path: Optional[Path], parent=None, config: Optional[Config] = None, session: Optional["Session"] = None, nodeid: Optional[str] = None, ) -> None: + path, fspath = _imply_path(path, fspath=fspath) name = fspath.basename - if parent is not None: - rel = fspath.relto(parent.fspath) - if rel: - name = rel + if parent is not None and parent.path != path: + try: + rel = path.relative_to(parent.path) + except ValueError: + pass + else: + name = str(rel) name = name.replace(os.sep, SEP) - self.fspath = fspath + self.path = Path(fspath) session = session or parent.session if nodeid is None: - nodeid = self.fspath.relto(session.config.rootdir) - - if not nodeid: + try: + nodeid = str(self.path.relative_to(session.config.rootpath)) + except ValueError: nodeid = _check_initialpaths_for_relpath(session, fspath) + if nodeid and os.sep != SEP: nodeid = nodeid.replace(os.sep, SEP) - super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath) + super().__init__( + name, parent, config, session, nodeid=nodeid, fspath=fspath, path=path + ) @classmethod - def from_parent(cls, parent, *, fspath, **kw): + def from_parent( + cls, + parent, + *, + fspath: Optional[py.path.local] = None, + path: Optional[Path] = None, + **kw, + ): """The public constructor.""" - return super().from_parent(parent=parent, fspath=fspath, **kw) + path, fspath = _imply_path(path, fspath=fspath) + return super().from_parent(parent=parent, fspath=fspath, path=path, **kw) def gethookproxy(self, fspath: "os.PathLike[str]"): warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 853dfbe9489..f2a6d2aab92 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -61,6 +61,7 @@ from _pytest.outcomes import fail from _pytest.outcomes import importorskip from _pytest.outcomes import skip +from _pytest.pathlib import bestrelpath from _pytest.pathlib import make_numbered_dir from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -976,10 +977,10 @@ def getpathnode(self, path: Union[str, "os.PathLike[str]"]): :param py.path.local path: Path to the file. """ - path = py.path.local(path) + path = Path(path) config = self.parseconfigure(path) session = Session.from_config(config) - x = session.fspath.bestrelpath(path) + x = bestrelpath(session.path, path) config.hook.pytest_sessionstart(session=session) res = session.perform_collect([x], genitems=False)[0] config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c19d2ed4fb4..7d518dbbf4b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -577,7 +577,7 @@ def _importtestmodule(self): # We assume we are only called once per module. importmode = self.config.getoption("--import-mode") try: - mod = import_path(self.fspath, mode=importmode) + mod = import_path(self.path, mode=importmode) except SyntaxError as e: raise self.CollectError( ExceptionInfo.from_current().getrepr(style="short") @@ -603,10 +603,10 @@ def _importtestmodule(self): ) formatted_tb = str(exc_repr) raise self.CollectError( - "ImportError while importing test module '{fspath}'.\n" + "ImportError while importing test module '{path}'.\n" "Hint: make sure your test modules/packages have valid Python names.\n" "Traceback:\n" - "{traceback}".format(fspath=self.fspath, traceback=formatted_tb) + "{traceback}".format(path=self.path, traceback=formatted_tb) ) from e except skip.Exception as e: if e.allow_module_level: @@ -624,18 +624,26 @@ def _importtestmodule(self): class Package(Module): def __init__( self, - fspath: py.path.local, + fspath: Optional[py.path.local], parent: nodes.Collector, # NOTE: following args are unused: config=None, session=None, nodeid=None, + path=Optional[Path], ) -> None: # NOTE: Could be just the following, but kept as-is for compat. # nodes.FSCollector.__init__(self, fspath, parent=parent) + path, fspath = nodes._imply_path(path, fspath=fspath) session = parent.session nodes.FSCollector.__init__( - self, fspath, parent=parent, config=config, session=session, nodeid=nodeid + self, + fspath=fspath, + path=path, + parent=parent, + config=config, + session=session, + nodeid=nodeid, ) self.name = os.path.basename(str(fspath.dirname)) @@ -704,12 +712,12 @@ def _collectfile( return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return] def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: - this_path = Path(self.fspath).parent + this_path = self.path.parent init_module = this_path / "__init__.py" if init_module.is_file() and path_matches_patterns( init_module, self.config.getini("python_files") ): - yield Module.from_parent(self, fspath=py.path.local(init_module)) + yield Module.from_parent(self, path=init_module) pkg_prefixes: Set[Path] = set() for direntry in visit(str(this_path), recurse=self._recurse): path = Path(direntry.path) diff --git a/testing/plugins_integration/pytest.ini b/testing/plugins_integration/pytest.ini index f6c77b0dee5..b42b07d145a 100644 --- a/testing/plugins_integration/pytest.ini +++ b/testing/plugins_integration/pytest.ini @@ -2,3 +2,4 @@ addopts = --strict-markers filterwarnings = error::pytest.PytestWarning + ignore:.*.fspath is deprecated and will be replaced by .*.path.*:pytest.PytestDeprecationWarning diff --git a/testing/python/collect.py b/testing/python/collect.py index 4256851e254..bb4c937c01e 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1125,7 +1125,8 @@ def pytest_pycollect_makeitem(collector, name, obj): def test_func_reportinfo(self, pytester: Pytester) -> None: item = pytester.getitem("def test_func(): pass") fspath, lineno, modpath = item.reportinfo() - assert fspath == item.fspath + with pytest.warns(DeprecationWarning): + assert fspath == item.fspath assert lineno == 0 assert modpath == "test_func" @@ -1140,7 +1141,8 @@ def test_hello(self): pass classcol = pytester.collect_by_name(modcol, "TestClass") assert isinstance(classcol, Class) fspath, lineno, msg = classcol.reportinfo() - assert fspath == modcol.fspath + with pytest.warns(DeprecationWarning): + assert fspath == modcol.fspath assert lineno == 1 assert msg == "TestClass" diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 3d5099c5399..e62143e5c18 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -966,7 +966,9 @@ def test_request_getmodulepath(self, pytester: Pytester) -> None: modcol = pytester.getmodulecol("def test_somefunc(): pass") (item,) = pytester.genitems([modcol]) req = fixtures.FixtureRequest(item, _ispytest=True) - assert req.fspath == modcol.fspath + assert req.path == modcol.path + with pytest.warns(pytest.PytestDeprecationWarning): + assert req.fspath == modcol.fspath def test_request_fixturenames(self, pytester: Pytester) -> None: pytester.makepyfile( diff --git a/testing/test_collection.py b/testing/test_collection.py index 055d27476a6..248071111f3 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -464,13 +464,13 @@ def test_collect_topdir(self, pytester: Pytester) -> None: config = pytester.parseconfig(id) topdir = pytester.path rcol = Session.from_config(config) - assert topdir == rcol.fspath + assert topdir == rcol.path # rootid = rcol.nodeid # root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0] # assert root2 == rcol, rootid colitems = rcol.perform_collect([rcol.nodeid], genitems=False) assert len(colitems) == 1 - assert colitems[0].fspath == p + assert colitems[0].path == p def get_reported_items(self, hookrec: HookRecorder) -> List[Item]: """Return pytest.Item instances reported by the pytest_collectreport hook""" @@ -494,10 +494,10 @@ def test_collect_protocol_single_function(self, pytester: Pytester) -> None: topdir = pytester.path # noqa hookrec.assert_contains( [ - ("pytest_collectstart", "collector.fspath == topdir"), - ("pytest_make_collect_report", "collector.fspath == topdir"), - ("pytest_collectstart", "collector.fspath == p"), - ("pytest_make_collect_report", "collector.fspath == p"), + ("pytest_collectstart", "collector.path == topdir"), + ("pytest_make_collect_report", "collector.path == topdir"), + ("pytest_collectstart", "collector.path == p"), + ("pytest_make_collect_report", "collector.path == p"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.result[0].name == 'test_func'"), ] @@ -547,7 +547,7 @@ def pytest_collect_file(path, parent): assert len(items) == 2 hookrec.assert_contains( [ - ("pytest_collectstart", "collector.fspath == collector.session.fspath"), + ("pytest_collectstart", "collector.path == collector.session.path"), ( "pytest_collectstart", "collector.__class__.__name__ == 'SpecialFile'", @@ -570,7 +570,7 @@ def test_collect_subdir_event_ordering(self, pytester: Pytester) -> None: pprint.pprint(hookrec.calls) hookrec.assert_contains( [ - ("pytest_collectstart", "collector.fspath == test_aaa"), + ("pytest_collectstart", "collector.path == test_aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.nodeid.startswith('aaa/test_aaa.py')"), ] @@ -592,10 +592,10 @@ def test_collect_two_commandline_args(self, pytester: Pytester) -> None: pprint.pprint(hookrec.calls) hookrec.assert_contains( [ - ("pytest_collectstart", "collector.fspath == test_aaa"), + ("pytest_collectstart", "collector.path == test_aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.nodeid == 'aaa/test_aaa.py'"), - ("pytest_collectstart", "collector.fspath == test_bbb"), + ("pytest_collectstart", "collector.path == test_bbb"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.nodeid == 'bbb/test_bbb.py'"), ] @@ -609,7 +609,9 @@ def test_serialization_byid(self, pytester: Pytester) -> None: items2, hookrec = pytester.inline_genitems(item.nodeid) (item2,) = items2 assert item2.name == item.name - assert item2.fspath == item.fspath + with pytest.warns(DeprecationWarning): + assert item2.fspath == item.fspath + assert item2.path == item.path def test_find_byid_without_instance_parents(self, pytester: Pytester) -> None: p = pytester.makepyfile( @@ -1347,14 +1349,10 @@ def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) -> """ class MyCollector(pytest.File): - def __init__(self, fspath, parent, x): - super().__init__(fspath, parent) + def __init__(self, *k, x, **kw): + super().__init__(*k, **kw) 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=py.path.local(pytester.path) / "foo", x=10 ) diff --git a/testing/test_mark.py b/testing/test_mark.py index 420faf91ec9..77991f9e273 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1048,11 +1048,12 @@ class TestBarClass(BaseTests): # assert skipped_k == failed_k == 0 -def test_addmarker_order() -> None: +def test_addmarker_order(pytester) -> None: session = mock.Mock() session.own_markers = [] session.parent = None session.nodeid = "" + session.path = pytester.path node = Node.from_parent(session, name="Test") node.add_marker("foo") node.add_marker("bar") diff --git a/testing/test_runner.py b/testing/test_runner.py index abb87c6d3d4..a34cd98f964 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -447,9 +447,9 @@ class TestClass(object): assert not rep.skipped assert rep.passed locinfo = rep.location - assert locinfo[0] == col.fspath.basename + assert locinfo[0] == col.path.name assert not locinfo[1] - assert locinfo[2] == col.fspath.basename + assert locinfo[2] == col.path.name res = rep.result assert len(res) == 2 assert res[0].name == "test_func1" diff --git a/testing/test_terminal.py b/testing/test_terminal.py index e536f70989c..53bced8e68e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -133,7 +133,7 @@ def test_show_runtest_logstart(self, pytester: Pytester, linecomp) -> None: item.config.pluginmanager.register(tr) location = item.reportinfo() tr.config.hook.pytest_runtest_logstart( - nodeid=item.nodeid, location=location, fspath=str(item.fspath) + nodeid=item.nodeid, location=location, fspath=str(item.path) ) linecomp.assert_contains_lines(["*test_show_runtest_logstart.py*"]) From 77cb110258c09d29eff90078722115ca8a0a1619 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 22 Feb 2021 09:43:52 +0100 Subject: [PATCH 0476/2846] drop usage of py.path.local calls Co-authored-by: Bruno Oliveira --- .pre-commit-config.yaml | 6 ++++ src/_pytest/_code/code.py | 3 +- src/_pytest/cacheprovider.py | 7 ++-- src/_pytest/compat.py | 11 +++--- src/_pytest/config/__init__.py | 25 ++++++------- src/_pytest/deprecated.py | 2 +- src/_pytest/doctest.py | 5 ++- src/_pytest/hookspec.py | 22 ++++++------ src/_pytest/main.py | 6 ++-- src/_pytest/nodes.py | 28 +++++++-------- src/_pytest/pytester.py | 65 +++++++++++++++++----------------- src/_pytest/python.py | 20 +++++------ src/_pytest/reports.py | 4 +-- src/_pytest/tmpdir.py | 23 ++++++------ testing/test_collection.py | 10 +++--- testing/test_main.py | 2 +- testing/test_nodes.py | 9 +++-- testing/test_parseopt.py | 9 +++-- testing/test_pathlib.py | 7 ++-- testing/test_reports.py | 5 ++- 20 files changed, 138 insertions(+), 131 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b324b1f484e..24196151952 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -87,3 +87,9 @@ repos: xml\. ) types: [python] + - id: py-path-deprecated + name: py.path usage is deprecated + language: pygrep + entry: \bpy\.path\.local + exclude: docs + types: [python] diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index b8521756067..331aaabc780 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -31,7 +31,6 @@ import attr import pluggy -import py import _pytest from _pytest._code.source import findsource @@ -1230,7 +1229,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, Path], int]: if _PLUGGY_DIR.name == "__init__.py": _PLUGGY_DIR = _PLUGGY_DIR.parent _PYTEST_DIR = Path(_pytest.__file__).parent -_PY_DIR = Path(py.__file__).parent +_PY_DIR = Path(__import__("py").__file__).parent def filter_traceback(entry: TracebackEntry) -> bool: diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 03e20bea18c..a7ec7989184 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -13,7 +13,6 @@ from typing import Union import attr -import py from .pathlib import resolve_from_str from .pathlib import rm_rf @@ -21,6 +20,8 @@ from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.compat import final +from _pytest.compat import LEGACY_PATH +from _pytest.compat import legacy_path from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl @@ -120,7 +121,7 @@ def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None: stacklevel=3, ) - def makedir(self, name: str) -> py.path.local: + def makedir(self, name: str) -> LEGACY_PATH: """Return a directory path object with the given name. If the directory does not yet exist, it will be created. You can use @@ -137,7 +138,7 @@ def makedir(self, name: str) -> py.path.local: raise ValueError("name is not allowed to contain path separators") res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path) res.mkdir(exist_ok=True, parents=True) - return py.path.local(res) + return legacy_path(res) def _getvaluepath(self, key: str) -> Path: return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key)) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index b9cbf85e04f..4236618e8b4 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -32,15 +32,18 @@ _T = TypeVar("_T") _S = TypeVar("_S") -#: constant to prepare valuing py.path.local replacements/lazy proxies later on +#: constant to prepare valuing pylib path replacements/lazy proxies later on # intended for removal in pytest 8.0 or 9.0 -LEGACY_PATH = py.path.local +# fmt: off +# intentional space to create a fake difference for the verification +LEGACY_PATH = py.path. local +# fmt: on def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH: - """Internal wrapper to prepare lazy proxies for py.path.local instances""" - return py.path.local(path) + """Internal wrapper to prepare lazy proxies for legacy_path instances""" + return LEGACY_PATH(path) # fmt: off diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c029c29a3a2..3f138efa750 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -32,7 +32,6 @@ from typing import Union import attr -import py from pluggy import HookimplMarker from pluggy import HookspecMarker from pluggy import PluginManager @@ -48,6 +47,8 @@ from _pytest._io import TerminalWriter from _pytest.compat import final from _pytest.compat import importlib_metadata +from _pytest.compat import LEGACY_PATH +from _pytest.compat import legacy_path from _pytest.outcomes import fail from _pytest.outcomes import Skipped from _pytest.pathlib import absolutepath @@ -937,15 +938,15 @@ def __init__( self.cache: Optional[Cache] = None @property - def invocation_dir(self) -> py.path.local: + def invocation_dir(self) -> LEGACY_PATH: """The directory from which pytest was invoked. Prefer to use :attr:`invocation_params.dir `, which is a :class:`pathlib.Path`. - :type: py.path.local + :type: LEGACY_PATH """ - return py.path.local(str(self.invocation_params.dir)) + return legacy_path(str(self.invocation_params.dir)) @property def rootpath(self) -> Path: @@ -958,14 +959,14 @@ def rootpath(self) -> Path: return self._rootpath @property - def rootdir(self) -> py.path.local: + def rootdir(self) -> LEGACY_PATH: """The path to the :ref:`rootdir `. Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. - :type: py.path.local + :type: LEGACY_PATH """ - return py.path.local(str(self.rootpath)) + return legacy_path(str(self.rootpath)) @property def inipath(self) -> Optional[Path]: @@ -978,14 +979,14 @@ def inipath(self) -> Optional[Path]: return self._inipath @property - def inifile(self) -> Optional[py.path.local]: + def inifile(self) -> Optional[LEGACY_PATH]: """The path to the :ref:`configfile `. Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. - :type: Optional[py.path.local] + :type: Optional[LEGACY_PATH] """ - return py.path.local(str(self.inipath)) if self.inipath else None + return legacy_path(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 @@ -1420,7 +1421,7 @@ def _getini(self, name: str): assert self.inipath is not None dp = self.inipath.parent input_values = shlex.split(value) if isinstance(value, str) else value - return [py.path.local(str(dp / x)) for x in input_values] + return [legacy_path(str(dp / x)) for x in input_values] elif type == "args": return shlex.split(value) if isinstance(value, str) else value elif type == "linelist": @@ -1446,7 +1447,7 @@ def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]: for relroot in relroots: if isinstance(relroot, Path): pass - elif isinstance(relroot, py.path.local): + elif isinstance(relroot, LEGACY_PATH): relroot = Path(relroot) else: relroot = relroot.replace("/", os.sep) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index c203eadc1ad..596574877bf 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -92,7 +92,7 @@ NODE_FSPATH = UnformattedWarning( PytestDeprecationWarning, "{type}.fspath is deprecated and will be replaced by {type}.path.\n" - "see TODO;URL for details on replacing py.path.local with pathlib.Path", + "see https://docs.pytest.org/en/latest/deprecations.html#node-fspath-in-favor-of-pathlib-and-node-path", ) # You want to make some `__init__` or function "private". diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 4942a8f793b..41d295daaba 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -22,14 +22,13 @@ from typing import TYPE_CHECKING from typing import Union -import py.path - import pytest from _pytest import outcomes from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprFileLocation from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter +from _pytest.compat import LEGACY_PATH from _pytest.compat import legacy_path from _pytest.compat import safe_getattr from _pytest.config import Config @@ -123,7 +122,7 @@ def pytest_unconfigure() -> None: def pytest_collect_file( fspath: Path, - path: py.path.local, + path: LEGACY_PATH, parent: Collector, ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: config = parent.config diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index b0b8fd53d85..7d5f767db7e 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -11,7 +11,6 @@ from typing import TYPE_CHECKING from typing import Union -import py.path from pluggy import HookspecMarker from _pytest.deprecated import WARNING_CAPTURED_HOOK @@ -42,6 +41,7 @@ from _pytest.reports import TestReport from _pytest.runner import CallInfo from _pytest.terminal import TerminalReporter + from _pytest.compat import LEGACY_PATH hookspec = HookspecMarker("pytest") @@ -263,7 +263,7 @@ def pytest_collection_finish(session: "Session") -> None: @hookspec(firstresult=True) def pytest_ignore_collect( - fspath: Path, path: py.path.local, config: "Config" + fspath: Path, path: "LEGACY_PATH", config: "Config" ) -> Optional[bool]: """Return True to prevent considering this path for collection. @@ -273,7 +273,7 @@ def pytest_ignore_collect( Stops at first non-None result, see :ref:`firstresult`. :param pathlib.Path fspath: The path to analyze. - :param py.path.local path: The path to analyze. + :param LEGACY_PATH path: The path to analyze. :param _pytest.config.Config config: The pytest config object. .. versionchanged:: 6.3.0 @@ -283,14 +283,14 @@ def pytest_ignore_collect( def pytest_collect_file( - fspath: Path, path: py.path.local, parent: "Collector" + fspath: Path, path: "LEGACY_PATH", parent: "Collector" ) -> "Optional[Collector]": """Create a Collector for the given path, or None if not relevant. The new node needs to have the specified ``parent`` as a parent. :param pathlib.Path fspath: The path to analyze. - :param py.path.local path: The path to collect. + :param LEGACY_PATH path: The path to collect. .. versionchanged:: 6.3.0 The ``fspath`` parameter was added as a :class:`pathlib.Path` @@ -335,7 +335,7 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor @hookspec(firstresult=True) def pytest_pycollect_makemodule( - fspath: Path, path: py.path.local, parent + fspath: Path, path: "LEGACY_PATH", parent ) -> Optional["Module"]: """Return a Module collector or None for the given path. @@ -346,7 +346,7 @@ def pytest_pycollect_makemodule( Stops at first non-None result, see :ref:`firstresult`. :param pathlib.Path fspath: The path of the module to collect. - :param py.path.local path: The path of the module to collect. + :param legacy_path path: The path of the module to collect. .. versionchanged:: 6.3.0 The ``fspath`` parameter was added as a :class:`pathlib.Path` @@ -676,13 +676,13 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No def pytest_report_header( - config: "Config", startpath: Path, startdir: py.path.local + config: "Config", startpath: Path, startdir: "LEGACY_PATH" ) -> 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: The pytest config object. :param Path startpath: The starting dir. - :param py.path.local startdir: The starting dir. + :param LEGACY_PATH startdir: The starting dir. .. note:: @@ -706,7 +706,7 @@ def pytest_report_header( def pytest_report_collectionfinish( config: "Config", startpath: Path, - startdir: py.path.local, + startdir: "LEGACY_PATH", items: Sequence["Item"], ) -> Union[str, List[str]]: """Return a string or list of strings to be displayed after collection @@ -718,7 +718,7 @@ def pytest_report_collectionfinish( :param _pytest.config.Config config: The pytest config object. :param Path startpath: The starting path. - :param py.path.local startdir: The starting dir. + :param LEGACY_PATH startdir: The starting dir. :param items: List of pytest items that are going to be executed; this list should not be modified. .. note:: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 3dc00fa691e..3e7213489ff 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -21,11 +21,11 @@ from typing import Union import attr -import py import _pytest._code from _pytest import nodes from _pytest.compat import final +from _pytest.compat import legacy_path from _pytest.config import Config from _pytest.config import directory_arg from _pytest.config import ExitCode @@ -543,7 +543,7 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if direntry.name == "__pycache__": return False fspath = Path(direntry.path) - path = py.path.local(fspath) + path = legacy_path(fspath) ihook = self.gethookproxy(fspath.parent) if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): return False @@ -555,7 +555,7 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: def _collectfile( self, fspath: Path, handle_dupes: bool = True ) -> Sequence[nodes.Collector]: - path = py.path.local(fspath) + path = legacy_path(fspath) assert ( fspath.is_file() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 47752d34c61..9d93659e1ad 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -16,8 +16,6 @@ from typing import TypeVar from typing import Union -import py - import _pytest._code from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo @@ -145,7 +143,7 @@ def __init__( parent: "Optional[Node]" = None, config: Optional[Config] = None, session: "Optional[Session]" = None, - fspath: Optional[py.path.local] = None, + fspath: Optional[LEGACY_PATH] = None, path: Optional[Path] = None, nodeid: Optional[str] = None, ) -> None: @@ -199,13 +197,13 @@ def __init__( self._store = Store() @property - def fspath(self): - """(deprecated) returns a py.path.local copy of self.path""" + def fspath(self) -> LEGACY_PATH: + """(deprecated) returns a legacy_path copy of self.path""" warnings.warn(NODE_FSPATH.format(type=type(self).__name__), stacklevel=2) - return py.path.local(self.path) + return legacy_path(self.path) @fspath.setter - def fspath(self, value: py.path.local): + def fspath(self, value: LEGACY_PATH) -> None: warnings.warn(NODE_FSPATH.format(type=type(self).__name__), stacklevel=2) self.path = Path(value) @@ -464,7 +462,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i * "obj": a Python object that the node wraps. * "fspath": just a path - :rtype: A tuple of (str|py.path.local, int) with filename and line number. + :rtype: A tuple of (str|Path, int) with filename and line number. """ # See Item.location. location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None) @@ -520,10 +518,10 @@ def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: def _check_initialpaths_for_relpath( - session: "Session", fspath: py.path.local + session: "Session", fspath: LEGACY_PATH ) -> Optional[str]: for initial_path in session._initialpaths: - initial_path_ = py.path.local(initial_path) + initial_path_ = legacy_path(initial_path) if fspath.common(initial_path_) == initial_path_: return fspath.relto(initial_path_) return None @@ -532,7 +530,7 @@ def _check_initialpaths_for_relpath( class FSCollector(Collector): def __init__( self, - fspath: Optional[py.path.local], + fspath: Optional[LEGACY_PATH], path: Optional[Path], parent=None, config: Optional[Config] = None, @@ -571,7 +569,7 @@ def from_parent( cls, parent, *, - fspath: Optional[py.path.local] = None, + fspath: Optional[LEGACY_PATH] = None, path: Optional[Path] = None, **kw, ): @@ -638,8 +636,10 @@ def add_report_section(self, when: str, key: str, content: str) -> None: if content: self._report_sections.append((when, key, content)) - def reportinfo(self) -> Tuple[Union[py.path.local, str], Optional[int], str]: - return self.fspath, None, "" + def reportinfo(self) -> Tuple[Union[LEGACY_PATH, str], Optional[int], str]: + + # TODO: enable Path objects in reportinfo + return legacy_path(self.path), None, "" @cached_property def location(self) -> Tuple[str, Optional[int], str]: diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index f2a6d2aab92..699738e1282 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -34,7 +34,6 @@ from weakref import WeakKeyDictionary import attr -import py from iniconfig import IniConfig from iniconfig import SectionWrapper @@ -42,6 +41,8 @@ from _pytest._code import Source from _pytest.capture import _get_multicapture from _pytest.compat import final +from _pytest.compat import LEGACY_PATH +from _pytest.compat import legacy_path from _pytest.compat import NOTSET from _pytest.compat import NotSetType from _pytest.config import _PluggyPlugin @@ -475,7 +476,7 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt def testdir(pytester: "Pytester") -> "Testdir": """ Identical to :fixture:`pytester`, and provides an instance whose methods return - legacy ``py.path.local`` objects instead when applicable. + legacy ``LEGACY_PATH`` objects instead when applicable. New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. """ @@ -934,10 +935,10 @@ def copy_example(self, name: Optional[str] = None) -> Path: 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, + # TODO: legacy_path.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)) + # we will need to roll our own in order to drop legacy_path completely + legacy_path(example_path).copy(legacy_path(self.path)) return self.path elif example_path.is_file(): result = self.path.joinpath(example_path.name) @@ -958,12 +959,12 @@ def getnode( :param _pytest.config.Config config: A pytest config. See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it. - :param py.path.local arg: + :param os.PathLike[str] arg: Path to the file. """ session = Session.from_config(config) assert "::" not in str(arg) - p = py.path.local(arg) + p = legacy_path(arg) config.hook.pytest_sessionstart(session=session) res = session.perform_collect([str(p)], genitems=False)[0] config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) @@ -975,7 +976,7 @@ def getpathnode(self, path: Union[str, "os.PathLike[str]"]): This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to create the (configured) pytest Config instance. - :param py.path.local path: Path to the file. + :param os.PathLike[str] path: Path to the file. """ path = Path(path) config = self.parseconfigure(path) @@ -1520,10 +1521,10 @@ def assert_contains_lines(self, lines2: Sequence[str]) -> None: @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. + Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead. All methods just forward to an internal :class:`Pytester` instance, converting results - to `py.path.local` objects as necessary. + to `legacy_path` objects as necessary. """ __test__ = False @@ -1537,13 +1538,13 @@ def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None: self._pytester = pytester @property - def tmpdir(self) -> py.path.local: + def tmpdir(self) -> LEGACY_PATH: """Temporary directory where tests are executed.""" - return py.path.local(self._pytester.path) + return legacy_path(self._pytester.path) @property - def test_tmproot(self) -> py.path.local: - return py.path.local(self._pytester._test_tmproot) + def test_tmproot(self) -> LEGACY_PATH: + return legacy_path(self._pytester._test_tmproot) @property def request(self): @@ -1573,7 +1574,7 @@ def finalize(self) -> None: """See :meth:`Pytester._finalize`.""" return self._pytester._finalize() - def makefile(self, ext, *args, **kwargs) -> py.path.local: + def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH: """See :meth:`Pytester.makefile`.""" if ext and not ext.startswith("."): # pytester.makefile is going to throw a ValueError in a way that @@ -1583,47 +1584,47 @@ def makefile(self, ext, *args, **kwargs) -> py.path.local: # allowed this, we will prepend "." as a workaround to avoid breaking # testdir usage that worked before ext = "." + ext - return py.path.local(str(self._pytester.makefile(ext, *args, **kwargs))) + return legacy_path(self._pytester.makefile(ext, *args, **kwargs)) - def makeconftest(self, source) -> py.path.local: + def makeconftest(self, source) -> LEGACY_PATH: """See :meth:`Pytester.makeconftest`.""" - return py.path.local(str(self._pytester.makeconftest(source))) + return legacy_path(self._pytester.makeconftest(source)) - def makeini(self, source) -> py.path.local: + def makeini(self, source) -> LEGACY_PATH: """See :meth:`Pytester.makeini`.""" - return py.path.local(str(self._pytester.makeini(source))) + return legacy_path(self._pytester.makeini(source)) def getinicfg(self, source: str) -> SectionWrapper: """See :meth:`Pytester.getinicfg`.""" return self._pytester.getinicfg(source) - def makepyprojecttoml(self, source) -> py.path.local: + def makepyprojecttoml(self, source) -> LEGACY_PATH: """See :meth:`Pytester.makepyprojecttoml`.""" - return py.path.local(str(self._pytester.makepyprojecttoml(source))) + return legacy_path(self._pytester.makepyprojecttoml(source)) - def makepyfile(self, *args, **kwargs) -> py.path.local: + def makepyfile(self, *args, **kwargs) -> LEGACY_PATH: """See :meth:`Pytester.makepyfile`.""" - return py.path.local(str(self._pytester.makepyfile(*args, **kwargs))) + return legacy_path(self._pytester.makepyfile(*args, **kwargs)) - def maketxtfile(self, *args, **kwargs) -> py.path.local: + def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH: """See :meth:`Pytester.maketxtfile`.""" - return py.path.local(str(self._pytester.maketxtfile(*args, **kwargs))) + return legacy_path(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: + def mkdir(self, name) -> LEGACY_PATH: """See :meth:`Pytester.mkdir`.""" - return py.path.local(str(self._pytester.mkdir(name))) + return legacy_path(self._pytester.mkdir(name)) - def mkpydir(self, name) -> py.path.local: + def mkpydir(self, name) -> LEGACY_PATH: """See :meth:`Pytester.mkpydir`.""" - return py.path.local(str(self._pytester.mkpydir(name))) + return legacy_path(self._pytester.mkpydir(name)) - def copy_example(self, name=None) -> py.path.local: + def copy_example(self, name=None) -> LEGACY_PATH: """See :meth:`Pytester.copy_example`.""" - return py.path.local(str(self._pytester.copy_example(name))) + return legacy_path(self._pytester.copy_example(name)) def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]: """See :meth:`Pytester.getnode`.""" diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 7d518dbbf4b..ccd685f54a9 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -26,8 +26,6 @@ from typing import TYPE_CHECKING from typing import Union -import py - import _pytest from _pytest import fixtures from _pytest import nodes @@ -45,6 +43,8 @@ from _pytest.compat import getlocation from _pytest.compat import is_async_function from _pytest.compat import is_generator +from _pytest.compat import LEGACY_PATH +from _pytest.compat import legacy_path from _pytest.compat import NOTSET from _pytest.compat import REGEX_TYPE from _pytest.compat import safe_getattr @@ -189,7 +189,7 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: def pytest_collect_file( - fspath: Path, path: py.path.local, parent: nodes.Collector + fspath: Path, path: LEGACY_PATH, parent: nodes.Collector ) -> Optional["Module"]: if fspath.suffix == ".py": if not parent.session.isinitpath(fspath): @@ -210,7 +210,7 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: return any(fnmatch_ex(pattern, path) for pattern in patterns) -def pytest_pycollect_makemodule(fspath: Path, path: py.path.local, parent) -> "Module": +def pytest_pycollect_makemodule(fspath: Path, path: LEGACY_PATH, parent) -> "Module": if fspath.name == "__init__.py": pkg: Package = Package.from_parent(parent, fspath=path) return pkg @@ -321,7 +321,7 @@ def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> parts.reverse() return ".".join(parts) - def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]: + def reportinfo(self) -> Tuple[Union[LEGACY_PATH, str], int, str]: # XXX caching? obj = self.obj compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) @@ -330,12 +330,12 @@ 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: Union[py.path.local, str] = file_path + fspath: Union[LEGACY_PATH, str] = file_path lineno = compat_co_firstlineno else: path, lineno = getfslineno(obj) if isinstance(path, Path): - fspath = py.path.local(path) + fspath = legacy_path(path) else: fspath = path modpath = self.getmodpath() @@ -624,7 +624,7 @@ def _importtestmodule(self): class Package(Module): def __init__( self, - fspath: Optional[py.path.local], + fspath: Optional[LEGACY_PATH], parent: nodes.Collector, # NOTE: following args are unused: config=None, @@ -675,7 +675,7 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if direntry.name == "__pycache__": return False fspath = Path(direntry.path) - path = py.path.local(fspath) + path = legacy_path(fspath) ihook = self.session.gethookproxy(fspath.parent) if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): return False @@ -687,7 +687,7 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: def _collectfile( self, fspath: Path, handle_dupes: bool = True ) -> Sequence[nodes.Collector]: - path = py.path.local(fspath) + path = legacy_path(fspath) assert ( fspath.is_file() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 303f731ddaa..657e0683378 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -15,7 +15,6 @@ from typing import Union import attr -import py from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo @@ -30,6 +29,7 @@ from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter from _pytest.compat import final +from _pytest.compat import LEGACY_PATH from _pytest.config import Config from _pytest.nodes import Collector from _pytest.nodes import Item @@ -500,7 +500,7 @@ def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]: else: d["longrepr"] = report.longrepr for name in d: - if isinstance(d[name], (py.path.local, Path)): + if isinstance(d[name], (LEGACY_PATH, Path)): d[name] = str(d[name]) elif name == "result": d[name] = None # for now diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 47729ae5fee..d30e1e57feb 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -6,13 +6,14 @@ from typing import Optional import attr -import py 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.compat import LEGACY_PATH +from _pytest.compat import legacy_path from _pytest.config import Config from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture @@ -133,7 +134,7 @@ def getbasetemp(self) -> Path: @final @attr.s(init=False) class TempdirFactory: - """Backward comptibility wrapper that implements :class:``py.path.local`` + """Backward comptibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH`` for :class:``TempPathFactory``.""" _tmppath_factory = attr.ib(type=TempPathFactory) @@ -144,13 +145,13 @@ def __init__( 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()) + def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH: + """Same as :meth:`TempPathFactory.mktemp`, but returns a ``_pytest.compat.LEGACY_PATH`` object.""" + return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve()) - def getbasetemp(self) -> py.path.local: + def getbasetemp(self) -> LEGACY_PATH: """Backward compat wrapper for ``_tmppath_factory.getbasetemp``.""" - return py.path.local(self._tmppath_factory.getbasetemp().resolve()) + return legacy_path(self._tmppath_factory.getbasetemp().resolve()) def get_user() -> Optional[str]: @@ -202,7 +203,7 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: @fixture -def tmpdir(tmp_path: Path) -> py.path.local: +def tmpdir(tmp_path: Path) -> LEGACY_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. @@ -212,11 +213,11 @@ def tmpdir(tmp_path: Path) -> py.path.local: ``--basetemp`` is used then it is cleared each session. See :ref:`base temporary directory`. - The returned object is a `py.path.local`_ path object. + The returned object is a `legacy_path`_ object. - .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html + .. _legacy_path: https://py.readthedocs.io/en/latest/path.html """ - return py.path.local(tmp_path) + return legacy_path(tmp_path) @fixture diff --git a/testing/test_collection.py b/testing/test_collection.py index 248071111f3..5f0b5902ab8 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -6,8 +6,6 @@ from pathlib import Path from typing import List -import py.path - import pytest from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest @@ -369,9 +367,10 @@ def pytest_ignore_collect(path, config): def test_collectignore_exclude_on_option(self, pytester: Pytester) -> None: pytester.makeconftest( """ - import py + # potentially avoid dependency on pylib + from _pytest.compat import legacy_path from pathlib import Path - collect_ignore = [py.path.local('hello'), 'test_world.py', Path('bye')] + collect_ignore = [legacy_path('hello'), 'test_world.py', Path('bye')] def pytest_addoption(parser): parser.addoption("--XX", action="store_true", default=False) def pytest_configure(config): @@ -1347,6 +1346,7 @@ def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) -> Context: https://github.com/pytest-dev/pytest-cpp/pull/47 """ + from _pytest.compat import legacy_path class MyCollector(pytest.File): def __init__(self, *k, x, **kw): @@ -1354,7 +1354,7 @@ def __init__(self, *k, x, **kw): self.x = x collector = MyCollector.from_parent( - parent=request.session, fspath=py.path.local(pytester.path) / "foo", x=10 + parent=request.session, fspath=legacy_path(pytester.path) / "foo", x=10 ) assert collector.x == 10 diff --git a/testing/test_main.py b/testing/test_main.py index 2ed111895cd..4450029342e 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -205,7 +205,7 @@ def test_absolute_paths_are_resolved_correctly(self, invocation_path: Path) -> N def test_module_full_path_without_drive(pytester: Pytester) -> None: """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 + Passing a full path without a drive letter would trigger a bug in legacy_path 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. """ diff --git a/testing/test_nodes.py b/testing/test_nodes.py index 59d9f409eac..dde161777cd 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -3,10 +3,9 @@ from typing import List from typing import Type -import py - import pytest from _pytest import nodes +from _pytest.compat import legacy_path from _pytest.pytester import Pytester from _pytest.warning_types import PytestWarning @@ -77,7 +76,7 @@ class FakeSession1: session = cast(pytest.Session, FakeSession1) - assert nodes._check_initialpaths_for_relpath(session, py.path.local(cwd)) == "" + assert nodes._check_initialpaths_for_relpath(session, legacy_path(cwd)) == "" sub = cwd / "file" @@ -86,9 +85,9 @@ class FakeSession2: session = cast(pytest.Session, FakeSession2) - assert nodes._check_initialpaths_for_relpath(session, py.path.local(sub)) == "file" + assert nodes._check_initialpaths_for_relpath(session, legacy_path(sub)) == "file" - outside = py.path.local("/outside") + outside = legacy_path("/outside") assert nodes._check_initialpaths_for_relpath(session, outside) is None diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index c33337b67b3..6ba9269e564 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -4,9 +4,8 @@ import subprocess import sys -import py - import pytest +from _pytest.compat import legacy_path from _pytest.config import argparsing as parseopt from _pytest.config.exceptions import UsageError from _pytest.monkeypatch import MonkeyPatch @@ -124,11 +123,11 @@ def test_parse(self, parser: parseopt.Parser) -> None: assert not getattr(args, parseopt.FILE_OR_DIR) def test_parse2(self, parser: parseopt.Parser) -> None: - args = parser.parse([py.path.local()]) - assert getattr(args, parseopt.FILE_OR_DIR)[0] == py.path.local() + args = parser.parse([legacy_path(".")]) + assert getattr(args, parseopt.FILE_OR_DIR)[0] == legacy_path(".") def test_parse_known_args(self, parser: parseopt.Parser) -> None: - parser.parse_known_args([py.path.local()]) + parser.parse_known_args([legacy_path(".")]) parser.addoption("--hello", action="store_true") ns = parser.parse_known_args(["x", "--y", "--hello", "this"]) assert ns.hello diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 48149084ece..d71e44e36b6 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -7,9 +7,8 @@ from types import ModuleType from typing import Generator -import py - import pytest +from _pytest.compat import legacy_path from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import bestrelpath from _pytest.pathlib import commonpath @@ -28,14 +27,14 @@ class TestFNMatcherPort: """Test that our port of py.common.FNMatcher (fnmatch_ex) produces the - same results as the original py.path.local.fnmatch method.""" + same results as the original legacy_path.fnmatch method.""" @pytest.fixture(params=["pathlib", "py.path"]) def match(self, request): if request.param == "py.path": def match_(pattern, path): - return py.path.local(path).fnmatch(pattern) + return legacy_path(path).fnmatch(pattern) else: assert request.param == "pathlib" diff --git a/testing/test_reports.py b/testing/test_reports.py index b376f6198ae..3da63c2c873 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -1,11 +1,10 @@ from typing import Sequence from typing import Union -import py.path - import pytest from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionRepr +from _pytest.compat import legacy_path from _pytest.config import Config from _pytest.pytester import Pytester from _pytest.reports import CollectReport @@ -237,7 +236,7 @@ def test_a(): reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 3 test_a_call = reports[1] - test_a_call.path1 = py.path.local(pytester.path) # type: ignore[attr-defined] + test_a_call.path1 = legacy_path(pytester.path) # type: ignore[attr-defined] test_a_call.path2 = pytester.path # type: ignore[attr-defined] data = test_a_call._to_json() assert data["path1"] == str(pytester.path) From 620e8196561e0d8b359b25f1c494ea0002d3cb3b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 6 Mar 2021 22:59:33 +0100 Subject: [PATCH 0477/2846] Add enterPy training (#8396) --- doc/en/index.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/en/index.rst b/doc/en/index.rst index 084725ec2c1..7f74534027c 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -1,5 +1,11 @@ :orphan: +.. sidebar:: Next Open Trainings + + - `Professionelles Testen für Python mit pytest `_ (German), part of the enterPy conference, April 22nd, remote. + + Also see `previous talks and blogposts `_. + .. _features: pytest: helps you write better programs From f0ad73c4b05b4307e3dbf890f4e2772ea73e0a2e Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sun, 7 Mar 2021 00:48:50 +0000 Subject: [PATCH 0478/2846] [automated] Update plugin list --- doc/en/plugin_list.rst | 43 ++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/doc/en/plugin_list.rst b/doc/en/plugin_list.rst index f50b6033ff1..8426f469c94 100644 --- a/doc/en/plugin_list.rst +++ b/doc/en/plugin_list.rst @@ -3,7 +3,7 @@ Plugins List PyPI projects that match "pytest-\*" are considered plugins and are listed automatically. Packages classified as inactive are excluded. -This list contains 833 plugins. +This list contains 836 plugins. ============================================================================================================== ======================================================================================================================================================================== ============== ===================== ============================================ name summary last release status requires @@ -11,7 +11,7 @@ name `pytest-adaptavist `_ pytest plugin for generating test execution results within Jira Test Management (tm4j) Feb 05, 2020 N/A pytest (>=3.4.1) `pytest-adf `_ Pytest plugin for writing Azure Data Factory integration tests Jun 03, 2020 4 - Beta pytest (>=3.5.0) `pytest-aggreport `_ pytest plugin for pytest-repeat that generate aggregate report of the same test cases with additional statistics details. Jul 19, 2019 4 - Beta pytest (>=4.3.1) -`pytest-aio `_ Pytest plugin for testing async python code Feb 27, 2021 4 - Beta pytest ; extra == 'tests' +`pytest-aio `_ Pytest plugin for testing async python code Mar 02, 2021 4 - Beta pytest ; extra == 'tests' `pytest-aiofiles `_ pytest fixtures for writing aiofiles tests with pyfakefs May 14, 2017 5 - Production/Stable N/A `pytest-aiohttp `_ pytest plugin for aiohttp support Dec 05, 2017 N/A pytest `pytest-aiohttp-client `_ Pytest `client` fixture for the Aiohttp Nov 01, 2020 N/A pytest (>=6) @@ -152,8 +152,9 @@ name `pytest-curl-report `_ pytest plugin to generate curl command line report Dec 11, 2016 4 - Beta N/A `pytest-custom-concurrency `_ Custom grouping concurrence for pytest Feb 08, 2021 N/A N/A `pytest-custom-exit-code `_ Exit pytest test session with custom exit code in different scenarios Aug 07, 2019 4 - Beta pytest (>=4.0.2) +`pytest-custom-nodeid `_ Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report Mar 02, 2021 N/A N/A `pytest-custom-report `_ Configure the symbols displayed for test outcomes Jan 30, 2019 N/A pytest -`pytest-custom-scheduling `_ Custom grouping for pytest-xdist Feb 22, 2021 N/A N/A +`pytest-custom-scheduling `_ Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report Mar 01, 2021 N/A N/A `pytest-cython `_ A plugin for testing Cython extension modules Jan 26, 2021 4 - Beta pytest (>=2.7.3) `pytest-darker `_ A pytest plugin for checking of modified code using Darker Aug 16, 2020 N/A pytest (>=6.0.1) ; extra == 'test' `pytest-dash `_ pytest fixtures to run dash applications. Mar 18, 2019 N/A N/A @@ -200,7 +201,7 @@ name `pytest-django-lite `_ The bare minimum to integrate py.test with Django. Jan 30, 2014 N/A N/A `pytest-django-model `_ A Simple Way to Test your Django Models Feb 14, 2019 4 - Beta N/A `pytest-django-ordering `_ A pytest plugin for preserving the order in which Django runs tests. Jul 25, 2019 5 - Production/Stable pytest (>=2.3.0) -`pytest-django-queries `_ Generate performance reports from your django database performance tests. Sep 03, 2020 N/A N/A +`pytest-django-queries `_ Generate performance reports from your django database performance tests. Mar 01, 2021 N/A N/A `pytest-djangorestframework `_ A djangorestframework plugin for pytest Aug 11, 2019 4 - Beta N/A `pytest-django-rq `_ A pytest plugin to help writing unit test for django-rq Apr 13, 2020 4 - Beta N/A `pytest-django-sqlcounts `_ py.test plugin for reporting the number of SQLs executed per django testcase. Jun 16, 2015 4 - Beta N/A @@ -216,8 +217,8 @@ name `pytest-docker-pexpect `_ pytest plugin for writing functional tests with pexpect and docker Jan 14, 2019 N/A pytest `pytest-docker-postgresql `_ A simple plugin to use with pytest Sep 24, 2019 4 - Beta pytest (>=3.5.0) `pytest-docker-py `_ Easy to use, simple to extend, pytest plugin that minimally leverages docker-py. Nov 27, 2018 N/A pytest (==4.0.0) -`pytest-docker-registry-fixtures `_ Pytest fixtures for testing with docker registries. Feb 17, 2021 4 - Beta pytest -`pytest-docker-tools `_ Docker integration tests for pytest Jan 15, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) +`pytest-docker-registry-fixtures `_ Pytest fixtures for testing with docker registries. Mar 04, 2021 4 - Beta pytest +`pytest-docker-tools `_ Docker integration tests for pytest Mar 02, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) `pytest-docs `_ Documentation tool for pytest Nov 11, 2018 4 - Beta pytest (>=3.5.0) `pytest-docstyle `_ pytest plugin to run pydocstyle Mar 23, 2020 3 - Alpha N/A `pytest-doctest-custom `_ A py.test plugin for customizing string representations of doctest results. Jul 25, 2016 4 - Beta N/A @@ -274,6 +275,7 @@ name `pytest-factoryboy `_ Factory Boy support for pytest. Dec 30, 2020 6 - Mature pytest (>=4.6) `pytest-factoryboy-fixtures `_ Generates pytest fixtures that allow the use of type hinting Jun 25, 2020 N/A N/A `pytest-factoryboy-state `_ Simple factoryboy random state management Dec 11, 2020 4 - Beta pytest (>=5.0) +`pytest-failed-screenshot `_ Test case fails,take a screenshot,save it,attach it to the allure Feb 28, 2021 N/A N/A `pytest-failed-to-verify `_ A pytest plugin that helps better distinguishing real test failures from setup flakiness. Aug 08, 2019 5 - Production/Stable pytest (>=4.1.0) `pytest-faker `_ Faker integration with the pytest framework. Dec 19, 2016 6 - Mature N/A `pytest-falcon `_ Pytest helpers for Falcon. Sep 07, 2016 4 - Beta N/A @@ -321,11 +323,11 @@ name `pytest-girder `_ A set of pytest fixtures for testing Girder applications. Feb 25, 2021 N/A N/A `pytest-git `_ Git repository fixture for py.test May 28, 2019 5 - Production/Stable pytest `pytest-gitcov `_ Pytest plugin for reporting on coverage of the last git commit. Jan 11, 2020 2 - Pre-Alpha N/A -`pytest-git-fixtures `_ Pytest fixtures for testing with git. Jan 25, 2021 4 - Beta pytest +`pytest-git-fixtures `_ Pytest fixtures for testing with git. Mar 04, 2021 4 - Beta pytest `pytest-github `_ Plugin for py.test that associates tests with github issues using a marker. Mar 07, 2019 5 - Production/Stable N/A `pytest-github-actions-annotate-failures `_ pytest plugin to annotate failed tests with a workflow command for GitHub Actions Oct 13, 2020 N/A pytest (>=4.0.0) `pytest-gitignore `_ py.test plugin to ignore the same files as git Jul 17, 2015 4 - Beta N/A -`pytest-gnupg-fixtures `_ Pytest fixtures for testing with gnupg. Feb 22, 2021 4 - Beta pytest +`pytest-gnupg-fixtures `_ Pytest fixtures for testing with gnupg. Mar 04, 2021 4 - Beta pytest `pytest-golden `_ Plugin for pytest that offloads expected outputs to data files Nov 23, 2020 N/A pytest (>=6.1.2,<7.0.0) `pytest-graphql-schema `_ Get graphql schema as fixture for pytest Oct 18, 2019 N/A N/A `pytest-greendots `_ Green progress dots Feb 08, 2014 3 - Alpha N/A @@ -342,8 +344,9 @@ name `pytest-historic `_ Custom report to display pytest historical execution records Apr 08, 2020 N/A pytest `pytest-historic-hook `_ Custom listener to store execution results into MYSQL DB, which is used for pytest-historic report Apr 08, 2020 N/A pytest `pytest-homeassistant `_ A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A -`pytest-homeassistant-custom-component `_ Experimental package to automatically extract test plugins for Home Assistant custom components Feb 10, 2021 3 - Alpha pytest (==6.2.2) +`pytest-homeassistant-custom-component `_ Experimental package to automatically extract test plugins for Home Assistant custom components Mar 03, 2021 3 - Alpha pytest (==6.2.2) `pytest-honors `_ Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A +`pytest-hoverfly `_ Simplify working with Hoverfly from pytest Mar 04, 2021 N/A pytest (>=5.0) `pytest-hoverfly-wrapper `_ Integrates the Hoverfly HTTP proxy into Pytest Jan 31, 2021 4 - Beta N/A `pytest-html `_ pytest plugin for generating HTML reports Dec 13, 2020 5 - Production/Stable pytest (!=6.0.0,>=5.0) `pytest-html-lee `_ optimized pytest plugin for generating HTML reports Jun 30, 2020 5 - Production/Stable pytest (>=5.0) @@ -355,7 +358,7 @@ name `pytest-http-mocker `_ Pytest plugin for http mocking (via https://github.com/vilus/mocker) Oct 20, 2019 N/A N/A `pytest-httpretty `_ A thin wrapper of HTTPretty for pytest Feb 16, 2014 3 - Alpha N/A `pytest-httpserver `_ pytest-httpserver is a httpserver for pytest Feb 14, 2021 3 - Alpha pytest ; extra == 'dev' -`pytest-httpx `_ Send responses to httpx. Nov 25, 2020 5 - Production/Stable pytest (==6.*) +`pytest-httpx `_ Send responses to httpx. Mar 01, 2021 5 - Production/Stable pytest (==6.*) `pytest-hue `_ Visualise PyTest status via your Phillips Hue lights May 09, 2019 N/A N/A `pytest-hypo-25 `_ help hypo module for pytest Jan 12, 2020 3 - Alpha N/A `pytest-ibutsu `_ A plugin to sent pytest results to an Ibutsu server Feb 24, 2021 4 - Beta pytest @@ -371,7 +374,7 @@ name `pytest-inmanta `_ A py.test plugin providing fixtures to simplify inmanta modules testing. Oct 12, 2020 5 - Production/Stable N/A `pytest-inmanta-extensions `_ Inmanta tests package Jan 07, 2021 5 - Production/Stable N/A `pytest-Inomaly `_ A simple image diff plugin for pytest Feb 13, 2018 4 - Beta N/A -`pytest-insta `_ A practical snapshot testing plugin for pytest Feb 25, 2021 N/A pytest (>=6.0.2,<7.0.0) +`pytest-insta `_ A practical snapshot testing plugin for pytest Mar 03, 2021 N/A pytest (>=6.0.2,<7.0.0) `pytest-instafail `_ pytest plugin to show failures instantly Jun 14, 2020 4 - Beta pytest (>=2.9) `pytest-instrument `_ pytest plugin to instrument tests Apr 05, 2020 5 - Production/Stable pytest (>=5.1.0) `pytest-integration `_ Organizing pytests by integration or not Apr 16, 2020 N/A N/A @@ -642,29 +645,29 @@ name `pytest-roast `_ pytest plugin for ROAST configuration override and fixtures Feb 05, 2021 5 - Production/Stable pytest (<6) `pytest-rotest `_ Pytest integration with rotest Sep 08, 2019 N/A pytest (>=3.5.0) `pytest-rpc `_ Extend py.test for RPC OpenStack testing. Feb 22, 2019 4 - Beta pytest (~=3.6) -`pytest-rt `_ pytest data collector plugin for Testgr Jan 24, 2021 N/A N/A -`pytest-rts `_ Coverage-based regression test selection (RTS) plugin for pytest Feb 15, 2021 N/A pytest +`pytest-rt `_ pytest data collector plugin for Testgr Mar 03, 2021 N/A N/A +`pytest-rts `_ Coverage-based regression test selection (RTS) plugin for pytest Mar 03, 2021 N/A pytest `pytest-runfailed `_ implement a --failed option for pytest Mar 24, 2016 N/A N/A `pytest-runner `_ Invoke py.test as distutils command with dependency resolution Feb 12, 2021 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' `pytest-salt `_ Pytest Salt Plugin Jan 27, 2020 4 - Beta N/A `pytest-salt-containers `_ A Pytest plugin that builds and creates docker containers Nov 09, 2016 4 - Beta N/A -`pytest-salt-factories `_ Pytest Salt Plugin Feb 19, 2021 4 - Beta pytest (>=6.1.1) +`pytest-salt-factories `_ Pytest Salt Plugin Mar 05, 2021 4 - Beta pytest (>=6.1.1) `pytest-salt-from-filenames `_ Simple PyTest Plugin For Salt's Test Suite Specifically Jan 29, 2019 4 - Beta pytest (>=4.1) `pytest-salt-runtests-bridge `_ Simple PyTest Plugin For Salt's Test Suite Specifically Dec 05, 2019 4 - Beta pytest (>=4.1) `pytest-sanic `_ a pytest plugin for Sanic Feb 27, 2021 N/A pytest (>=5.2) `pytest-sanity `_ Dec 07, 2020 N/A N/A `pytest-sa-pg `_ May 14, 2019 N/A N/A -`pytest-sbase `_ A complete web automation framework for end-to-end testing. Feb 27, 2021 5 - Production/Stable N/A +`pytest-sbase `_ A complete web automation framework for end-to-end testing. Mar 06, 2021 5 - Production/Stable N/A `pytest-scenario `_ pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A `pytest-schema `_ 👍 Validate return values against a schema-like object in testing Aug 31, 2020 5 - Production/Stable pytest (>=3.5.0) `pytest-securestore `_ An encrypted password store for use within pytest cases Jun 19, 2019 4 - Beta N/A `pytest-select `_ A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) `pytest-selenium `_ pytest plugin for Selenium Sep 19, 2020 5 - Production/Stable pytest (>=5.0.0) -`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Feb 27, 2021 5 - Production/Stable N/A +`pytest-seleniumbase `_ A complete web automation framework for end-to-end testing. Mar 06, 2021 5 - Production/Stable N/A `pytest-selenium-enhancer `_ pytest plugin for Selenium Nov 26, 2020 5 - Production/Stable N/A `pytest-selenium-pdiff `_ A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A `pytest-send-email `_ Send pytest execution result email Dec 04, 2019 N/A N/A -`pytest-sentry `_ A pytest plugin to send testrun information to Sentry.io Dec 16, 2020 N/A N/A +`pytest-sentry `_ A pytest plugin to send testrun information to Sentry.io Mar 02, 2021 N/A N/A `pytest-server-fixtures `_ Extensible server fixures for py.test May 28, 2019 5 - Production/Stable pytest `pytest-serverless `_ Automatically mocks resources from serverless.yml in pytest using moto. Feb 20, 2021 4 - Beta N/A `pytest-services `_ Services plugin for pytest testing framework Oct 30, 2020 6 - Mature N/A @@ -726,7 +729,7 @@ name `pytest-stubprocess `_ Provide stub implementations for subprocesses in Python tests Sep 17, 2018 3 - Alpha pytest (>=3.5.0) `pytest-study `_ A pytest plugin to organize long run tests (named studies) without interfering the regular tests Sep 26, 2017 3 - Alpha pytest (>=2.0) `pytest-subprocess `_ A plugin to fake subprocess for pytest Aug 22, 2020 5 - Production/Stable pytest (>=4.0.0) -`pytest-subtesthack `_ A hack to explicitly set up and tear down fixtures. Jan 31, 2016 N/A N/A +`pytest-subtesthack `_ A hack to explicitly set up and tear down fixtures. Mar 02, 2021 N/A N/A `pytest-subtests `_ unittest subTest() support and subtests fixture Dec 13, 2020 4 - Beta pytest (>=5.3.0) `pytest-subunit `_ pytest-subunit is a plugin for py.test which outputs testsresult in subunit format. Aug 29, 2017 N/A N/A `pytest-sugar `_ pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly). Jul 06, 2020 3 - Alpha N/A @@ -827,10 +830,10 @@ name `pytest-xfiles `_ Pytest fixtures providing data read from function, module or package related (x)files. Feb 27, 2018 N/A N/A `pytest-xlog `_ Extended logging for test and decorators May 31, 2020 4 - Beta N/A `pytest-xpara `_ An extended parametrizing plugin of pytest. Oct 30, 2017 3 - Alpha pytest -`pytest-xprocess `_ A pytest plugin for managing processes across test runs. Nov 26, 2020 4 - Beta pytest (>=2.8) +`pytest-xprocess `_ A pytest plugin for managing processes across test runs. Mar 02, 2021 4 - Beta pytest (>=2.8) `pytest-xray `_ May 30, 2019 3 - Alpha N/A `pytest-xrayjira `_ Mar 17, 2020 3 - Alpha pytest (==4.3.1) -`pytest-xray-server `_ Nov 29, 2020 3 - Alpha N/A +`pytest-xray-server `_ Mar 03, 2021 3 - Alpha N/A `pytest-xvfb `_ A pytest plugin to run Xvfb for tests. Jun 09, 2020 4 - Beta pytest (>=2.8.1) `pytest-yaml `_ This plugin is used to load yaml output to your test using pytest framework. Oct 05, 2018 N/A pytest `pytest-yamltree `_ Create or check file/directory trees described by YAML Mar 02, 2020 4 - Beta pytest (>=3.1.1) From 412fc001a0770d44b281d9b39f736df3cdc80c37 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 7 Mar 2021 14:57:19 +0100 Subject: [PATCH 0479/2846] fix bug in test for issue 519 assert the actual outcome and fix the filename --- changelog/8411.trivial.rst | 1 + testing/example_scripts/issue_519.py | 4 ++-- testing/examples/test_issue519.py | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 changelog/8411.trivial.rst diff --git a/changelog/8411.trivial.rst b/changelog/8411.trivial.rst new file mode 100644 index 00000000000..8f169a900fa --- /dev/null +++ b/changelog/8411.trivial.rst @@ -0,0 +1 @@ +Assert the outcomes for the issue 518 test and fix the test. diff --git a/testing/example_scripts/issue_519.py b/testing/example_scripts/issue_519.py index 3928294886f..e44367fca04 100644 --- a/testing/example_scripts/issue_519.py +++ b/testing/example_scripts/issue_519.py @@ -20,12 +20,12 @@ def checked_order(): yield order pprint.pprint(order) assert order == [ - ("testing/example_scripts/issue_519.py", "fix1", "arg1v1"), + ("issue_519.py", "fix1", "arg1v1"), ("test_one[arg1v1-arg2v1]", "fix2", "arg2v1"), ("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"), ("test_one[arg1v1-arg2v2]", "fix2", "arg2v2"), ("test_two[arg1v1-arg2v2]", "fix2", "arg2v2"), - ("testing/example_scripts/issue_519.py", "fix1", "arg1v2"), + ("issue_519.py", "fix1", "arg1v2"), ("test_one[arg1v2-arg2v1]", "fix2", "arg2v1"), ("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"), ("test_one[arg1v2-arg2v2]", "fix2", "arg2v2"), diff --git a/testing/examples/test_issue519.py b/testing/examples/test_issue519.py index 85ba545e671..7b9c109889e 100644 --- a/testing/examples/test_issue519.py +++ b/testing/examples/test_issue519.py @@ -1,6 +1,7 @@ from _pytest.pytester import Pytester -def test_510(pytester: Pytester) -> None: +def test_519(pytester: Pytester) -> None: pytester.copy_example("issue_519.py") - pytester.runpytest("issue_519.py") + res = pytester.runpytest("issue_519.py") + res.assert_outcomes(passed=8) From 125633728f4215c59a5e0da2985f888a2a945325 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 16:54:31 +0000 Subject: [PATCH 0480/2846] [pre-commit.ci] pre-commit autoupdate --- .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 24196151952..83a50be93c4 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.9.2 + rev: v1.10.0 hooks: - id: blacken-docs additional_dependencies: [black==20.8b1] From ddde3266c6ec0b198e9e2ae68bd1692baf329071 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 16:55:53 +0000 Subject: [PATCH 0481/2846] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_doctest.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index b63665349a1..1d33e737830 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -730,12 +730,11 @@ def test_unicode_doctest(self, pytester: Pytester): test_unicode_doctest=""" .. doctest:: - >>> print( - ... "Hi\\n\\nByé") + >>> print("Hi\\n\\nByé") Hi ... Byé - >>> 1/0 # Byé + >>> 1 / 0 # Byé 1 """ ) From 7a6ec5616d071e3b21f0ce1c07c66388b3cce81a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 8 Mar 2021 19:12:08 -0800 Subject: [PATCH 0482/2846] clean up mkdtemp usage Committed via https://github.com/asottile/all-repos --- doc/en/fixture.rst | 12 +++++------- testing/test_assertrewrite.py | 6 ++---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 028786f6523..50bc9ee8e34 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -2228,7 +2228,6 @@ file: # content of conftest.py import os - import shutil import tempfile import pytest @@ -2236,12 +2235,11 @@ file: @pytest.fixture def cleandir(): - old_cwd = os.getcwd() - newpath = tempfile.mkdtemp() - os.chdir(newpath) - yield - os.chdir(old_cwd) - shutil.rmtree(newpath) + with tempfile.TemporaryDirectory() as newpath: + old_cwd = os.getcwd() + os.chdir(newpath) + yield + os.chdir(old_cwd) and declare its use in a test module via a ``usefixtures`` marker: diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index cba03406e86..f7d9d62ef63 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1395,12 +1395,10 @@ def test_cwd_changed(self, pytester: Pytester, monkeypatch) -> None: **{ "test_setup_nonexisting_cwd.py": """\ import os - import shutil import tempfile - d = tempfile.mkdtemp() - os.chdir(d) - shutil.rmtree(d) + with tempfile.TemporaryDirectory() as d: + os.chdir(d) """, "test_test.py": """\ def test(): From dbed1ff68fce457c8dd94e2926dd296b0c1c5ca0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 9 Mar 2021 21:56:16 +0100 Subject: [PATCH 0483/2846] adopt main terminology in the configs ref pytest-dev/meta#8 --- .github/workflows/main.yml | 8 ++++---- CONTRIBUTING.rst | 4 ++-- README.rst | 10 +++++----- doc/en/development_guide.rst | 2 +- doc/en/funcarg_compare.rst | 2 +- doc/en/index.rst | 4 ++-- doc/en/license.rst | 2 +- testing/test_config.py | 2 +- tox.ini | 4 ++-- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a3ea24b7cb0..7872f978ae1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ name: main on: push: branches: - - master + - main - "[0-9]+.[0-9]+.x" tags: - "[0-9]+.[0-9]+.[0-9]+" @@ -11,7 +11,7 @@ on: pull_request: branches: - - master + - main - "[0-9]+.[0-9]+.x" jobs: @@ -56,7 +56,7 @@ jobs: - name: "windows-py37-pluggy" python: "3.7" os: windows-latest - tox_env: "py37-pluggymaster-xdist" + tox_env: "py37-pluggymain-xdist" - name: "windows-py38" python: "3.8" os: windows-latest @@ -75,7 +75,7 @@ jobs: - name: "ubuntu-py37-pluggy" python: "3.7" os: ubuntu-latest - tox_env: "py37-pluggymaster-xdist" + tox_env: "py37-pluggymain-xdist" - name: "ubuntu-py37-freeze" python: "3.7" os: ubuntu-latest diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 054f809a818..ba783d5c106 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -236,7 +236,7 @@ Here is a simple overview, with pytest-specific bits: $ cd pytest # now, create your own branch off "master": - $ git checkout -b your-bugfix-branch-name master + $ git checkout -b your-bugfix-branch-name main Given we have "major.minor.micro" version numbers, bug fixes will usually be released in micro releases whereas features will be released in @@ -318,7 +318,7 @@ Here is a simple overview, with pytest-specific bits: compare: your-branch-name base-fork: pytest-dev/pytest - base: master + base: main Writing Tests diff --git a/README.rst b/README.rst index 159bf1d4f75..d0014e8bfff 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://github.com/pytest-dev/pytest/raw/master/doc/en/img/pytest_logo_curves.svg +.. image:: https://github.com/pytest-dev/pytest/raw/main/doc/en/img/pytest_logo_curves.svg :target: https://docs.pytest.org/en/stable/ :align: center :height: 200 @@ -16,14 +16,14 @@ .. image:: https://img.shields.io/pypi/pyversions/pytest.svg :target: https://pypi.org/project/pytest/ -.. image:: https://codecov.io/gh/pytest-dev/pytest/branch/master/graph/badge.svg +.. image:: https://codecov.io/gh/pytest-dev/pytest/branch/main/graph/badge.svg :target: https://codecov.io/gh/pytest-dev/pytest :alt: Code coverage Status .. image:: https://github.com/pytest-dev/pytest/workflows/main/badge.svg :target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Amain -.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/master.svg +.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg :target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/master :alt: pre-commit.ci status @@ -151,8 +151,8 @@ Tidelift will coordinate the fix and disclosure. License ------- -Copyright Holger Krekel and others, 2004-2020. +Copyright Holger Krekel and others, 2004-2021. Distributed under the terms of the `MIT`_ license, pytest is free and open source software. -.. _`MIT`: https://github.com/pytest-dev/pytest/blob/master/LICENSE +.. _`MIT`: https://github.com/pytest-dev/pytest/blob/main/LICENSE diff --git a/doc/en/development_guide.rst b/doc/en/development_guide.rst index 77076d4834e..3ee0ebbc239 100644 --- a/doc/en/development_guide.rst +++ b/doc/en/development_guide.rst @@ -4,4 +4,4 @@ Development Guide The contributing guidelines are to be found :ref:`here `. The release procedure for pytest is documented on -`GitHub `_. +`GitHub `_. diff --git a/doc/en/funcarg_compare.rst b/doc/en/funcarg_compare.rst index 5e2a050063c..0f44d1da05c 100644 --- a/doc/en/funcarg_compare.rst +++ b/doc/en/funcarg_compare.rst @@ -168,7 +168,7 @@ pytest for a long time offered a pytest_configure and a pytest_sessionstart hook which are often used to setup global resources. This suffers from several problems: -1. in distributed testing the master process would setup test resources +1. in distributed testing the managing process would setup test resources that are never needed because it only co-ordinates the test run activities of the worker processes. diff --git a/doc/en/index.rst b/doc/en/index.rst index 7f74534027c..cfe3a271ede 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -129,8 +129,8 @@ Tidelift will coordinate the fix and disclosure. License ------- -Copyright Holger Krekel and others, 2004-2020. +Copyright Holger Krekel and others, 2004-2021. Distributed under the terms of the `MIT`_ license, pytest is free and open source software. -.. _`MIT`: https://github.com/pytest-dev/pytest/blob/master/LICENSE +.. _`MIT`: https://github.com/pytest-dev/pytest/blob/main/LICENSE diff --git a/doc/en/license.rst b/doc/en/license.rst index c6c10bbf358..13765be1595 100644 --- a/doc/en/license.rst +++ b/doc/en/license.rst @@ -29,4 +29,4 @@ Distributed under the terms of the `MIT`_ license, pytest is free and open sourc OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -.. _`MIT`: https://github.com/pytest-dev/pytest/blob/master/LICENSE +.. _`MIT`: https://github.com/pytest-dev/pytest/blob/main/LICENSE diff --git a/testing/test_config.py b/testing/test_config.py index 8c1441e0680..b23264e1d79 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1780,7 +1780,7 @@ class DummyPlugin: ) def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None: if plugin == "debugging": - # Fixed in xdist master (after 1.27.0). + # Fixed in xdist (after 1.27.0). # https://github.com/pytest-dev/pytest-xdist/pull/422 try: import xdist # noqa: F401 diff --git a/tox.ini b/tox.ini index 908f56ea681..da5a634de8b 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ envlist = py38 py39 pypy3 - py37-{pexpect,xdist,unittestextras,numpy,pluggymaster} + py37-{pexpect,xdist,unittestextras,numpy,pluggymain} doctesting plugins py37-freeze @@ -45,7 +45,7 @@ deps = doctesting: PyYAML numpy: numpy>=1.19.4 pexpect: pexpect>=4.8.0 - pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master + pluggymain: pluggy @ git+https://github.com/pytest-dev/pluggy.git pygments>=2.7.2 unittestextras: twisted unittestextras: asynctest From 4a19533ba5f65f839b6bd0fe307a053049230796 Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Wed, 10 Mar 2021 18:27:30 +0000 Subject: [PATCH 0484/2846] Renamed Install to Getting started; moved notes to index --- doc/en/_templates/globaltoc.html | 2 +- doc/en/getting-started.rst | 18 +++++------------- doc/en/index.rst | 17 +++++++++++++---- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/doc/en/_templates/globaltoc.html b/doc/en/_templates/globaltoc.html index 5fc1ea13e5b..25666d8db78 100644 --- a/doc/en/_templates/globaltoc.html +++ b/doc/en/_templates/globaltoc.html @@ -2,7 +2,7 @@

    {{ _('Table Of Contents') }}