From 9275012ef789048998d1b87b9536bd7c2c063596 Mon Sep 17 00:00:00 2001 From: Daniil Galiev Date: Mon, 9 Sep 2019 00:27:55 +0500 Subject: [PATCH 01/95] fix bug with nonskipped first test in package --- AUTHORS | 1 + changelog/5830.bugfix.rst | 1 + src/_pytest/python.py | 16 ++++++++++------ testing/test_skipping.py | 23 +++++++++++++++++++++++ 4 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 changelog/5830.bugfix.rst diff --git a/AUTHORS b/AUTHORS index e11400c1f..d0e584f63 100644 --- a/AUTHORS +++ b/AUTHORS @@ -70,6 +70,7 @@ Daniel Hahler Daniel Nuri Daniel Wandschneider Danielle Jenkins +Daniil Galiev Dave Hunt David Díaz-Barquero David Mohr diff --git a/changelog/5830.bugfix.rst b/changelog/5830.bugfix.rst new file mode 100644 index 000000000..355790fd4 --- /dev/null +++ b/changelog/5830.bugfix.rst @@ -0,0 +1 @@ +Fix fail skipping the first test in package marked as ``skip`` diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 913a93bc0..97a3ac805 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -251,18 +251,21 @@ class PyobjMixin(PyobjContext): @property def obj(self): """Underlying Python object.""" + self._mount_obj_if_needed() + return self._obj + + @obj.setter + def obj(self, value): + self._obj = value + + def _mount_obj_if_needed(self): obj = getattr(self, "_obj", None) if obj is None: self._obj = obj = self._getobj() # XXX evil hack # used to avoid Instance collector marker duplication if self._ALLOW_MARKERS: - self.own_markers.extend(get_unpacked_marks(self.obj)) - return obj - - @obj.setter - def obj(self, value): - self._obj = value + self.own_markers.extend(get_unpacked_marks(obj)) def _getobj(self): """Gets the underlying Python object. May be overwritten by subclasses.""" @@ -628,6 +631,7 @@ class Package(Module): return path in self.session._initialpaths def collect(self): + self._mount_obj_if_needed() this_path = self.fspath.dirpath() init_module = this_path.join("__init__.py") if init_module.check(file=1) and path_matches_patterns( diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 8bba479f1..371c3a4db 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1162,3 +1162,26 @@ def test_importorskip(): match="^could not import 'doesnotexist': No module named .*", ): pytest.importorskip("doesnotexist") + + +def test_skip_package(testdir): + testdir.makepyfile( + __init__=""" + import pytest + pytestmark = pytest.mark.skip + """ + ) + + testdir.makepyfile( + """ + import pytest + def test_skip1(): + assert 0 + def test_skip2(): + assert 0 + """ + ) + + result = testdir.inline_run() + _, skipped, _ = result.listoutcomes() + assert len(skipped) == 2 From b94eb4cb7b455282144e99e9c0b21aa80596e993 Mon Sep 17 00:00:00 2001 From: Daniil Galiev Date: Thu, 12 Sep 2019 01:57:04 +0500 Subject: [PATCH 02/95] disable _ALLOW_MARKERS in module __init__.py --- src/_pytest/python.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 97a3ac805..a1f6d5067 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -548,6 +548,15 @@ class Module(nodes.File, PyCollector): return mod +class InitModule(Module): + _ALLOW_MARKERS = False + + def __repr__(self): + if type(self) == InitModule: + return "<{} {}>".format(Module.__name__, getattr(self, "name", None)) + return super().__repr__() + + class Package(Module): def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): session = parent.session @@ -637,7 +646,7 @@ class Package(Module): if init_module.check(file=1) and path_matches_patterns( init_module, self.config.getini("python_files") ): - yield Module(init_module, self) + yield InitModule(init_module, self) pkg_prefixes = set() for path in this_path.visit(rec=self._recurse, bf=True, sort=True): # We will visit our own __init__.py file, in which case we skip it. From 5cefcb20522e3d48282ba3f889162be5ef8e9040 Mon Sep 17 00:00:00 2001 From: Daniil Galiev Date: Sat, 28 Sep 2019 00:06:46 +0500 Subject: [PATCH 03/95] refactor disabling markers --- src/_pytest/python.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index a1f6d5067..45a4e66a9 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -432,6 +432,14 @@ class PyCollector(PyobjMixin, nodes.Collector): class Module(nodes.File, PyCollector): """ Collector for test classes and functions. """ + def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): + if fspath.basename == "__init__.py": + self._ALLOW_MARKERS = False + + nodes.FSCollector.__init__( + self, fspath, parent=parent, config=config, session=session, nodeid=nodeid + ) + def _getobj(self): return self._importtestmodule() @@ -548,15 +556,6 @@ class Module(nodes.File, PyCollector): return mod -class InitModule(Module): - _ALLOW_MARKERS = False - - def __repr__(self): - if type(self) == InitModule: - return "<{} {}>".format(Module.__name__, getattr(self, "name", None)) - return super().__repr__() - - class Package(Module): def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): session = parent.session @@ -646,7 +645,7 @@ class Package(Module): if init_module.check(file=1) and path_matches_patterns( init_module, self.config.getini("python_files") ): - yield InitModule(init_module, self) + yield Module(init_module, self) pkg_prefixes = set() for path in this_path.visit(rec=self._recurse, bf=True, sort=True): # We will visit our own __init__.py file, in which case we skip it. From 1cecdf66198049c7ca48356d8349e2f07732764c Mon Sep 17 00:00:00 2001 From: Steffen Schroeder Date: Sat, 13 Jul 2019 16:06:56 +0200 Subject: [PATCH 04/95] Added checklinks to tox and release.py --- doc/en/conf.py | 13 +++++++++++++ scripts/release.py | 7 +++++++ tox.ini | 9 +++++++++ 3 files changed, 29 insertions(+) diff --git a/doc/en/conf.py b/doc/en/conf.py index 1a6ef7ca8..c0c71cf05 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -112,6 +112,19 @@ pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] +# A list of regular expressions that match URIs that should not be checked when +# doing a linkcheck. +linkcheck_ignore = [ + "https://github.com/numpy/numpy/blob/master/doc/release/1.16.0-notes.rst#new-deprecations", + "https://blogs.msdn.microsoft.com/bharry/2017/06/28/testing-in-a-cloud-delivery-cadence/", + "http://pythontesting.net/framework/pytest-introduction/", + r"https://github.com/pytest-dev/pytest/issues/\d+", + r"https://github.com/pytest-dev/pytest/pull/\d+", +] + +# The number of worker threads to use when checking links (default=5). +linkcheck_workers = 5 + # -- Options for HTML output --------------------------------------------------- diff --git a/scripts/release.py b/scripts/release.py index 5009df359..884d9bfb1 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -79,12 +79,19 @@ def fix_formatting(): call(["pre-commit", "run", "--all-files"]) +def check_links(): + """Runs sphinx-build to check links""" + print(f"{Fore.CYAN}[generate.check_links] {Fore.RESET}Checking links") + check_call(["tox", "-e", "docs-checklinks"]) + + def pre_release(version): """Generates new docs, release announcements and creates a local tag.""" announce(version) regen() changelog(version, write_out=True) fix_formatting() + check_links() msg = "Preparing release version {}".format(version) check_call(["git", "commit", "-a", "-m", msg]) diff --git a/tox.ini b/tox.ini index b03941657..d1a34c264 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ envlist = doctesting py37-freeze docs + docs-checklinks [testenv] commands = @@ -66,6 +67,14 @@ deps = -r{toxinidir}/doc/en/requirements.txt commands = sphinx-build -W -b html . _build +[testenv:docs-checklinks] +basepython = python3 +usedevelop = True +changedir = doc/en +deps = -r{toxinidir}/doc/en/requirements.txt +commands = + sphinx-build -W --keep-going -b linkcheck . _build + [testenv:doctesting] basepython = python3 skipsdist = True From ceeb7bd0851e87b47bd54c88e3a8d9f04470baad Mon Sep 17 00:00:00 2001 From: Steffen Schroeder Date: Tue, 27 Aug 2019 21:00:48 +0200 Subject: [PATCH 05/95] Fixed broken links --- CHANGELOG.rst | 14 +++++++------- doc/en/announce/release-2.0.0.rst | 8 ++++---- doc/en/announce/release-2.0.1.rst | 2 +- doc/en/announce/release-2.2.0.rst | 6 +++--- doc/en/announce/release-2.3.0.rst | 10 +++++----- doc/en/announce/release-2.3.4.rst | 2 +- doc/en/announce/release-2.4.0.rst | 2 +- doc/en/announce/release-2.7.0.rst | 2 +- doc/en/announce/release-2.9.0.rst | 2 +- doc/en/fixture.rst | 6 +++--- doc/en/projects.rst | 1 - doc/en/talks.rst | 2 +- tox.ini | 2 +- 13 files changed, 29 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 375b5dabf..aa90006f1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3462,7 +3462,7 @@ Deprecations and Removals - ``pytest.approx`` no longer supports ``>``, ``>=``, ``<`` and ``<=`` operators to avoid surprising/inconsistent behavior. See `the approx docs - `_ for more + `_ for more information. (`#2003 `_) - All old-style specific behavior in current classes in the pytest's API is @@ -4819,7 +4819,7 @@ time or change existing behaviors in order to make them less surprising/more use * Fix (`#1422`_): junit record_xml_property doesn't allow multiple records with same name. -.. _`traceback style docs`: https://pytest.org/latest/usage.html#modifying-python-traceback-printing +.. _`traceback style docs`: https://pytest.org/en/latest/usage.html#modifying-python-traceback-printing .. _#1609: https://github.com/pytest-dev/pytest/issues/1609 .. _#1422: https://github.com/pytest-dev/pytest/issues/1422 @@ -5337,7 +5337,7 @@ time or change existing behaviors in order to make them less surprising/more use - add ability to set command line options by environment variable PYTEST_ADDOPTS. - added documentation on the new pytest-dev teams on bitbucket and - github. See https://pytest.org/latest/contributing.html . + github. See https://pytest.org/en/latest/contributing.html . Thanks to Anatoly for pushing and initial work on this. - fix issue650: new option ``--docttest-ignore-import-errors`` which @@ -6078,7 +6078,7 @@ Bug fixes: - yielded test functions will now have autouse-fixtures active but cannot accept fixtures as funcargs - it's anyway recommended to rather use the post-2.0 parametrize features instead of yield, see: - http://pytest.org/latest/example/parametrize.html + http://pytest.org/en/latest/example/parametrize.html - fix autouse-issue where autouse-fixtures would not be discovered if defined in an a/conftest.py file and tests in a/tests/test_some.py - fix issue226 - LIFO ordering for fixture teardowns @@ -6211,7 +6211,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/latest/faq.html +- fix issue159: improve http://pytest.org/en/latest/faq.html especially with respect to the "magic" history, also mention pytest-django, trial and unittest integration. @@ -6324,7 +6324,7 @@ Bug fixes: or through plugin hooks. Also introduce a "--strict" option which will treat unregistered markers as errors allowing to avoid typos and maintain a well described set of markers - for your test suite. See exaples at http://pytest.org/latest/mark.html + for your test suite. See exaples at http://pytest.org/en/latest/mark.html and its links. - issue50: introduce "-m marker" option to select tests based on markers (this is a stricter and more predictable version of '-k' in that "-m" @@ -6507,7 +6507,7 @@ Bug fixes: - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output - introduce a mechanism to prevent/unregister plugins from the - command line, see http://pytest.org/plugins.html#cmdunregister + command line, see http://pytest.org/en/latest/plugins.html#cmdunregister - activate resultlog plugin by default - fix regression wrt yielded tests which due to the collection-before-running semantics were not diff --git a/doc/en/announce/release-2.0.0.rst b/doc/en/announce/release-2.0.0.rst index af745fc59..d9d90c09a 100644 --- a/doc/en/announce/release-2.0.0.rst +++ b/doc/en/announce/release-2.0.0.rst @@ -7,7 +7,7 @@ see below for summary and detailed lists. A lot of long-deprecated code has been removed, resulting in a much smaller and cleaner implementation. See the new docs with examples here: - http://pytest.org/2.0.0/index.html + http://pytest.org/en/latest/index.html A note on packaging: pytest used to part of the "py" distribution up until version py-1.3.4 but this has changed now: pytest-2.0.0 only @@ -36,12 +36,12 @@ New Features import pytest ; pytest.main(arglist, pluginlist) - see http://pytest.org/2.0.0/usage.html for details. + see http://pytest.org/en/latest/usage.html for details. - new and better reporting information in assert expressions if comparing lists, sequences or strings. - see http://pytest.org/2.0.0/assert.html#newreport + see http://pytest.org/en/latest/assert.html#newreport - new configuration through ini-files (setup.cfg or tox.ini recognized), for example:: @@ -50,7 +50,7 @@ New Features norecursedirs = .hg data* # don't ever recurse in such dirs addopts = -x --pyargs # add these command line options by default - see http://pytest.org/2.0.0/customize.html + see http://pytest.org/en/latest/customize.html - improved standard unittest support. In general py.test should now better be able to run custom unittest.TestCases like twisted trial diff --git a/doc/en/announce/release-2.0.1.rst b/doc/en/announce/release-2.0.1.rst index 2f41ef943..f86537e1d 100644 --- a/doc/en/announce/release-2.0.1.rst +++ b/doc/en/announce/release-2.0.1.rst @@ -57,7 +57,7 @@ Changes between 2.0.0 and 2.0.1 - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output - introduce a mechanism to prevent/unregister plugins from the - command line, see http://pytest.org/latest/plugins.html#cmdunregister + command line, see http://pytest.org/en/latest/plugins.html#cmdunregister - activate resultlog plugin by default - fix regression wrt yielded tests which due to the collection-before-running semantics were not diff --git a/doc/en/announce/release-2.2.0.rst b/doc/en/announce/release-2.2.0.rst index 20bfe0a19..79e4dfd15 100644 --- a/doc/en/announce/release-2.2.0.rst +++ b/doc/en/announce/release-2.2.0.rst @@ -9,7 +9,7 @@ with these improvements: - new @pytest.mark.parametrize decorator to run tests with different arguments - new metafunc.parametrize() API for parametrizing arguments independently - - see examples at http://pytest.org/latest/example/parametrize.html + - see examples at http://pytest.org/en/latest/example/parametrize.html - NOTE that parametrize() related APIs are still a bit experimental and might change in future releases. @@ -18,7 +18,7 @@ with these improvements: - "-m markexpr" option for selecting tests according to their mark - a new "markers" ini-variable for registering test markers for your project - the new "--strict" bails out with an error if using unregistered markers. - - see examples at http://pytest.org/latest/example/markers.html + - see examples at http://pytest.org/en/latest/example/markers.html * duration profiling: new "--duration=N" option showing the N slowest test execution or setup/teardown calls. This is most useful if you want to @@ -78,7 +78,7 @@ Changes between 2.1.3 and 2.2.0 or through plugin hooks. Also introduce a "--strict" option which will treat unregistered markers as errors allowing to avoid typos and maintain a well described set of markers - for your test suite. See examples at http://pytest.org/latest/mark.html + for your test suite. See examples at http://pytest.org/en/latest/mark.html and its links. - issue50: introduce "-m marker" option to select tests based on markers (this is a stricter and more predictable version of "-k" in that "-m" diff --git a/doc/en/announce/release-2.3.0.rst b/doc/en/announce/release-2.3.0.rst index 061aa025c..5fb253670 100644 --- a/doc/en/announce/release-2.3.0.rst +++ b/doc/en/announce/release-2.3.0.rst @@ -13,12 +13,12 @@ re-useable fixture design. For detailed info and tutorial-style examples, see: - http://pytest.org/latest/fixture.html + http://pytest.org/en/latest/fixture.html Moreover, there is now support for using pytest fixtures/funcargs with unittest-style suites, see here for examples: - http://pytest.org/latest/unittest.html + http://pytest.org/en/latest/unittest.html Besides, more unittest-test suites are now expected to "simply work" with pytest. @@ -29,11 +29,11 @@ pytest-2.2.4. If you are interested in the precise reasoning (including examples) of the pytest-2.3 fixture evolution, please consult -http://pytest.org/latest/funcarg_compare.html +http://pytest.org/en/latest/funcarg_compare.html For general info on installation and getting started: - http://pytest.org/latest/getting-started.html + http://pytest.org/en/latest/getting-started.html Docs and PDF access as usual at: @@ -94,7 +94,7 @@ Changes between 2.2.4 and 2.3.0 - pluginmanager.register(...) now raises ValueError if the plugin has been already registered or the name is taken -- fix issue159: improve http://pytest.org/latest/faq.html +- fix issue159: improve http://pytest.org/en/latest/faq.html especially with respect to the "magic" history, also mention pytest-django, trial and unittest integration. diff --git a/doc/en/announce/release-2.3.4.rst b/doc/en/announce/release-2.3.4.rst index e2e8cb143..b00430f94 100644 --- a/doc/en/announce/release-2.3.4.rst +++ b/doc/en/announce/release-2.3.4.rst @@ -16,7 +16,7 @@ comes with the following fixes and features: - yielded test functions will now have autouse-fixtures active but cannot accept fixtures as funcargs - it's anyway recommended to rather use the post-2.0 parametrize features instead of yield, see: - http://pytest.org/latest/example/parametrize.html + http://pytest.org/en/latest/example/parametrize.html - fix autouse-issue where autouse-fixtures would not be discovered if defined in an a/conftest.py file and tests in a/tests/test_some.py - fix issue226 - LIFO ordering for fixture teardowns diff --git a/doc/en/announce/release-2.4.0.rst b/doc/en/announce/release-2.4.0.rst index 1b0168841..6cd14bc2d 100644 --- a/doc/en/announce/release-2.4.0.rst +++ b/doc/en/announce/release-2.4.0.rst @@ -7,7 +7,7 @@ from a few supposedly very minor incompatibilities. See below for a full list of details. A few feature highlights: - new yield-style fixtures `pytest.yield_fixture - `_, allowing to use + `_, allowing to use existing with-style context managers in fixture functions. - improved pdb support: ``import pdb ; pdb.set_trace()`` now works diff --git a/doc/en/announce/release-2.7.0.rst b/doc/en/announce/release-2.7.0.rst index d63081edb..8952ff50f 100644 --- a/doc/en/announce/release-2.7.0.rst +++ b/doc/en/announce/release-2.7.0.rst @@ -52,7 +52,7 @@ holger krekel - add ability to set command line options by environment variable PYTEST_ADDOPTS. - added documentation on the new pytest-dev teams on bitbucket and - github. See https://pytest.org/latest/contributing.html . + github. See https://pytest.org/en/latest/contributing.html . Thanks to Anatoly for pushing and initial work on this. - fix issue650: new option ``--docttest-ignore-import-errors`` which diff --git a/doc/en/announce/release-2.9.0.rst b/doc/en/announce/release-2.9.0.rst index 05d9a394f..9e0856690 100644 --- a/doc/en/announce/release-2.9.0.rst +++ b/doc/en/announce/release-2.9.0.rst @@ -131,7 +131,7 @@ The py.test Development Team with same name. -.. _`traceback style docs`: https://pytest.org/latest/usage.html#modifying-python-traceback-printing +.. _`traceback style docs`: https://pytest.org/en/latest/usage.html#modifying-python-traceback-printing .. _#1422: https://github.com/pytest-dev/pytest/issues/1422 .. _#1379: https://github.com/pytest-dev/pytest/issues/1379 diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 6e1e554bf..08305a5cc 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -9,9 +9,9 @@ pytest fixtures: explicit, modular, scalable -.. _`xUnit`: http://en.wikipedia.org/wiki/XUnit -.. _`purpose of test fixtures`: http://en.wikipedia.org/wiki/Test_fixture#Software -.. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection +.. _`xUnit`: https://en.wikipedia.org/wiki/XUnit +.. _`purpose of test fixtures`: https://en.wikipedia.org/wiki/Test_fixture#Software +.. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection The `purpose of test fixtures`_ is to provide a fixed baseline upon which tests can reliably and repeatedly execute. pytest fixtures diff --git a/doc/en/projects.rst b/doc/en/projects.rst index 751d0abe9..2febcd24b 100644 --- a/doc/en/projects.rst +++ b/doc/en/projects.rst @@ -73,7 +73,6 @@ Some organisations using pytest * `Square Kilometre Array, Cape Town `_ * `Some Mozilla QA people `_ use pytest to distribute their Selenium tests -* `Tandberg `_ * `Shootq `_ * `Stups department of Heinrich Heine University Duesseldorf `_ * cellzome diff --git a/doc/en/talks.rst b/doc/en/talks.rst index 04eb97c7f..16bdd665b 100644 --- a/doc/en/talks.rst +++ b/doc/en/talks.rst @@ -64,7 +64,7 @@ Talks and blog postings - `pytest introduction from Brian Okken (January 2013) `_ -- pycon australia 2012 pytest talk from Brianna Laugher (`video `_, `slides `_, `code `_) +- pycon australia 2012 pytest talk from Brianna Laugher (`video `_, `slides `_, `code `_) - `pycon 2012 US talk video from Holger Krekel `_ - `monkey patching done right`_ (blog post, consult `monkeypatch plugin`_ for up-to-date API) diff --git a/tox.ini b/tox.ini index d1a34c264..2506201fd 100644 --- a/tox.ini +++ b/tox.ini @@ -73,7 +73,7 @@ usedevelop = True changedir = doc/en deps = -r{toxinidir}/doc/en/requirements.txt commands = - sphinx-build -W --keep-going -b linkcheck . _build + sphinx-build -W -q --keep-going -b linkcheck . _build [testenv:doctesting] basepython = python3 From cc503c1821e709376d9a05ee4f6459e4e4f41a0c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 8 Nov 2019 04:04:23 +0100 Subject: [PATCH 06/95] _compare_eq_iterable: use AlwaysDispatchingPrettyPrinter This fixes/removes the previous hack of re-trying with minimum width, which fails short when it splits strings. This inherits from `pprint.PrettyPrinter` to override `_format` in a minimal way to always dispatch, regardless of the given width. Code ref: https://github.com/python/cpython/blob/5c0c325453a175350e3c18ebb10cc10c37f9595c/Lib/pprint.py#L170-L178 --- src/_pytest/assertion/util.py | 32 +++++++++++++++++++++++--------- testing/test_assertion.py | 33 +++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 7521c08e4..4af35bd57 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -28,6 +28,27 @@ _reprcompare = None # type: Optional[Callable[[str, object, object], Optional[s _assertion_pass = None # type: Optional[Callable[[int, str, str], None]] +class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter): + """PrettyPrinter that always dispatches (regardless of width).""" + + def _format(self, object, stream, indent, allowance, context, level): + p = self._dispatch.get(type(object).__repr__, None) + + objid = id(object) + if objid in context or p is None: + return super()._format(object, stream, indent, allowance, context, level) + + context[objid] = 1 + p(self, object, stream, indent, allowance, context, level + 1) + del context[objid] + + +def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False): + return AlwaysDispatchingPrettyPrinter( + indent=1, width=80, depth=None, compact=False + ).pformat(object) + + def format_explanation(explanation: str) -> str: """This formats an explanation @@ -270,15 +291,8 @@ def _compare_eq_iterable( lines_left = len(left_formatting) lines_right = len(right_formatting) if lines_left != lines_right: - if lines_left > lines_right: - max_width = min(len(x) for x in left_formatting) - else: - max_width = min(len(x) for x in right_formatting) - - right_formatting = pprint.pformat(right, width=max_width).splitlines() - lines_right = len(right_formatting) - left_formatting = pprint.pformat(left, width=max_width).splitlines() - lines_left = len(left_formatting) + left_formatting = _pformat_dispatch(left).splitlines() + right_formatting = _pformat_dispatch(right).splitlines() if lines_left > 1 or lines_right > 1: _surrounding_parens_on_own_lines(left_formatting) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index aac21a0df..6c700567a 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -462,6 +462,29 @@ class TestAssert_reprcompare: " ]", ] + def test_list_dont_wrap_strings(self): + long_a = "a" * 10 + l1 = ["a"] + [long_a for _ in range(0, 7)] + l2 = ["should not get wrapped"] + diff = callequal(l1, l2, verbose=True) + assert diff == [ + "['a', 'aaaaaa...aaaaaaa', ...] == ['should not get wrapped']", + "At index 0 diff: 'a' != 'should not get wrapped'", + "Left contains 7 more items, first extra item: 'aaaaaaaaaa'", + "Full diff:", + " [", + "+ 'should not get wrapped',", + "- 'a',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + " ]", + ] + def test_dict_wrap(self): d1 = {"common": 1, "env": {"env1": 1}} d2 = {"common": 1, "env": {"env1": 1, "env2": 2}} @@ -479,22 +502,20 @@ class TestAssert_reprcompare: ] long_a = "a" * 80 - sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped"}} + sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 2}} d1 = {"env": {"sub": sub}} d2 = {"env": {"sub": sub}, "new": 1} diff = callequal(d1, d2, verbose=True) assert diff == [ - "{'env': {'sub...s wrapped'}}}} == {'env': {'sub...}}}, 'new': 1}", + "{'env': {'sub... wrapped '}}}} == {'env': {'sub...}}}, 'new': 1}", "Omitting 1 identical items, use -vv to show", "Right contains 1 more item:", "{'new': 1}", "Full diff:", " {", " 'env': {'sub': {'long_a': '" + long_a + "',", - " 'sub1': {'long_a': 'substring '", - " 'that '", - " 'gets '", - " 'wrapped'}}},", + " 'sub1': {'long_a': 'substring that gets wrapped substring '", + " 'that gets wrapped '}}},", "+ 'new': 1,", " }", ] From 41604eeb3e70c35103b5260b20419a817a0fe6ac Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 8 Nov 2019 22:38:50 +0100 Subject: [PATCH 07/95] setup.cfg: fix check-manifest ignore [ci skip] --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 60e866562..0c0cb4861 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,7 +57,7 @@ upload-dir = doc/en/build/html [check-manifest] ignore = - _pytest/_version.py + src/_pytest/_version.py [devpi:upload] formats = sdist.tgz,bdist_wheel From 42a46ea78617b8e210636bc2f9d9bf06435b60fa Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 6 Nov 2019 10:24:09 +0200 Subject: [PATCH 08/95] Add a @cached_property implementation This is a useful utility to abstract the caching property idiom. It is in compat.py since eventually it will be replaced by functools.cached_property. Fixes #6131. --- src/_pytest/compat.py | 41 +++++++++++++++++++++++++++++++++++++++++ src/_pytest/nodes.py | 19 ++++++------------- testing/test_compat.py | 21 +++++++++++++++++++++ 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 83947d3eb..5e066c18e 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -10,7 +10,11 @@ import sys from contextlib import contextmanager from inspect import Parameter from inspect import signature +from typing import Callable +from typing import Generic +from typing import Optional from typing import overload +from typing import TypeVar import attr import py @@ -20,6 +24,13 @@ from _pytest._io.saferepr import saferepr from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME +if False: # TYPE_CHECKING + from typing import Type # noqa: F401 (used in type string) + + +_T = TypeVar("_T") +_S = TypeVar("_S") + NOTSET = object() @@ -374,3 +385,33 @@ if getattr(attr, "__version_info__", ()) >= (19, 2): ATTRS_EQ_FIELD = "eq" else: ATTRS_EQ_FIELD = "cmp" + + +if sys.version_info >= (3, 8): + # TODO: Remove type ignore on next mypy update. + # https://github.com/python/typeshed/commit/add0b5e930a1db16560fde45a3b710eefc625709 + from functools import cached_property # type: ignore +else: + + class cached_property(Generic[_S, _T]): + __slots__ = ("func", "__doc__") + + def __init__(self, func: Callable[[_S], _T]) -> None: + self.func = func + self.__doc__ = func.__doc__ + + @overload + def __get__( + self, instance: None, owner: Optional["Type[_S]"] = ... + ) -> "cached_property[_S, _T]": + raise NotImplementedError() + + @overload # noqa: F811 + def __get__(self, instance: _S, owner: Optional["Type[_S]"] = ...) -> _T: + raise NotImplementedError() + + def __get__(self, instance, owner=None): # noqa: F811 + if instance is None: + return self + value = instance.__dict__[self.func.__name__] = self.func(instance) + return value diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index eae783f16..737bc11b7 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -15,6 +15,7 @@ import _pytest._code from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprExceptionInfo +from _pytest.compat import cached_property from _pytest.compat import getfslineno from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError @@ -448,17 +449,9 @@ class Item(Node): def reportinfo(self) -> Tuple[str, Optional[int], str]: return self.fspath, None, "" - @property + @cached_property def location(self) -> Tuple[str, Optional[int], str]: - try: - return self._location - except AttributeError: - location = self.reportinfo() - fspath = self.session._node_location_to_relpath(location[0]) - assert type(location[2]) is str - self._location = ( - fspath, - location[1], - location[2], - ) # type: Tuple[str, Optional[int], str] - return self._location + location = self.reportinfo() + fspath = self.session._node_location_to_relpath(location[0]) + assert type(location[2]) is str + return (fspath, location[1], location[2]) diff --git a/testing/test_compat.py b/testing/test_compat.py index 94dac439d..04d818b4e 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -4,6 +4,7 @@ from functools import wraps import pytest from _pytest.compat import _PytestWrapper +from _pytest.compat import cached_property from _pytest.compat import get_real_func from _pytest.compat import is_generator from _pytest.compat import safe_getattr @@ -178,3 +179,23 @@ def test_safe_isclass(): assert False, "Should be ignored" assert safe_isclass(CrappyClass()) is False + + +def test_cached_property() -> None: + ncalls = 0 + + class Class: + @cached_property + def prop(self) -> int: + nonlocal ncalls + ncalls += 1 + return ncalls + + c1 = Class() + assert ncalls == 0 + assert c1.prop == 1 + assert c1.prop == 1 + c2 = Class() + assert ncalls == 1 + assert c2.prop == 2 + assert c1.prop == 1 From 3ef8aa8173eb10c5a57fd38b4df7d3ba959e8b5f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 9 Nov 2019 22:43:43 +0100 Subject: [PATCH 09/95] A bit more typing around Node --- src/_pytest/nodes.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index eae783f16..0028e68dd 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -16,6 +16,7 @@ from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprExceptionInfo from _pytest.compat import getfslineno +from _pytest.config import Config from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureLookupErrorRepr @@ -78,11 +79,11 @@ class Node: def __init__( self, name, - parent=None, - config=None, + parent: Optional["Node"] = None, + config: Optional[Config] = None, session: Optional["Session"] = None, - fspath=None, - nodeid=None, + fspath: Optional[py.path.local] = None, + nodeid: Optional[str] = None, ) -> None: #: a unique name within the scope of the parent node self.name = name @@ -91,14 +92,20 @@ class Node: self.parent = parent #: the pytest config object - self.config = config or parent.config + if config: + self.config = config + else: + if not parent: + raise TypeError("config or parent must be provided") + self.config = parent.config #: the session this node is part of - if session is None: - assert parent.session is not None - self.session = parent.session - else: + if session: self.session = session + else: + if not parent: + raise TypeError("session or parent must be provided") + self.session = parent.session #: filesystem path where this node was collected from (can be None) self.fspath = fspath or getattr(parent, "fspath", None) @@ -119,6 +126,8 @@ class Node: assert "::()" not in nodeid self._nodeid = nodeid else: + if not self.parent: + raise TypeError("nodeid or parent must be provided") self._nodeid = self.parent.nodeid if self.name != "()": self._nodeid += "::" + self.name @@ -182,7 +191,7 @@ class Node: """ return list of all parent collectors up to self, starting from root of collection tree. """ chain = [] - item = self + item = self # type: Optional[Node] while item is not None: chain.append(item) item = item.parent @@ -263,7 +272,7 @@ class Node: def getparent(self, cls): """ get the next parent node (including ourself) which is an instance of the given class""" - current = self + current = self # type: Optional[Node] while current and not isinstance(current, cls): current = current.parent return current From 4c7d971f13a3912a23398b3eea562178d5d5ec76 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 12 Nov 2019 14:29:50 +0100 Subject: [PATCH 10/95] filterwarnings: ignore DeprecationWarning from nose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This comes via hypothesis: ``` % COLUMNS=80 p testing/python/metafunc.py::TestMetafunc::test_idval_hypothesis -vv --tb=short ============================= test session starts ============================== platform linux -- Python 3.7.4, pytest-3.1.4.dev721+g3367bf03b.d20191112, py-1.8.1.dev11+g34f716fe, pluggy-0.13.1.dev8+ga5130ac.d20191103 -- …/Vcs/pytest/.venv/bin/python cachedir: .pytest_cache hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('…/Vcs/pytest/.hypothesis/examples') rootdir: …/Vcs/pytest, inifile: tox.ini plugins: forked-1.1.3, hypothesis-4.44.1, cov-2.8.1, coverage-pytest-plugin-0.1, enhancements-0.0.5.dev1-gf361636-dirty, xdist-1.30.0 collected 1 item testing/python/metafunc.py::TestMetafunc::test_idval_hypothesis FAILED [100%] =================================== FAILURES =================================== ______________________ TestMetafunc.test_idval_hypothesis ______________________ .venv/lib/python3.7/site-packages/hypothesis/core.py:588: in evaluate_test_data result = self.execute(data) .venv/lib/python3.7/site-packages/hypothesis/core.py:553: in execute result = self.test_runner(data, run) .venv/lib/python3.7/site-packages/hypothesis/executors.py:56: in default_new_style_executor return function(data) .venv/lib/python3.7/site-packages/hypothesis/core.py:536: in run args, kwargs = data.draw(self.search_strategy) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:857: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/core.py:223: in do_draw return self.mapped_strategy.do_draw(data) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/collections.py:60: in do_draw return tuple(data.draw(e) for e in self.element_strategies) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/collections.py:60: in return tuple(data.draw(e) for e in self.element_strategies) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/strategies.py:570: in do_draw result = self.pack(data.draw(self.mapped_strategy)) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/lazy.py:156: in do_draw return data.draw(self.wrapped_strategy) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/strategies.py:570: in do_draw result = self.pack(data.draw(self.mapped_strategy)) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/collections.py:60: in do_draw return tuple(data.draw(e) for e in self.element_strategies) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/collections.py:60: in return tuple(data.draw(e) for e in self.element_strategies) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/strategies.py:508: in do_draw return data.draw(self.element_strategies[i], label=self.branch_labels[i]) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/lazy.py:156: in do_draw return data.draw(self.wrapped_strategy) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/strategies.py:570: in do_draw result = self.pack(data.draw(self.mapped_strategy)) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/lazy.py:156: in do_draw return data.draw(self.wrapped_strategy) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/collections.py:120: in do_draw result.append(data.draw(self.element_strategy)) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/numbers.py:62: in do_draw return d.integer_range(data, self.start, self.end) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/utils.py:105: in integer_range probe = data.draw_bits(bits) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:974: in draw_bits self.__check_capacity(n_bytes) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:1019: in __check_capacity self.mark_overrun() .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:1036: in mark_overrun self.conclude_test(Status.OVERRUN) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:1027: in conclude_test raise StopTest(self.testcounter) E hypothesis.errors.StopTest: 0 During handling of the above exception, another exception occurred: testing/python/metafunc.py:195: in test_idval_hypothesis @hypothesis.settings( .venv/lib/python3.7/site-packages/nose/__init__.py:1: in from nose.core import collector, main, run, run_exit, runmodule .venv/lib/python3.7/site-packages/nose/core.py:12: in from nose.loader import defaultTestLoader .venv/lib/python3.7/site-packages/nose/loader.py:21: in from nose.importer import Importer, add_path, remove_path .venv/lib/python3.7/site-packages/nose/importer.py:12: in from imp import find_module, load_module, acquire_model1, release_model1 /usr/lib/python3.7/imp.py:33: in DeprecationWarning, stacklevel=2) E DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses ---------------------------------- Hypothesis ---------------------------------- You can add @seed(198901559535749756451579900660745168041) to this test or run pytest with --hypothesis-seed=198901559535749756451579900660745168041 to reproduce this failure. =============================== warnings summary =============================== testing/python/metafunc.py::TestMetafunc::test_idval_hypothesis …/Vcs/pytest/.venv/lib/python3.7/site-packages/unittest2/compatibility.py:143: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working class ChainMap(collections.MutableMapping): -- Docs: https://docs.pytest.org/en/latest/warnings.html =========================== short test summary info ============================ FAILED testing/python/metafunc.py::TestMetafunc::test_idval_hypothesis - Depr... ``` --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 6bdc5d73f..863a30bb3 100644 --- a/tox.ini +++ b/tox.ini @@ -138,6 +138,7 @@ xfail_strict=true filterwarnings = error default:Using or importing the ABCs:DeprecationWarning:unittest2.* + default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.* ignore:Module already imported so cannot be rewritten:pytest.PytestWarning # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8). ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest)) From fc1c015c6b79ead0d76793cf0b7ae155b365f3a6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 12 Nov 2019 14:42:10 +0100 Subject: [PATCH 11/95] tests: remove test_nested_marks (xfail) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It currently fails with a TypeError, and was not updated since 2013 - therefore it can be assumed that it is not important to support it. ``` ____________________ ERROR collecting test_nested_marks.py _____________________ …/Vcs/pluggy/src/pluggy/hooks.py:286: in __call__ return self._hookexec(self, self.get_hookimpls(), kwargs) …/Vcs/pluggy/src/pluggy/manager.py:93: in _hookexec return self._inner_hookexec(hook, methods, kwargs) …/Vcs/pluggy/src/pluggy/manager.py:337: in traced_hookexec return outcome.get_result() …/Vcs/pluggy/src/pluggy/manager.py:335: in outcome = _Result.from_call(lambda: oldcall(hook, hook_impls, kwargs)) …/Vcs/pluggy/src/pluggy/manager.py:87: in firstresult=hook.spec.opts.get("firstresult") if hook.spec else False, …/Vcs/pytest/src/_pytest/python.py:235: in pytest_pycollect_makeitem res = list(collector._genfunctions(name, obj)) …/Vcs/pytest/src/_pytest/python.py:404: in _genfunctions self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) …/Vcs/pluggy/src/pluggy/hooks.py:324: in call_extra return self(**kwargs) …/Vcs/pluggy/src/pluggy/hooks.py:286: in __call__ return self._hookexec(self, self.get_hookimpls(), kwargs) …/Vcs/pluggy/src/pluggy/manager.py:93: in _hookexec return self._inner_hookexec(hook, methods, kwargs) …/Vcs/pluggy/src/pluggy/manager.py:337: in traced_hookexec return outcome.get_result() …/Vcs/pluggy/src/pluggy/manager.py:335: in outcome = _Result.from_call(lambda: oldcall(hook, hook_impls, kwargs)) …/Vcs/pluggy/src/pluggy/manager.py:87: in firstresult=hook.spec.opts.get("firstresult") if hook.spec else False, …/Vcs/pytest/src/_pytest/python.py:130: in pytest_generate_tests metafunc.parametrize(*marker.args, **marker.kwargs) …/Vcs/pytest/src/_pytest/python.py:965: in parametrize function_definition=self.definition, …/Vcs/pytest/src/_pytest/mark/structures.py:111: in _for_parametrize if len(param.values) != len(argnames): E TypeError: object of type 'MarkDecorator' has no len() !!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!! ``` --- testing/python/metafunc.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 5becb0f8c..15c146e90 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1538,27 +1538,6 @@ class TestMarkersWithParametrization: assert len(skipped) == 0 assert len(fail) == 0 - @pytest.mark.xfail(reason="is this important to support??") - def test_nested_marks(self, testdir): - s = """ - import pytest - mastermark = pytest.mark.foo(pytest.mark.bar) - - @pytest.mark.parametrize(("n", "expected"), [ - (1, 2), - mastermark((1, 3)), - (2, 3), - ]) - def test_increment(n, expected): - assert n + 1 == expected - """ - items = testdir.getitems(s) - assert len(items) == 3 - for mark in ["foo", "bar"]: - assert mark not in items[0].keywords - assert mark in items[1].keywords - assert mark not in items[2].keywords - def test_simple_xfail(self, testdir): s = """ import pytest From 86e9ae39f0ff0f16179d3e1d25674445432cfaef Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 12 Nov 2019 15:28:36 +0100 Subject: [PATCH 12/95] pytester: assert_outcomes: use/set __tracebackhide__ --- src/_pytest/pytester.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 6b45e077b..9f3b4d8ab 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -441,8 +441,9 @@ class RunResult: ) -> 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 + d = self.parseoutcomes() obtained = { "passed": d.get("passed", 0), From 0b40749c9826d67ff44044608778d6ccd763e95a Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Tue, 12 Nov 2019 12:32:05 -0800 Subject: [PATCH 13/95] Delineate syntactically that the 'match' argument to 'raises' is keyword-only --- 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 9c3a4c731..4391cfcb1 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -59,7 +59,7 @@ pytest.raises **Tutorial**: :ref:`assertraises`. -.. autofunction:: pytest.raises(expected_exception: Exception, [match]) +.. autofunction:: pytest.raises(expected_exception: Exception [, *, match]) :with: excinfo pytest.deprecated_call From 6ddf7c3d42efcf01c8641893e7331f9e33e6877d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 13 Nov 2019 02:13:35 +0100 Subject: [PATCH 14/95] pytester: Hookrecorder: improve assertoutcome Before: def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: realpassed, realskipped, realfailed = self.listoutcomes() assert passed == len(realpassed) > assert skipped == len(realskipped) E assert 1 == 0 E + where 0 = len([]) After: > reprec = testdir.inline_run(testpath, "-s") E AssertionError: ([], [], []) E assert {'failed': 1, 'passed': 0, 'skipped': 0} == {'failed': 0, 'passed': 0, 'skipped': 1} --- changelog/6176.improvement.rst | 1 + src/_pytest/pytester.py | 15 +++++++++++---- testing/test_assertion.py | 9 ++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 changelog/6176.improvement.rst diff --git a/changelog/6176.improvement.rst b/changelog/6176.improvement.rst new file mode 100644 index 000000000..39787da2e --- /dev/null +++ b/changelog/6176.improvement.rst @@ -0,0 +1 @@ +Improved failure reporting with pytester's ``Hookrecorder.assertoutcome``. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 9f3b4d8ab..ca780a9f5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -332,10 +332,17 @@ class HookRecorder: return [len(x) for x in self.listoutcomes()] def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: - realpassed, realskipped, realfailed = self.listoutcomes() - assert passed == len(realpassed) - assert skipped == len(realskipped) - assert failed == len(realfailed) + __tracebackhide__ = True + + 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 def clear(self) -> None: self.calls[:] = [] diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 6c700567a..e4d68ff8c 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -70,7 +70,14 @@ class TestImportHookInstallation: """ ) result = testdir.runpytest_subprocess() - result.stdout.fnmatch_lines(["*assert 1 == 0*"]) + result.stdout.fnmatch_lines( + [ + "E * AssertionError: ([[][]], [[][]], [[][]])*", + "E * assert" + " {'failed': 1, 'passed': 0, 'skipped': 0} ==" + " {'failed': 0, 'passed': 1, 'skipped': 0}", + ] + ) @pytest.mark.parametrize("mode", ["plain", "rewrite"]) def test_pytest_plugins_rewrite(self, testdir, mode): From b06f33f4748ee1bf928c01b4e12d5506a4e05870 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 13 Nov 2019 15:55:11 +0100 Subject: [PATCH 15/95] terminal: report ``session.shouldfail`` reason (``-x``) Via https://github.com/blueyed/pytest/pull/108. --- changelog/6181.improvement.rst | 1 + doc/en/usage.rst | 4 ++-- src/_pytest/terminal.py | 6 +++++- testing/test_collection.py | 13 +++++++++---- testing/test_terminal.py | 26 +++++++++++++++++++++++++- 5 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 changelog/6181.improvement.rst diff --git a/changelog/6181.improvement.rst b/changelog/6181.improvement.rst new file mode 100644 index 000000000..0960f6203 --- /dev/null +++ b/changelog/6181.improvement.rst @@ -0,0 +1 @@ +The reason for a stopped session, e.g. with ``--maxfail`` / ``-x`` gets reported. diff --git a/doc/en/usage.rst b/doc/en/usage.rst index a23cf764a..3b5919363 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -66,8 +66,8 @@ To stop the testing process after the first (N) failures: .. code-block:: bash - pytest -x # stop after first failure - pytest --maxfail=2 # stop after two failures + pytest -x # stop after first failure + pytest --maxfail=2 # stop after two failures .. _select-tests: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 59f0fe0f3..14267b208 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -676,7 +676,7 @@ class TerminalReporter: self._tw.line("{}{}".format(indent + " ", line.strip())) @pytest.hookimpl(hookwrapper=True) - def pytest_sessionfinish(self, exitstatus): + def pytest_sessionfinish(self, session: Session, exitstatus: ExitCode): outcome = yield outcome.get_result() self._tw.line("") @@ -691,9 +691,13 @@ class TerminalReporter: self.config.hook.pytest_terminal_summary( terminalreporter=self, exitstatus=exitstatus, config=self.config ) + if session.shouldfail: + self.write_sep("!", session.shouldfail, red=True) if exitstatus == ExitCode.INTERRUPTED: self._report_keyboardinterrupt() del self._keyboardinterrupt_memo + elif session.shouldstop: + self.write_sep("!", session.shouldstop, red=True) self.summary_stats() @pytest.hookimpl(hookwrapper=True) diff --git a/testing/test_collection.py b/testing/test_collection.py index 83345d2c6..f18d36d24 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -852,11 +852,15 @@ def test_exit_on_collection_with_maxfail_smaller_than_n_errors(testdir): res = testdir.runpytest("--maxfail=1") assert res.ret == 1 - res.stdout.fnmatch_lines( - ["*ERROR collecting test_02_import_error.py*", "*No module named *asdfa*"] + [ + "collected 1 item / 1 error", + "*ERROR collecting test_02_import_error.py*", + "*No module named *asdfa*", + "*! stopping after 1 failures !*", + "*= 1 error in *", + ] ) - res.stdout.no_fnmatch_line("*test_03*") @@ -869,7 +873,6 @@ def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir): res = testdir.runpytest("--maxfail=4") assert res.ret == 2 - res.stdout.fnmatch_lines( [ "collected 2 items / 2 errors", @@ -877,6 +880,8 @@ def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir): "*No module named *asdfa*", "*ERROR collecting test_03_import_error.py*", "*No module named *asdfa*", + "*! Interrupted: 2 errors during collection !*", + "*= 2 errors in *", ] ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 1bec577b8..d31033197 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -963,7 +963,31 @@ class TestGenericReporting: ) result = testdir.runpytest("--maxfail=2", *option.args) result.stdout.fnmatch_lines( - ["*def test_1():*", "*def test_2():*", "*2 failed*"] + [ + "*def test_1():*", + "*def test_2():*", + "*! stopping after 2 failures !*", + "*2 failed*", + ] + ) + + def test_maxfailures_with_interrupted(self, testdir): + testdir.makepyfile( + """ + def test(request): + request.session.shouldstop = "session_interrupted" + assert 0 + """ + ) + result = testdir.runpytest("--maxfail=1", "-ra") + result.stdout.fnmatch_lines( + [ + "*= short test summary info =*", + "FAILED *", + "*! stopping after 1 failures !*", + "*! session_interrupted !*", + "*= 1 failed in*", + ] ) def test_tb_option(self, testdir, option): From b3bb60468331d49ff3eaa3241adb9fa070c90d19 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 23 Oct 2019 20:01:04 +0200 Subject: [PATCH 16/95] fix typo in _issue_warning_captured doc --- src/_pytest/warnings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 8fdb61c2b..8ac1ee225 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -138,7 +138,7 @@ def _issue_warning_captured(warning, hook, stacklevel): """ This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured - hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891. + hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891. :param warning: the warning instance. :param hook: the hook caller From 55bc084dcc783bda88a221a5437a515df3e7b2ae Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 31 Oct 2019 04:39:07 +0100 Subject: [PATCH 17/95] doc: s/_pytest.config.Parser/_pytest.config.argparsing.Parser/ --- src/_pytest/config/__init__.py | 2 +- src/_pytest/config/argparsing.py | 2 +- src/_pytest/hookspec.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 8e11b56e5..e94bec5d6 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -974,7 +974,7 @@ class Config: def getini(self, name: str): """ 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.Parser.addini>` + :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>` call (usually from a plugin), a ValueError is raised. """ try: return self._inicache[name] diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 9b526ff3e..4eec6be05 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -47,7 +47,7 @@ class Parser: The returned group object has an ``addoption`` method with the same signature as :py:func:`parser.addoption - <_pytest.config.Parser.addoption>` but will be shown in the + <_pytest.config.argparsing.Parser.addoption>` but will be shown in the respective group in the output of ``pytest. --help``. """ for group in self._groups: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 8b45c5f9b..03e060eb8 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -45,10 +45,10 @@ def pytest_addoption(parser, pluginmanager): files situated at the tests root directory due to how pytest :ref:`discovers plugins during startup `. - :arg _pytest.config.Parser parser: To add command line options, call - :py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`. + :arg _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.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 @@ -148,7 +148,7 @@ def pytest_load_initial_conftests(early_config, parser, args): :param _pytest.config.Config early_config: pytest config object :param list[str] args: list of arguments passed on the command line - :param _pytest.config.Parser parser: to add command line options + :param _pytest.config.argparsing.Parser parser: to add command line options """ From 772dfc4f9d610ebd735be48fd1c0924384c94e04 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 13 Nov 2019 23:24:17 +0100 Subject: [PATCH 18/95] terminal: fix/remove wrong typing for currentfspath Can be -2, or py.path.local (not typed). --- 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 59f0fe0f3..a84733d45 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -254,7 +254,7 @@ class TerminalReporter: # self.writer will be deprecated in pytest-3.4 self.writer = self._tw self._screen_width = self._tw.fullwidth - self.currentfspath = None # type: Optional[int] + self.currentfspath = None # type: Any self.reportchars = getreportopt(config) self.hasmarkup = self._tw.hasmarkup self.isatty = file.isatty() From 2a67637accc9b5f25b4d3fda3b99ad37cfcab18b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 13 Nov 2019 18:20:44 -0300 Subject: [PATCH 19/95] Issue a warning to prepare change of 'junit_family' default value Fix #6179 --- changelog/6179.deprecation.rst | 7 +++++++ doc/en/deprecations.rst | 21 +++++++++++++++++++++ src/_pytest/deprecated.py | 5 +++++ src/_pytest/junitxml.py | 12 ++++++++---- testing/deprecated_test.py | 29 +++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 changelog/6179.deprecation.rst diff --git a/changelog/6179.deprecation.rst b/changelog/6179.deprecation.rst new file mode 100644 index 000000000..97f7ec74b --- /dev/null +++ b/changelog/6179.deprecation.rst @@ -0,0 +1,7 @@ +The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given +that this is the version supported by default in modern tools that manipulate this type of file. + +In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option +is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``. + +For more information, `see the docs `__. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 5cf3b0903..34a05e1e6 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -19,6 +19,27 @@ Below is a complete list of all pytest features which are considered deprecated. :class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. +``junit_family`` default value change to "xunit2" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.2 + +The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given +that this is the version supported by default in modern tools that manipulate this type of file. + +In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option +is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``:: + + PytestDeprecationWarning: The 'junit_family' default value will change to 'xunit2' in pytest 6.0. + Add 'junit_family=legacy' to your pytest.ini file to silence this warning and make your suite compatible. + +In order to silence this warning, users just need to configure the ``junit_family`` option explicitly: + +.. code-block:: ini + + [pytest] + junit_family=legacy + ``funcargnames`` alias for ``fixturenames`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 5186067ef..442f102d1 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -34,3 +34,8 @@ FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning( "Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them " "as a keyword argument instead." ) + +JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning( + "The 'junit_family' default value will change to 'xunit2' in pytest 6.0.\n" + "Add 'junit_family=legacy' to your pytest.ini file to silence this warning and make your suite compatible." +) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index fb951106f..9cf22705e 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -19,8 +19,10 @@ from datetime import datetime import py import pytest +from _pytest import deprecated from _pytest import nodes from _pytest.config import filename_arg +from _pytest.warnings import _issue_warning_captured class Junit(py.xml.Namespace): @@ -421,9 +423,7 @@ def pytest_addoption(parser): default="total", ) # choices=['total', 'call']) parser.addini( - "junit_family", - "Emit XML for schema: one of legacy|xunit1|xunit2", - default="xunit1", + "junit_family", "Emit XML for schema: one of legacy|xunit1|xunit2", default=None ) @@ -431,13 +431,17 @@ def pytest_configure(config): xmlpath = config.option.xmlpath # prevent opening xmllog on slave nodes (xdist) if xmlpath and not hasattr(config, "slaveinput"): + 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._xml = LogXML( xmlpath, config.option.junitprefix, config.getini("junit_suite_name"), config.getini("junit_logging"), config.getini("junit_duration_report"), - config.getini("junit_family"), + junit_family, config.getini("junit_log_passing_tests"), ) config.pluginmanager.register(config._xml) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index b8a22428f..64ec11b7a 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -44,3 +44,32 @@ def test_external_plugins_integrated(testdir, plugin): with pytest.warns(pytest.PytestConfigWarning): 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]) From 6f2c0fd2e8e8154c8129900c9a1c650068811404 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 13 Nov 2019 17:51:14 -0300 Subject: [PATCH 20/95] Show a better message when 'request' is used in parametrize Fix #6183 --- changelog/6183.bugfix.rst | 2 ++ src/_pytest/python.py | 6 ++++++ testing/python/metafunc.py | 13 +++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 changelog/6183.bugfix.rst diff --git a/changelog/6183.bugfix.rst b/changelog/6183.bugfix.rst new file mode 100644 index 000000000..5c4d66e24 --- /dev/null +++ b/changelog/6183.bugfix.rst @@ -0,0 +1,2 @@ +Using ``request`` as a parameter name in ``@pytest.mark.parametrize`` now produces a more +user-friendly error. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index d16407bdd..306e5f217 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -977,6 +977,12 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): ) del argvalues + if "request" in argnames: + fail( + "'request' is a reserved name and cannot be used in @pytest.mark.parametrize", + pytrace=False, + ) + if scope is None: scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 5becb0f8c..860b21ff2 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -72,6 +72,19 @@ class TestMetafunc: ): metafunc.parametrize("x", [1], scope="doggy") + def test_parametrize_request_name(self, testdir): + """Show proper error when 'request' is used as a parameter name in parametrize (#6183)""" + + def func(request): + raise NotImplementedError() + + metafunc = self.Metafunc(func) + with pytest.raises( + pytest.fail.Exception, + match=r"'request' is a reserved name and cannot be used in @pytest.mark.parametrize", + ): + metafunc.parametrize("request", [1]) + def test_find_parametrized_scope(self): """unittest for _find_parametrized_scope (#3941)""" from _pytest.python import _find_parametrized_scope From 350c27c8b441ea73a3c5bf45500659da364f090c Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 14 Nov 2019 11:36:47 +0200 Subject: [PATCH 21/95] Update text in PR template --- .github/PULL_REQUEST_TEMPLATE.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f516959bc..843b67294 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,6 @@ Thanks for submitting a PR, your contribution is really appreciated! Here is a quick checklist that should be present in PRs. -(please delete this text from the final description, this is just a guideline) --> - [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes. @@ -10,7 +9,7 @@ Here is a quick checklist that should be present in PRs. - [ ] Include documentation when adding new features. - [ ] Include new tests or update existing tests when applicable. -Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please: +Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please: - [ ] Create a new changelog file in the `changelog` folder, with a name like `..rst`. See [changelog/README.rst](https://github.com/pytest-dev/pytest/blob/master/changelog/README.rst) for details. -- [ ] Add yourself to `AUTHORS` in alphabetical order; +- [ ] Add yourself to `AUTHORS` in alphabetical order. From 48ec7d28c6485da6dc220cd9a5acdbcfc6615ff1 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 14 Nov 2019 12:01:40 +0200 Subject: [PATCH 22/95] Make whole checklist a comment to avoid incomplete TODOs in PRs --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 843b67294..7436f7146 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,6 @@ Thanks for submitting a PR, your contribution is really appreciated! Here is a quick checklist that should be present in PRs. ---> - [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes. - [ ] Target the `features` branch for new features, improvements, and removals/deprecations. @@ -13,3 +12,4 @@ Unless your change is trivial or a small documentation fix (e.g., a typo or rewo - [ ] Create a new changelog file in the `changelog` folder, with a name like `..rst`. See [changelog/README.rst](https://github.com/pytest-dev/pytest/blob/master/changelog/README.rst) for details. - [ ] Add yourself to `AUTHORS` in alphabetical order. +--> From 5e8c47faad91ccf49897a290efd62bcac1971ff7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 14 Nov 2019 11:12:06 -0300 Subject: [PATCH 23/95] Preparing release version 5.2.3 --- CHANGELOG.rst | 16 ++++++++++++++++ changelog/5830.bugfix.rst | 1 - changelog/6099.bugfix.rst | 1 - changelog/6183.bugfix.rst | 2 -- doc/en/announce/index.rst | 1 + doc/en/announce/release-5.2.3.rst | 28 ++++++++++++++++++++++++++++ doc/en/example/parametrize.rst | 4 ++-- doc/en/example/reportingdemo.rst | 4 ++-- doc/en/getting-started.rst | 2 +- 9 files changed, 50 insertions(+), 9 deletions(-) delete mode 100644 changelog/5830.bugfix.rst delete mode 100644 changelog/6099.bugfix.rst delete mode 100644 changelog/6183.bugfix.rst create mode 100644 doc/en/announce/release-5.2.3.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 375b5dabf..402a35d73 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,22 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.2.3 (2019-11-14) +========================= + +Bug Fixes +--------- + +- `#5830 `_: Fix fail skipping the first test in package marked as ``skip`` + + +- `#6099 `_: Fix ``--trace`` when used with parametrized functions. + + +- `#6183 `_: Using ``request`` as a parameter name in ``@pytest.mark.parametrize`` now produces a more + user-friendly error. + + pytest 5.2.2 (2019-10-24) ========================= diff --git a/changelog/5830.bugfix.rst b/changelog/5830.bugfix.rst deleted file mode 100644 index 355790fd4..000000000 --- a/changelog/5830.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix fail skipping the first test in package marked as ``skip`` diff --git a/changelog/6099.bugfix.rst b/changelog/6099.bugfix.rst deleted file mode 100644 index 77f33cde1..000000000 --- a/changelog/6099.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``--trace`` when used with parametrized functions. diff --git a/changelog/6183.bugfix.rst b/changelog/6183.bugfix.rst deleted file mode 100644 index 5c4d66e24..000000000 --- a/changelog/6183.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Using ``request`` as a parameter name in ``@pytest.mark.parametrize`` now produces a more -user-friendly error. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index f7a634b31..05530dd2e 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.2.3 release-5.2.2 release-5.2.1 release-5.2.0 diff --git a/doc/en/announce/release-5.2.3.rst b/doc/en/announce/release-5.2.3.rst new file mode 100644 index 000000000..bfb62a1b8 --- /dev/null +++ b/doc/en/announce/release-5.2.3.rst @@ -0,0 +1,28 @@ +pytest-5.2.3 +======================================= + +pytest 5.2.3 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Brett Cannon +* Bruno Oliveira +* Daniel Hahler +* Daniil Galiev +* David Szotten +* Florian Bruhin +* Patrick Harmon +* Ran Benita +* Zac Hatfield-Dodds +* Zak Hassan + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 1220cfb4d..48ab95fda 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -475,10 +475,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] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found - SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.6' not found + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.7' not found 3 passed, 24 skipped in 0.12s Indirect parametrization of optional implementations/imports diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index eb978c5ea..1c06782f6 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -436,7 +436,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: items = [1, 2, 3] print("items is {!r}".format(items)) > a, b = items.pop() - E TypeError: cannot unpack non-iterable int object + E TypeError: 'int' object is not iterable failure_demo.py:181: TypeError --------------------------- Captured stdout call --------------------------- @@ -516,7 +516,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_z2_type_error(self): items = 3 > a, b = items - E TypeError: cannot unpack non-iterable int object + E TypeError: 'int' object is not iterable failure_demo.py:222: TypeError ______________________ TestMoreErrors.test_startswith ______________________ diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 97347f126..2bdd68ea3 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.7/site-packages/pytest.py + This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py .. _`simpletest`: From bd68c2a3dca6e6aadd1f9a0e4810f53256d4f6a8 Mon Sep 17 00:00:00 2001 From: Michael Shields Date: Tue, 12 Nov 2019 02:02:47 +0000 Subject: [PATCH 24/95] Update advice about _called_from_test. Instead of giving an example of using sys and then, at the end, advising not to use sys, just give a correct example. This is especially helpful since mypy 0.740 has started (correctly) complaining about sys._called_from_pytest not being present. --- doc/en/example/simple.rst | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index a7cd06d31..05ccbc9b2 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -300,36 +300,33 @@ behave differently if called from a test. But if you absolutely must find out if your application code is running from a test you can do something like this: +.. code-block:: python + + # content of your_module.py + + + _called_from_test = False + .. code-block:: python # content of conftest.py def pytest_configure(config): - import sys + your_module._called_from_test = True - sys._called_from_test = True - - - def pytest_unconfigure(config): - import sys - - del sys._called_from_test - -and then check for the ``sys._called_from_test`` flag: +and then check for the ``your_module._called_from_test`` flag: .. code-block:: python - if hasattr(sys, "_called_from_test"): + if your_module._called_from_test: # called from within a test run ... else: # called "normally" ... -accordingly in your application. It's also a good idea -to use your own application module rather than ``sys`` -for handling flag. +accordingly in your application. Adding info to test report header -------------------------------------------------------------- From dd9a27cf543c39f91039ecd7aa6aa595b3e79330 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 14 Nov 2019 13:35:51 -0300 Subject: [PATCH 25/95] Adjust CHANGELOG --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 402a35d73..87ffb9e07 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,7 +24,7 @@ pytest 5.2.3 (2019-11-14) Bug Fixes --------- -- `#5830 `_: Fix fail skipping the first test in package marked as ``skip`` +- `#5830 `_: The first test in a package (``__init__.py``) marked with ``@pytest.mark.skip`` is now correctly skipped. - `#6099 `_: Fix ``--trace`` when used with parametrized functions. From d2ea9e2db58dfb6c5a3c2981bfd062369e51fbf3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 14 Nov 2019 18:26:49 -0300 Subject: [PATCH 26/95] Remove report_log in favor of pytest-reportlog Fix #6180 --- changelog/4488.feature.rst | 7 ++-- doc/en/contents.rst | 1 - doc/en/deprecations.rst | 7 ++-- doc/en/report_log.rst | 70 --------------------------------- doc/en/usage.rst | 2 +- src/_pytest/config/__init__.py | 1 - src/_pytest/deprecated.py | 2 +- src/_pytest/report_log.py | 72 ---------------------------------- testing/deprecated_test.py | 2 +- testing/test_report_log.py | 54 ------------------------- 10 files changed, 11 insertions(+), 207 deletions(-) delete mode 100644 doc/en/report_log.rst delete mode 100644 src/_pytest/report_log.py delete mode 100644 testing/test_report_log.py diff --git a/changelog/4488.feature.rst b/changelog/4488.feature.rst index ddbca65d6..1e0387f44 100644 --- a/changelog/4488.feature.rst +++ b/changelog/4488.feature.rst @@ -1,9 +1,10 @@ -New ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes. +The pytest team has created the `pytest-reportlog `__ +plugin, which provides a new ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes. Each line of the report log contains a self contained JSON object corresponding to a testing event, such as a collection or a test result report. The file is guaranteed to be flushed after writing each line, so systems can read and process events in real-time. -This option is meant to replace ``--resultlog``, which is deprecated and meant to be removed -in a future release. If you use ``--resultlog``, please try out ``--report-log`` and +The plugin is meant to replace the ``--resultlog`` option, which is deprecated and meant to be removed +in a future release. If you use ``--resultlog``, please try out ``pytest-reportlog`` and provide feedback. diff --git a/doc/en/contents.rst b/doc/en/contents.rst index 5d7599f50..c623d0602 100644 --- a/doc/en/contents.rst +++ b/doc/en/contents.rst @@ -27,7 +27,6 @@ Full pytest documentation unittest nose xunit_setup - report_log plugins writing_plugins logging diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 34a05e1e6..748d3ac65 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -64,11 +64,12 @@ 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 :ref:`--report-log ` option provides a more standard and extensible alternative, producing +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 after ``--result-log`` proves satisfactory -to all users and is deemed stable. +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. Removed Features diff --git a/doc/en/report_log.rst b/doc/en/report_log.rst deleted file mode 100644 index 619925180..000000000 --- a/doc/en/report_log.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. _report_log: - -Report files -============ - -.. versionadded:: 5.3 - -The ``--report-log=FILE`` option writes *report logs* into a file as the test session executes. - -Each line of the report log contains a self contained JSON object corresponding to a testing event, -such as a collection or a test result report. The file is guaranteed to be flushed after writing -each line, so systems can read and process events in real-time. - -Each JSON object contains a special key ``$report_type``, which contains a unique identifier for -that kind of report object. For future compatibility, consumers of the file should ignore reports -they don't recognize, as well as ignore unknown properties/keys in JSON objects that they do know, -as future pytest versions might enrich the objects with more properties/keys. - -.. note:: - This option is meant to the replace ``--resultlog``, which is deprecated and meant to be removed - in a future release. If you use ``--resultlog``, please try out ``--report-log`` and - provide feedback. - -Example -------- - -Consider this file: - -.. code-block:: python - - # content of test_report_example.py - - - def test_ok(): - assert 5 + 5 == 10 - - - def test_fail(): - assert 4 + 4 == 1 - - -.. code-block:: pytest - - $ pytest test_report_example.py -q --report-log=log.json - .F [100%] - ================================= FAILURES ================================= - ________________________________ test_fail _________________________________ - - def test_fail(): - > assert 4 + 4 == 1 - E assert (4 + 4) == 1 - - test_report_example.py:8: AssertionError - ------------------- generated report log file: log.json -------------------- - 1 failed, 1 passed in 0.12s - -The generated ``log.json`` will contain a JSON object per line: - -:: - - $ cat log.json - {"pytest_version": "5.2.3.dev90+gd1129cf96.d20191026", "$report_type": "Header"} - {"nodeid": "", "outcome": "passed", "longrepr": null, "result": null, "sections": [], "$report_type": "CollectReport"} - {"nodeid": "test_report_example.py", "outcome": "passed", "longrepr": null, "result": null, "sections": [], "$report_type": "CollectReport"} - {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "setup", "user_properties": [], "sections": [], "duration": 0.00021314620971679688, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "call", "user_properties": [], "sections": [], "duration": 0.00014543533325195312, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "teardown", "user_properties": [], "sections": [], "duration": 0.00016427040100097656, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "passed", "longrepr": null, "when": "setup", "user_properties": [], "sections": [], "duration": 0.00013589859008789062, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "failed", "longrepr": {"reprcrash": {"path": "$REGENDOC_TMPDIR/test_report_example.py", "lineno": 8, "message": "assert (4 + 4) == 1"}, "reprtraceback": {"reprentries": [{"type": "ReprEntry", "data": {"lines": [" def test_fail():", "> assert 4 + 4 == 1", "E assert (4 + 4) == 1"], "reprfuncargs": {"args": []}, "reprlocals": null, "reprfileloc": {"path": "test_report_example.py", "lineno": 8, "message": "AssertionError"}, "style": "long"}}], "extraline": null, "style": "long"}, "sections": [], "chain": [[{"reprentries": [{"type": "ReprEntry", "data": {"lines": [" def test_fail():", "> assert 4 + 4 == 1", "E assert (4 + 4) == 1"], "reprfuncargs": {"args": []}, "reprlocals": null, "reprfileloc": {"path": "test_report_example.py", "lineno": 8, "message": "AssertionError"}, "style": "long"}}], "extraline": null, "style": "long"}, {"path": "$REGENDOC_TMPDIR/test_report_example.py", "lineno": 8, "message": "assert (4 + 4) == 1"}, null]]}, "when": "call", "user_properties": [], "sections": [], "duration": 0.00027489662170410156, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "passed", "longrepr": null, "when": "teardown", "user_properties": [], "sections": [], "duration": 0.00016689300537109375, "$report_type": "TestReport"} diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 3b5919363..ea849c1a7 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -692,7 +692,7 @@ by the `PyPy-test`_ web page to show test results over several revisions. This option is rarely used and is scheduled for removal in pytest 6.0. - If you use this option, consider using the new :ref:`--result-log `. + If you use this option, consider using the new `pytest-reportlog `__ plugin instead. See `the deprecation docs `__ for more information. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index e94bec5d6..c5bf32bbf 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -154,7 +154,6 @@ default_plugins = essential_plugins + ( "assertion", "junitxml", "resultlog", - "report_log", "doctest", "cacheprovider", "freeze_support", diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 442f102d1..5a7066041 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -26,7 +26,7 @@ FUNCARGNAMES = PytestDeprecationWarning( RESULT_LOG = PytestDeprecationWarning( - "--result-log is deprecated and scheduled for removal in pytest 6.0.\n" + "--result-log is deprecated, please try the new pytest-reportlog plugin.\n" "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." ) diff --git a/src/_pytest/report_log.py b/src/_pytest/report_log.py deleted file mode 100644 index b12d0a55d..000000000 --- a/src/_pytest/report_log.py +++ /dev/null @@ -1,72 +0,0 @@ -import json -from pathlib import Path - -import pytest - - -def pytest_addoption(parser): - group = parser.getgroup("terminal reporting", "report-log plugin options") - group.addoption( - "--report-log", - action="store", - metavar="path", - default=None, - help="Path to line-based json objects of test session events.", - ) - - -def pytest_configure(config): - report_log = config.option.report_log - if report_log and not hasattr(config, "slaveinput"): - config._report_log_plugin = ReportLogPlugin(config, Path(report_log)) - config.pluginmanager.register(config._report_log_plugin) - - -def pytest_unconfigure(config): - report_log_plugin = getattr(config, "_report_log_plugin", None) - if report_log_plugin: - report_log_plugin.close() - del config._report_log_plugin - - -class ReportLogPlugin: - def __init__(self, config, log_path: Path): - self._config = config - self._log_path = log_path - - log_path.parent.mkdir(parents=True, exist_ok=True) - self._file = log_path.open("w", buffering=1, encoding="UTF-8") - - def close(self): - if self._file is not None: - self._file.close() - self._file = None - - def _write_json_data(self, data): - self._file.write(json.dumps(data) + "\n") - self._file.flush() - - def pytest_sessionstart(self): - data = {"pytest_version": pytest.__version__, "$report_type": "SessionStart"} - self._write_json_data(data) - - def pytest_sessionfinish(self, exitstatus): - data = {"exitstatus": exitstatus, "$report_type": "SessionFinish"} - self._write_json_data(data) - - def pytest_runtest_logreport(self, report): - data = self._config.hook.pytest_report_to_serializable( - config=self._config, report=report - ) - self._write_json_data(data) - - def pytest_collectreport(self, report): - data = self._config.hook.pytest_report_to_serializable( - config=self._config, report=report - ) - self._write_json_data(data) - - def pytest_terminal_summary(self, terminalreporter): - terminalreporter.write_sep( - "-", "generated report log file: {}".format(self._log_path) - ) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 64ec11b7a..5390d038d 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -16,7 +16,7 @@ def test_resultlog_is_deprecated(testdir): result = testdir.runpytest("--result-log=%s" % testdir.tmpdir.join("result.log")) result.stdout.fnmatch_lines( [ - "*--result-log is deprecated and scheduled for removal in pytest 6.0*", + "*--result-log is deprecated, please try the new pytest-reportlog plugin.", "*See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information*", ] ) diff --git a/testing/test_report_log.py b/testing/test_report_log.py deleted file mode 100644 index cc2a431ec..000000000 --- a/testing/test_report_log.py +++ /dev/null @@ -1,54 +0,0 @@ -import json - -import pytest -from _pytest.reports import BaseReport - - -def test_basics(testdir, tmp_path, pytestconfig): - """Basic testing of the report log functionality. - - We don't test the test reports extensively because they have been - tested already in ``test_reports``. - """ - testdir.makepyfile( - """ - def test_ok(): - pass - - def test_fail(): - assert 0 - """ - ) - - log_file = tmp_path / "log.json" - - result = testdir.runpytest("--report-log", str(log_file)) - assert result.ret == pytest.ExitCode.TESTS_FAILED - result.stdout.fnmatch_lines(["* generated report log file: {}*".format(log_file)]) - - json_objs = [json.loads(x) for x in log_file.read_text().splitlines()] - assert len(json_objs) == 10 - - # first line should be the session_start - session_start = json_objs[0] - assert session_start == { - "pytest_version": pytest.__version__, - "$report_type": "SessionStart", - } - - # last line should be the session_finish - session_start = json_objs[-1] - assert session_start == { - "exitstatus": pytest.ExitCode.TESTS_FAILED, - "$report_type": "SessionFinish", - } - - # rest of the json objects should be unserialized into report objects; we don't test - # the actual report object extensively because it has been tested in ``test_reports`` - # already. - pm = pytestconfig.pluginmanager - for json_obj in json_objs[1:-1]: - rep = pm.hook.pytest_report_from_serializable( - config=pytestconfig, data=json_obj - ) - assert isinstance(rep, BaseReport) From 176c7771fb4f3bd703bb6c9650cc32e31d548a2b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 15 Nov 2019 08:29:52 -0800 Subject: [PATCH 27/95] Revert "fix bug with nonskipped first test in package (#5831)" This reverts commit 85288b53218d4e0037dce9c34bd37f86fa9e212d, reversing changes made to 5f9db8a01721e10162769b96b82794dd069bbaeb. --- AUTHORS | 1 - src/_pytest/python.py | 24 ++++++------------------ testing/test_skipping.py | 23 ----------------------- 3 files changed, 6 insertions(+), 42 deletions(-) diff --git a/AUTHORS b/AUTHORS index d0e584f63..e11400c1f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -70,7 +70,6 @@ Daniel Hahler Daniel Nuri Daniel Wandschneider Danielle Jenkins -Daniil Galiev Dave Hunt David Díaz-Barquero David Mohr diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 306e5f217..b8b365ad3 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -251,21 +251,18 @@ class PyobjMixin(PyobjContext): @property def obj(self): """Underlying Python object.""" - self._mount_obj_if_needed() - return self._obj - - @obj.setter - def obj(self, value): - self._obj = value - - def _mount_obj_if_needed(self): obj = getattr(self, "_obj", None) if obj is None: self._obj = obj = self._getobj() # XXX evil hack # used to avoid Instance collector marker duplication if self._ALLOW_MARKERS: - self.own_markers.extend(get_unpacked_marks(obj)) + self.own_markers.extend(get_unpacked_marks(self.obj)) + return obj + + @obj.setter + def obj(self, value): + self._obj = value def _getobj(self): """Gets the underlying Python object. May be overwritten by subclasses.""" @@ -432,14 +429,6 @@ class PyCollector(PyobjMixin, nodes.Collector): class Module(nodes.File, PyCollector): """ Collector for test classes and functions. """ - def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): - if fspath.basename == "__init__.py": - self._ALLOW_MARKERS = False - - nodes.FSCollector.__init__( - self, fspath, parent=parent, config=config, session=session, nodeid=nodeid - ) - def _getobj(self): return self._importtestmodule() @@ -639,7 +628,6 @@ class Package(Module): return path in self.session._initialpaths def collect(self): - self._mount_obj_if_needed() this_path = self.fspath.dirpath() init_module = this_path.join("__init__.py") if init_module.check(file=1) and path_matches_patterns( diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 371c3a4db..8bba479f1 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1162,26 +1162,3 @@ def test_importorskip(): match="^could not import 'doesnotexist': No module named .*", ): pytest.importorskip("doesnotexist") - - -def test_skip_package(testdir): - testdir.makepyfile( - __init__=""" - import pytest - pytestmark = pytest.mark.skip - """ - ) - - testdir.makepyfile( - """ - import pytest - def test_skip1(): - assert 0 - def test_skip2(): - assert 0 - """ - ) - - result = testdir.inline_run() - _, skipped, _ = result.listoutcomes() - assert len(skipped) == 2 From 4e0f99260d438d750b55a0881110658b8bba5a4a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 15 Nov 2019 08:31:53 -0800 Subject: [PATCH 28/95] Add regression tests for __init__.py breakage --- AUTHORS | 1 + changelog/6194.bugfix.rst | 1 + changelog/6197.bugfix.rst | 1 + testing/test_collection.py | 21 +++++++++++++++++++++ 4 files changed, 24 insertions(+) create mode 100644 changelog/6194.bugfix.rst create mode 100644 changelog/6197.bugfix.rst diff --git a/AUTHORS b/AUTHORS index e11400c1f..d0e584f63 100644 --- a/AUTHORS +++ b/AUTHORS @@ -70,6 +70,7 @@ Daniel Hahler Daniel Nuri Daniel Wandschneider Danielle Jenkins +Daniil Galiev Dave Hunt David Díaz-Barquero David Mohr diff --git a/changelog/6194.bugfix.rst b/changelog/6194.bugfix.rst new file mode 100644 index 000000000..92e6aec78 --- /dev/null +++ b/changelog/6194.bugfix.rst @@ -0,0 +1 @@ +Fix incorrect discovery of non-test ``__init__.py`` files. diff --git a/changelog/6197.bugfix.rst b/changelog/6197.bugfix.rst new file mode 100644 index 000000000..9bd0a5a65 --- /dev/null +++ b/changelog/6197.bugfix.rst @@ -0,0 +1 @@ +Revert "The first test in a package (``__init__.py``) marked with ``@pytest.mark.skip`` is now correctly skipped.". diff --git a/testing/test_collection.py b/testing/test_collection.py index dee07d5c7..8050e80f9 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1257,3 +1257,24 @@ def test_collector_respects_tbstyle(testdir): "*= 1 error in *", ] ) + + +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() + assert result.ret == ExitCode.OK + + +def test_does_not_put_src_on_path(testdir): + # `src` is not on sys.path so it should not be importable + testdir.tmpdir.join("src/nope/__init__.py").ensure() + testdir.makepyfile( + "import pytest\n" + "def test():\n" + " with pytest.raises(ImportError):\n" + " import nope\n" + ) + result = testdir.runpytest() + assert result.ret == ExitCode.OK From 5979837c6084e80367ce4f7e1b97aabd755221b0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Nov 2019 03:53:05 +0100 Subject: [PATCH 29/95] Import Path from _pytest.pathlib for py35 This is important for `isinstance` checks etc. --- src/_pytest/assertion/rewrite.py | 2 +- src/_pytest/config/__init__.py | 2 +- testing/test_assertrewrite.py | 2 +- testing/test_config.py | 2 +- testing/test_conftest.py | 2 +- testing/test_junitxml.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index b84929936..af4d00194 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -13,7 +13,6 @@ import struct import sys import tokenize import types -from pathlib import Path from typing import Dict from typing import List from typing import Optional @@ -28,6 +27,7 @@ from _pytest.assertion.util import ( # noqa: F401 ) from _pytest.compat import fspath from _pytest.pathlib import fnmatch_ex +from _pytest.pathlib import Path from _pytest.pathlib import PurePath # pytest caches rewritten pycs in pycache dirs diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index e94bec5d6..06296fd35 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -8,7 +8,6 @@ import sys 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 @@ -40,6 +39,7 @@ from _pytest._code import filter_traceback from _pytest.compat import importlib_metadata from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import Path from _pytest.warning_types import PytestConfigWarning if False: # TYPE_CHECKING diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index e2d6b89c8..8490a59e6 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -9,7 +9,6 @@ import sys import textwrap import zipfile from functools import partial -from pathlib import Path import py @@ -23,6 +22,7 @@ from _pytest.assertion.rewrite import PYC_TAIL from _pytest.assertion.rewrite import PYTEST_TAG from _pytest.assertion.rewrite import rewrite_asserts from _pytest.main import ExitCode +from _pytest.pathlib import Path def setup_module(mod): diff --git a/testing/test_config.py b/testing/test_config.py index d4d624348..f146b52a4 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,7 +1,6 @@ import os import sys import textwrap -from pathlib import Path import _pytest._code import pytest @@ -13,6 +12,7 @@ from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import getcfg from _pytest.main import ExitCode +from _pytest.pathlib import Path class TestParseIni: diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 0374db0b3..2918ff04c 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 PytestPluginManager from _pytest.main import ExitCode +from _pytest.pathlib import Path def ConftestWithSetinitial(path): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 885d25941..4c2f22a3d 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,7 +1,6 @@ import os import platform from datetime import datetime -from pathlib import Path from xml.dom import minidom import py @@ -9,6 +8,7 @@ import xmlschema import pytest from _pytest.junitxml import LogXML +from _pytest.pathlib import Path from _pytest.reports import BaseReport From c9a96cdee8ee7c1a9ead49b355fab9aef73196fe Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 15 Nov 2019 13:24:35 -0800 Subject: [PATCH 30/95] Preparing release version 5.2.4 --- CHANGELOG.rst | 12 ++++++++++++ changelog/6194.bugfix.rst | 1 - changelog/6197.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-5.2.4.rst | 22 ++++++++++++++++++++++ doc/en/example/parametrize.rst | 7 +++---- 6 files changed, 38 insertions(+), 6 deletions(-) delete mode 100644 changelog/6194.bugfix.rst delete mode 100644 changelog/6197.bugfix.rst create mode 100644 doc/en/announce/release-5.2.4.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 87ffb9e07..b1a988de8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,18 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.2.4 (2019-11-15) +========================= + +Bug Fixes +--------- + +- `#6194 `_: Fix incorrect discovery of non-test ``__init__.py`` files. + + +- `#6197 `_: Revert "The first test in a package (``__init__.py``) marked with ``@pytest.mark.skip`` is now correctly skipped.". + + pytest 5.2.3 (2019-11-14) ========================= diff --git a/changelog/6194.bugfix.rst b/changelog/6194.bugfix.rst deleted file mode 100644 index 92e6aec78..000000000 --- a/changelog/6194.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix incorrect discovery of non-test ``__init__.py`` files. diff --git a/changelog/6197.bugfix.rst b/changelog/6197.bugfix.rst deleted file mode 100644 index 9bd0a5a65..000000000 --- a/changelog/6197.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Revert "The first test in a package (``__init__.py``) marked with ``@pytest.mark.skip`` is now correctly skipped.". diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 05530dd2e..e7c011411 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.2.4 release-5.2.3 release-5.2.2 release-5.2.1 diff --git a/doc/en/announce/release-5.2.4.rst b/doc/en/announce/release-5.2.4.rst new file mode 100644 index 000000000..05677e77f --- /dev/null +++ b/doc/en/announce/release-5.2.4.rst @@ -0,0 +1,22 @@ +pytest-5.2.4 +======================================= + +pytest 5.2.4 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Hugo +* Michael Shields + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 48ab95fda..0e131dace 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -475,11 +475,10 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ssssssssssss...ssssssssssss [100%] + ssssssssssss......sss...... [100%] ========================= short test summary info ========================== - SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found - SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.7' not found - 3 passed, 24 skipped in 0.12s + SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found + 12 passed, 15 skipped in 0.12s Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- From 1abb08d52f165e46bb3cc80a9ece860aa4afe5b5 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Nov 2019 23:12:25 +0100 Subject: [PATCH 31/95] tests: use sys.dont_write_bytecode Setting PYTHONDONTWRITEBYTECODE in the environment does not change it for the current process. --- testing/test_cacheprovider.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 3f03b5ff9..ea78358d6 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -253,7 +253,7 @@ def test_cache_show(testdir): class TestLastFailed: def test_lastfailed_usecase(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") + monkeypatch.setattr("sys.dont_write_bytecode", True) p = testdir.makepyfile( """ def test_1(): @@ -345,7 +345,7 @@ class TestLastFailed: result.stdout.no_fnmatch_line("*test_a.py*") def test_lastfailed_difference_invocations(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") + monkeypatch.setattr("sys.dont_write_bytecode", True) testdir.makepyfile( test_a="""\ def test_a1(): @@ -379,7 +379,7 @@ class TestLastFailed: result.stdout.fnmatch_lines(["*1 failed*1 desel*"]) def test_lastfailed_usecase_splice(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") + monkeypatch.setattr("sys.dont_write_bytecode", True) testdir.makepyfile( """\ def test_1(): From 329f56ecec5a9fcbfa160d4b467342a7c19ef2c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E7=8E=AE?= Date: Sat, 16 Nov 2019 14:49:17 +0800 Subject: [PATCH 32/95] Fix incorrect result of getmodpath method. --- AUTHORS | 1 + changelog/6189.bugfix.rst | 1 + src/_pytest/python.py | 3 +-- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog/6189.bugfix.rst diff --git a/AUTHORS b/AUTHORS index d0e584f63..c10ee5c15 100644 --- a/AUTHORS +++ b/AUTHORS @@ -261,6 +261,7 @@ Virgil Dupras Vitaly Lashmanov Vlad Dragos Volodymyr Piskun +Wei Lin Wil Cooley William Lee Wim Glenn diff --git a/changelog/6189.bugfix.rst b/changelog/6189.bugfix.rst new file mode 100644 index 000000000..060a2260a --- /dev/null +++ b/changelog/6189.bugfix.rst @@ -0,0 +1 @@ +Fix incorrect result of ``getmodpath`` method. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index b8b365ad3..734a92f9b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -285,8 +285,7 @@ class PyobjMixin(PyobjContext): break parts.append(name) parts.reverse() - s = ".".join(parts) - return s.replace(".[", "[") + return ".".join(parts) def reportinfo(self): # XXX caching? From 9c681b45e39a144f8c80abbd66eb4db0e185a238 Mon Sep 17 00:00:00 2001 From: TH3CHARLie Date: Sat, 16 Nov 2019 17:34:05 +0800 Subject: [PATCH 33/95] change: #3985 also introduce --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b1a988de8..47356d1d9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1901,7 +1901,8 @@ Features live-logging is enabled and/or when they are logged to a file. -- `#3985 `_: Introduce ``tmp_path`` as a fixture providing a Path object. +- `#3985 `_: Introduce ``tmp_path`` as a fixture providing a Path object. Also introduce ``tmp_path_factory`` as + a session-scoped fixture for creating arbitrary temporary directories from any other fixture or test. - `#4013 `_: Deprecation warnings are now shown even if you customize the warnings filters yourself. In the previous version From b090ac62048990cc4d54dab841e81a8921b97284 Mon Sep 17 00:00:00 2001 From: TH3CHARLie Date: Sat, 16 Nov 2019 18:01:08 +0800 Subject: [PATCH 34/95] remove trailing-whitespace --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 47356d1d9..e9ac09c8e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1901,7 +1901,7 @@ Features live-logging is enabled and/or when they are logged to a file. -- `#3985 `_: Introduce ``tmp_path`` as a fixture providing a Path object. Also introduce ``tmp_path_factory`` as +- `#3985 `_: Introduce ``tmp_path`` as a fixture providing a Path object. Also introduce ``tmp_path_factory`` as a session-scoped fixture for creating arbitrary temporary directories from any other fixture or test. From f760356578836fd888a1cd825460d8daa8808ea6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 15:16:40 +0200 Subject: [PATCH 35/95] A few linting fixes Add some Python 3.8 type: ignores; all are already fixed in the next mypy release, so can be removed once we upgrade. Also move some flake8 ignores which seem to have changed places. --- src/_pytest/_code/source.py | 2 +- src/_pytest/assertion/rewrite.py | 5 +++-- src/_pytest/compat.py | 7 +++++-- src/_pytest/python_api.py | 2 +- src/_pytest/recwarn.py | 6 +++--- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 1e9dd5031..a26a70e68 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -60,7 +60,7 @@ class Source: raise NotImplementedError() @overload # noqa: F811 - def __getitem__(self, key: slice) -> "Source": + def __getitem__(self, key: slice) -> "Source": # noqa: F811 raise NotImplementedError() def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: F811 diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index af4d00194..2f9ca6de0 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -1074,13 +1074,14 @@ def try_makedirs(cache_dir) -> bool: def get_cache_dir(file_path: Path) -> Path: """Returns the cache directory to write .pyc files for the given .py file path""" - if sys.version_info >= (3, 8) and sys.pycache_prefix: + # Type ignored until added in next mypy release. + if sys.version_info >= (3, 8) and sys.pycache_prefix: # type: ignore # given: # prefix = '/tmp/pycs' # path = '/home/user/proj/test_app.py' # we want: # '/tmp/pycs/home/user/proj' - return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) + return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) # type: ignore else: # classic pycache directory return file_path.parent / "__pycache__" diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 5e066c18e..c115ae98d 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -40,7 +40,8 @@ MODULE_NOT_FOUND_ERROR = ( if sys.version_info >= (3, 8): - from importlib import metadata as importlib_metadata # noqa: F401 + # Type ignored until next mypy release. + from importlib import metadata as importlib_metadata # type: ignore else: import importlib_metadata # noqa: F401 @@ -407,7 +408,9 @@ else: raise NotImplementedError() @overload # noqa: F811 - def __get__(self, instance: _S, owner: Optional["Type[_S]"] = ...) -> _T: + def __get__( # noqa: F811 + self, instance: _S, owner: Optional["Type[_S]"] = ... + ) -> _T: raise NotImplementedError() def __get__(self, instance, owner=None): # noqa: F811 diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 52a91a905..9f206ce9b 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -552,7 +552,7 @@ def raises( @overload # noqa: F811 -def raises( +def raises( # noqa: F811 expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], func: Callable, *args: Any, diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 4967106d9..5cf32c894 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -60,18 +60,18 @@ def warns( *, match: "Optional[Union[str, Pattern]]" = ... ) -> "WarningsChecker": - ... # pragma: no cover + raise NotImplementedError() @overload # noqa: F811 -def warns( +def warns( # noqa: F811 expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], func: Callable, *args: Any, match: Optional[Union[str, "Pattern"]] = ..., **kwargs: Any ) -> Union[Any]: - ... # pragma: no cover + raise NotImplementedError() def warns( # noqa: F811 From c7a83a0f316c8bebd42df636b8d5cccfe948a72c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Oct 2019 15:15:16 +0300 Subject: [PATCH 36/95] Remove a PyPy version check for an unsupported version pytest doesn't support these PyPy versions anymore, so no need to have checks for them. --- testing/test_capture.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index 85b0b05ae..94af3aef7 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -92,8 +92,6 @@ class TestCaptureManager: @pytest.mark.parametrize("method", ["fd", "sys"]) def test_capturing_unicode(testdir, method): - if hasattr(sys, "pypy_version_info") and sys.pypy_version_info < (2, 2): - pytest.xfail("does not work on pypy < 2.2") obj = "'b\u00f6y'" testdir.makepyfile( """\ From 04d68fbc9e53ff01962ed5da8686ced276425c95 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 15:51:44 +0200 Subject: [PATCH 37/95] Remove checks for Python2-only fields im_func and func_code --- src/_pytest/_code/code.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 3c2acfe7f..19d5efaa6 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1054,8 +1054,6 @@ def getrawcode(obj, trycall=True): try: return obj.__code__ except AttributeError: - obj = getattr(obj, "im_func", obj) - obj = getattr(obj, "func_code", obj) obj = getattr(obj, "f_code", obj) obj = getattr(obj, "__code__", obj) if trycall and not hasattr(obj, "co_firstlineno"): From 5bfe793fd5455c04049c4a564d1a6dc666cc647a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 15:54:56 +0200 Subject: [PATCH 38/95] Remove unneeded getrawcode() calls from tests --- testing/code/test_excinfo.py | 6 +++--- testing/code/test_source.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 199b8716f..b83ad93e2 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -59,9 +59,9 @@ def test_excinfo_getstatement(): except ValueError: excinfo = _pytest._code.ExceptionInfo.from_current() linenumbers = [ - _pytest._code.getrawcode(f).co_firstlineno - 1 + 4, - _pytest._code.getrawcode(f).co_firstlineno - 1 + 1, - _pytest._code.getrawcode(g).co_firstlineno - 1 + 1, + f.__code__.co_firstlineno - 1 + 4, + f.__code__.co_firstlineno - 1 + 1, + g.__code__.co_firstlineno - 1 + 1, ] values = list(excinfo.traceback) foundlinenumbers = [x.lineno for x in values] diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 5e7e1abf5..519344dd4 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -478,7 +478,7 @@ def test_getfslineno(): fspath, lineno = getfslineno(f) assert fspath.basename == "test_source.py" - assert lineno == _pytest._code.getrawcode(f).co_firstlineno - 1 # see findsource + assert lineno == f.__code__.co_firstlineno - 1 # see findsource class A: pass From e3ac44df360849809d8d156bd24827025238072c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 16:14:04 +0200 Subject: [PATCH 39/95] Inline the FuncargnamesCompatAttr compat helper It doesn't help much IMO, just adds indirection and makes it harder to type. --- src/_pytest/compat.py | 15 --------------- src/_pytest/fixtures.py | 10 ++++++++-- src/_pytest/python.py | 17 +++++++++++++++-- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index c115ae98d..09e621c5d 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -361,21 +361,6 @@ class CaptureIO(io.TextIOWrapper): return self.buffer.getvalue().decode("UTF-8") -class FuncargnamesCompatAttr: - """ helper class so that Metafunc, Function and FixtureRequest - don't need to each define the "funcargnames" compatibility attribute. - """ - - @property - def funcargnames(self): - """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" - import warnings - from _pytest.deprecated import FUNCARGNAMES - - warnings.warn(FUNCARGNAMES, stacklevel=2) - return self.fixturenames - - if sys.version_info < (3, 5, 2): # pragma: no cover def overload(f): # noqa: F811 diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index fc55ef2cf..34ecf2e21 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -18,7 +18,6 @@ from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper -from _pytest.compat import FuncargnamesCompatAttr from _pytest.compat import get_real_func from _pytest.compat import get_real_method from _pytest.compat import getfslineno @@ -29,6 +28,7 @@ from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import safe_getattr from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS +from _pytest.deprecated import FUNCARGNAMES from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -336,7 +336,7 @@ class FuncFixtureInfo: self.names_closure[:] = sorted(closure, key=self.names_closure.index) -class FixtureRequest(FuncargnamesCompatAttr): +class FixtureRequest: """ A request for a fixture from a test or fixture function. A request object gives access to the requesting test context @@ -363,6 +363,12 @@ class FixtureRequest(FuncargnamesCompatAttr): result.extend(set(self._fixture_defs).difference(result)) return result + @property + def funcargnames(self): + """ 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 c1654b1c9..3cee09332 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -31,6 +31,7 @@ from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass from _pytest.compat import STRING_TYPES from _pytest.config import hookimpl +from _pytest.deprecated import FUNCARGNAMES from _pytest.main import FSHookProxy from _pytest.mark import MARK_GEN from _pytest.mark.structures import get_unpacked_marks @@ -882,7 +883,7 @@ class CallSpec2: self.marks.extend(normalize_mark_list(marks)) -class Metafunc(fixtures.FuncargnamesCompatAttr): +class Metafunc: """ Metafunc objects are 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 @@ -916,6 +917,12 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): self._ids = set() self._arg2fixturedefs = fixtureinfo.name2fixturedefs + @property + def funcargnames(self): + """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + warnings.warn(FUNCARGNAMES, stacklevel=2) + return self.fixturenames + def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None): """ Add new invocations to the underlying test function using the list of argvalues for the given argnames. Parametrization is performed @@ -1333,7 +1340,7 @@ def write_docstring(tw, doc, indent=" "): tw.write(indent + line + "\n") -class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): +class Function(FunctionMixin, nodes.Item): """ a Function Item is responsible for setting up and executing a Python test function. """ @@ -1420,6 +1427,12 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): "(compatonly) for code expecting pytest-2.2 style request objects" return self + @property + def funcargnames(self): + """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + warnings.warn(FUNCARGNAMES, stacklevel=2) + return self.fixturenames + def runtest(self): """ execute the underlying test function. """ self.ihook.pytest_pyfunc_call(pyfuncitem=self) From 307add025b5464e87a55b49c6e5e4ce4c6373eee Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Nov 2019 12:53:49 +0200 Subject: [PATCH 40/95] Simplify a FormattedExcinfo test The previous test was better in that it used fakes to test all of the real code paths. The problem with that is that it makes it impossible to simplify the code with `isinstance` checks. So let's just simulate the issue directly with a monkeypatch. --- src/_pytest/_code/code.py | 4 +-- testing/code/test_excinfo.py | 63 +++++------------------------------- 2 files changed, 9 insertions(+), 58 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 19d5efaa6..334365042 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -283,8 +283,6 @@ class Traceback(list): access to Traceback entries. """ - Entry = TracebackEntry - def __init__(self, tb, excinfo=None): """ initialize from given python traceback object and ExceptionInfo """ self._excinfo = excinfo @@ -292,7 +290,7 @@ class Traceback(list): def f(cur): while cur is not None: - yield self.Entry(cur, excinfo=excinfo) + yield TracebackEntry(cur, excinfo=excinfo) cur = cur.tb_next list.__init__(self, f(tb)) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index b83ad93e2..f08808648 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -502,65 +502,18 @@ raise ValueError() assert repr.reprtraceback.reprentries[1].lines[0] == "> ???" assert repr.chain[0][0].reprentries[1].lines[0] == "> ???" - def test_repr_source_failing_fullsource(self): + def test_repr_source_failing_fullsource(self, monkeypatch) -> None: pr = FormattedExcinfo() - class FakeCode: - class raw: - co_filename = "?" + try: + 1 / 0 + except ZeroDivisionError: + excinfo = ExceptionInfo.from_current() - path = "?" - firstlineno = 5 + with monkeypatch.context() as m: + m.setattr(_pytest._code.Code, "fullsource", property(lambda self: None)) + repr = pr.repr_excinfo(excinfo) - def fullsource(self): - return None - - fullsource = property(fullsource) - - class FakeFrame: - code = FakeCode() - f_locals = {} - f_globals = {} - - class FakeTracebackEntry(_pytest._code.Traceback.Entry): - def __init__(self, tb, excinfo=None): - self.lineno = 5 + 3 - - @property - def frame(self): - return FakeFrame() - - class Traceback(_pytest._code.Traceback): - Entry = FakeTracebackEntry - - class FakeExcinfo(_pytest._code.ExceptionInfo): - typename = "Foo" - value = Exception() - - def __init__(self): - pass - - def exconly(self, tryshort): - return "EXC" - - def errisinstance(self, cls): - return False - - excinfo = FakeExcinfo() - - class FakeRawTB: - tb_next = None - - tb = FakeRawTB() - excinfo.traceback = Traceback(tb) - - fail = IOError() - repr = pr.repr_excinfo(excinfo) - assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" - assert repr.chain[0][0].reprentries[0].lines[0] == "> ???" - - fail = py.error.ENOENT # noqa - repr = pr.repr_excinfo(excinfo) assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" assert repr.chain[0][0].reprentries[0].lines[0] == "> ???" From a649f157de8bc16fb99152b2f64e407f1e842692 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Nov 2019 15:50:58 +0200 Subject: [PATCH 41/95] Make Source explicitly implement __iter__() Source was previously iterable because it implements `__getitem__()`, which is apparently a thing from before `__iter__()` was introduced. To reduce mypy's and my own confusion, implement `__iter__()` directly. --- src/_pytest/_code/source.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index a26a70e68..d7cef683d 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -8,6 +8,7 @@ import warnings from ast import PyCF_ONLY_AST as _AST_FLAG from bisect import bisect_right from types import FrameType +from typing import Iterator from typing import List from typing import Optional from typing import Sequence @@ -73,6 +74,9 @@ class Source: newsource.lines = self.lines[key.start : key.stop] return newsource + def __iter__(self) -> Iterator[str]: + return iter(self.lines) + def __len__(self) -> int: return len(self.lines) From 562d4811d59e495bdfd3123a7f725d55462769ec Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 16:26:46 +0200 Subject: [PATCH 42/95] Add type annotations to _pytest.compat --- src/_pytest/compat.py | 47 ++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 09e621c5d..fc810b3e5 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -10,11 +10,14 @@ import sys from contextlib import contextmanager from inspect import Parameter from inspect import signature +from typing import Any from typing import Callable from typing import Generic from typing import Optional from typing import overload +from typing import Tuple from typing import TypeVar +from typing import Union import attr import py @@ -46,7 +49,7 @@ else: import importlib_metadata # noqa: F401 -def _format_args(func): +def _format_args(func: Callable[..., Any]) -> str: return str(signature(func)) @@ -67,12 +70,12 @@ else: fspath = os.fspath -def is_generator(func): +def is_generator(func: object) -> bool: genfunc = inspect.isgeneratorfunction(func) return genfunc and not iscoroutinefunction(func) -def iscoroutinefunction(func): +def iscoroutinefunction(func: object) -> bool: """ 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 @@ -85,7 +88,7 @@ def iscoroutinefunction(func): return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False) -def getlocation(function, curdir=None): +def getlocation(function, curdir=None) -> str: function = get_real_func(function) fn = py.path.local(inspect.getfile(function)) lineno = function.__code__.co_firstlineno @@ -94,7 +97,7 @@ def getlocation(function, curdir=None): return "%s:%d" % (fn, lineno + 1) -def num_mock_patch_args(function): +def num_mock_patch_args(function) -> int: """ return number of arguments used up by mock arguments (if any) """ patchings = getattr(function, "patchings", None) if not patchings: @@ -113,7 +116,13 @@ def num_mock_patch_args(function): ) -def getfuncargnames(function, *, name: str = "", is_method=False, cls=None): +def getfuncargnames( + function: Callable[..., Any], + *, + name: str = "", + is_method: bool = False, + cls: Optional[type] = None +) -> Tuple[str, ...]: """Returns the names of a function's mandatory arguments. This should return the names of all function arguments that: @@ -181,7 +190,7 @@ else: from contextlib import nullcontext # noqa -def get_default_arg_names(function): +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 return tuple( @@ -200,18 +209,18 @@ _non_printable_ascii_translate_table.update( ) -def _translate_non_printable(s): +def _translate_non_printable(s: str) -> str: return s.translate(_non_printable_ascii_translate_table) STRING_TYPES = bytes, str -def _bytes_to_ascii(val): +def _bytes_to_ascii(val: bytes) -> str: return val.decode("ascii", "backslashreplace") -def ascii_escaped(val): +def ascii_escaped(val: Union[bytes, str]): """If val is pure ascii, returns it as a str(). Otherwise, escapes bytes objects into a sequence of escaped bytes: @@ -308,7 +317,7 @@ def getimfunc(func): return func -def safe_getattr(object, name, default): +def safe_getattr(object: Any, name: str, default: Any) -> Any: """ Like getattr but return default upon any Exception or any OutcomeException. Attribute access can potentially fail for 'evil' Python objects. @@ -322,7 +331,7 @@ def safe_getattr(object, name, default): return default -def safe_isclass(obj): +def safe_isclass(obj: object) -> bool: """Ignore any exception via isinstance on Python 3.""" try: return inspect.isclass(obj) @@ -343,21 +352,23 @@ COLLECT_FAKEMODULE_ATTRIBUTES = ( ) -def _setup_collect_fakemodule(): +def _setup_collect_fakemodule() -> None: from types import ModuleType import pytest - pytest.collect = ModuleType("pytest.collect") - pytest.collect.__all__ = [] # used for setns + # Types ignored because the module is created dynamically. + pytest.collect = ModuleType("pytest.collect") # type: ignore + pytest.collect.__all__ = [] # type: ignore # used for setns for attr_name in COLLECT_FAKEMODULE_ATTRIBUTES: - setattr(pytest.collect, attr_name, getattr(pytest, attr_name)) + setattr(pytest.collect, attr_name, getattr(pytest, attr_name)) # type: ignore class CaptureIO(io.TextIOWrapper): - def __init__(self): + def __init__(self) -> None: super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) - def getvalue(self): + def getvalue(self) -> str: + assert isinstance(self.buffer, io.BytesIO) return self.buffer.getvalue().decode("UTF-8") From 786d839db1e584e33f2f0543b6c16cdfeefe11ee Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Nov 2019 17:17:57 +0100 Subject: [PATCH 43/95] cacheprovider: set: use json.dumps + write ``json.dump`` is slower since it iterates over chunks [1]. For 100 ``cache.set`` calls this saved ~0.5s (2.5s => 2s), using a dict with 1500 entries, and an encoded size of 500kb. Python 3.7.4. 1: https://github.com/blueyed/cpython/blob/1c2e81ed00/Lib/json/__init__.py#L177-L180 --- changelog/6206.improvement.rst | 1 + src/_pytest/cacheprovider.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog/6206.improvement.rst diff --git a/changelog/6206.improvement.rst b/changelog/6206.improvement.rst new file mode 100644 index 000000000..67d8363b3 --- /dev/null +++ b/changelog/6206.improvement.rst @@ -0,0 +1 @@ +cacheprovider: improved robustness and performance with ``cache.set``. diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 3c60fdb33..6e53545d6 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -125,13 +125,14 @@ class Cache: return if not cache_dir_exists_already: self._ensure_supporting_files() + data = json.dumps(value, indent=2, sort_keys=True) try: f = path.open("w") except (IOError, OSError): self.warn("cache could not write path {path}", path=path) else: with f: - json.dump(value, f, indent=2, sort_keys=True) + f.write(data) def _ensure_supporting_files(self): """Create supporting files in the cache dir that are not really part of the cache.""" From 5d247b9caf5c74ced8fcf48950fd4a15b678c7f6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Nov 2019 18:42:17 +0100 Subject: [PATCH 44/95] pre-commit: upgrade black This brings https://github.com/psf/black/pull/826, which helps with https://github.com/psf/black/issues/601. --- .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 8481848f7..8210ef5d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ exclude: doc/en/example/py2py3/test_py2.py repos: - repo: https://github.com/psf/black - rev: 19.3b0 + rev: 19.10b0 hooks: - id: black args: [--safe, --quiet] From b1a597ab0292a1360a750f7df8cd9bf498e2cd72 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Nov 2019 18:51:02 +0100 Subject: [PATCH 45/95] Remove (now) unnecessary fmt: off --- src/_pytest/config/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 06296fd35..d4521adf6 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -131,13 +131,13 @@ def directory_arg(path, optname): # Plugins that cannot be disabled via "-p no:X" currently. -essential_plugins = ( # fmt: off +essential_plugins = ( "mark", "main", "runner", "fixtures", "helpconfig", # Provides -p. -) # fmt: on +) default_plugins = essential_plugins + ( "python", From 54a954514b5a02b4858707ef653a1a204cd05509 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Nov 2019 18:53:29 +0100 Subject: [PATCH 46/95] re-run black --- src/_pytest/config/argparsing.py | 2 +- src/_pytest/pytester.py | 2 +- testing/python/fixtures.py | 6 +++--- testing/python/raises.py | 2 +- testing/test_collection.py | 8 ++++---- testing/test_mark.py | 2 +- testing/test_skipping.py | 2 +- testing/test_tmpdir.py | 2 +- testing/test_unittest.py | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 4eec6be05..7cbb676bd 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -395,7 +395,7 @@ class MyOptionParser(argparse.ArgumentParser): options = ", ".join(option for _, option, _ in option_tuples) self.error(msg % {"option": arg_string, "matches": options}) elif len(option_tuples) == 1: - option_tuple, = option_tuples + (option_tuple,) = option_tuples return option_tuple if self._negative_number_matcher.match(arg_string): if not self._has_negative_number_optionals: diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index ca780a9f5..02414a299 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -312,7 +312,7 @@ class HookRecorder: return self.getfailures("pytest_collectreport") def listoutcomes( - self + self, ) -> Tuple[List[TestReport], List[TestReport], List[TestReport]]: passed = [] skipped = [] diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 6dca793e0..52fd32cc4 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -503,7 +503,7 @@ class TestRequestBasic: assert repr(req).find(req.function.__name__) != -1 def test_request_attributes_method(self, testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ import pytest class TestB(object): @@ -531,7 +531,7 @@ class TestRequestBasic: pass """ ) - item1, = testdir.genitems([modcol]) + (item1,) = testdir.genitems([modcol]) assert item1.name == "test_method" arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs assert len(arg2fixturedefs) == 1 @@ -781,7 +781,7 @@ class TestRequestBasic: def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") - item, = testdir.genitems([modcol]) + (item,) = testdir.genitems([modcol]) req = fixtures.FixtureRequest(item) assert req.fspath == modcol.fspath diff --git a/testing/python/raises.py b/testing/python/raises.py index 28b0715c0..1c701796a 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -205,7 +205,7 @@ class TestRaises: with pytest.raises(AssertionError) as excinfo: with pytest.raises(AssertionError, match="'foo"): raise AssertionError("'bar") - msg, = excinfo.value.args + (msg,) = excinfo.value.args assert msg == 'Pattern "\'foo" not found in "\'bar"' def test_raises_match_wrong_type(self): diff --git a/testing/test_collection.py b/testing/test_collection.py index f18d36d24..e4a70b1a7 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -486,7 +486,7 @@ class TestSession: p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) items, hookrec = testdir.inline_genitems(id) - item, = items + (item,) = items assert item.name == "test_func" newid = item.nodeid assert newid == id @@ -605,9 +605,9 @@ class TestSession: testdir.makepyfile("def test_func(): pass") items, hookrec = testdir.inline_genitems() assert len(items) == 1 - item, = items + (item,) = items items2, hookrec = testdir.inline_genitems(item.nodeid) - item2, = items2 + (item2,) = items2 assert item2.name == item.name assert item2.fspath == item.fspath @@ -622,7 +622,7 @@ class TestSession: arg = p.basename + "::TestClass::test_method" items, hookrec = testdir.inline_genitems(arg) assert len(items) == 1 - item, = items + (item,) = items assert item.nodeid.endswith("TestClass::test_method") # 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"] diff --git a/testing/test_mark.py b/testing/test_mark.py index ba7599804..0e4422025 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1011,7 +1011,7 @@ def test_markers_from_parametrize(testdir): def test_pytest_param_id_requires_string(): with pytest.raises(TypeError) as excinfo: pytest.param(id=True) - msg, = excinfo.value.args + (msg,) = excinfo.value.args assert msg == "Expected id to be a string, got : True" diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 86f328a93..67714d030 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -115,7 +115,7 @@ class TestEvaluator: ) def test_skipif_class(self, testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ import pytest class TestClass(object): diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 29b6db947..eb1c1f300 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -258,7 +258,7 @@ class TestNumberedDir: registry = [] register_cleanup_lock_removal(lock, register=registry.append) - cleanup_func, = registry + (cleanup_func,) = registry assert lock.is_file() diff --git a/testing/test_unittest.py b/testing/test_unittest.py index f56284d85..885178402 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -383,7 +383,7 @@ def test_testcase_custom_exception_info(testdir, type): def test_testcase_totally_incompatible_exception_info(testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ from unittest import TestCase class MyTestCase(TestCase): From eaa34a9df0fa341c1b21bd3b232928812e6e8a06 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 23:02:55 +0200 Subject: [PATCH 47/95] Add type annotations to _pytest._code.code --- src/_pytest/_code/code.py | 240 ++++++++++++++++++++++------------- testing/code/test_code.py | 57 +++++---- testing/code/test_excinfo.py | 19 +-- testing/code/test_source.py | 197 +++++++++++++++------------- 4 files changed, 299 insertions(+), 214 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 334365042..a8f117366 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -7,13 +7,17 @@ from inspect import CO_VARKEYWORDS from io import StringIO from traceback import format_exception_only from types import CodeType +from types import FrameType from types import TracebackType from typing import Any +from typing import Callable from typing import Dict from typing import Generic +from typing import Iterable from typing import List from typing import Optional from typing import Pattern +from typing import Sequence from typing import Set from typing import Tuple from typing import TypeVar @@ -27,9 +31,16 @@ import py import _pytest from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr +from _pytest.compat import overload if False: # TYPE_CHECKING from typing import Type + from typing_extensions import Literal + from weakref import ReferenceType # noqa: F401 + + from _pytest._code import Source + + _TracebackStyle = Literal["long", "short", "no", "native"] class Code: @@ -38,13 +49,12 @@ class Code: def __init__(self, rawcode) -> None: if not hasattr(rawcode, "co_filename"): rawcode = getrawcode(rawcode) - try: - self.filename = rawcode.co_filename - self.firstlineno = rawcode.co_firstlineno - 1 - self.name = rawcode.co_name - except AttributeError: + if not isinstance(rawcode, CodeType): raise TypeError("not a code object: {!r}".format(rawcode)) - self.raw = rawcode # type: CodeType + self.filename = rawcode.co_filename + self.firstlineno = rawcode.co_firstlineno - 1 + self.name = rawcode.co_name + self.raw = rawcode def __eq__(self, other): return self.raw == other.raw @@ -72,7 +82,7 @@ class Code: return p @property - def fullsource(self): + def fullsource(self) -> Optional["Source"]: """ return a _pytest._code.Source object for the full source file of the code """ from _pytest._code import source @@ -80,7 +90,7 @@ class Code: full, _ = source.findsource(self.raw) return full - def source(self): + def source(self) -> "Source": """ return a _pytest._code.Source object for the code object's source only """ # return source only for that part of code @@ -88,7 +98,7 @@ class Code: return _pytest._code.Source(self.raw) - def getargs(self, var=False): + def getargs(self, var: bool = False) -> Tuple[str, ...]: """ return a tuple with the argument names for the code object if 'var' is set True also return the names of the variable and @@ -107,7 +117,7 @@ class Frame: """Wrapper around a Python frame holding f_locals and f_globals in which expressions can be evaluated.""" - def __init__(self, frame): + def __init__(self, frame: FrameType) -> None: self.lineno = frame.f_lineno - 1 self.f_globals = frame.f_globals self.f_locals = frame.f_locals @@ -115,7 +125,7 @@ class Frame: self.code = Code(frame.f_code) @property - def statement(self): + def statement(self) -> "Source": """ statement this frame is at """ import _pytest._code @@ -134,7 +144,7 @@ class Frame: f_locals.update(vars) return eval(code, self.f_globals, f_locals) - def exec_(self, code, **vars): + def exec_(self, code, **vars) -> None: """ exec 'code' in the frame 'vars' are optional; additional local variables @@ -143,7 +153,7 @@ class Frame: f_locals.update(vars) exec(code, self.f_globals, f_locals) - def repr(self, object): + def repr(self, object: object) -> str: """ return a 'safe' (non-recursive, one-line) string repr for 'object' """ return saferepr(object) @@ -151,7 +161,7 @@ class Frame: def is_true(self, object): return object - def getargs(self, var=False): + def getargs(self, var: bool = False): """ return a list of tuples (name, value) for all arguments if 'var' is set True also include the variable and keyword @@ -169,35 +179,34 @@ class Frame: class TracebackEntry: """ a single entry in a traceback """ - _repr_style = None + _repr_style = None # type: Optional[Literal["short", "long"]] exprinfo = None - def __init__(self, rawentry, excinfo=None): + def __init__(self, rawentry: TracebackType, excinfo=None) -> None: self._excinfo = excinfo self._rawentry = rawentry self.lineno = rawentry.tb_lineno - 1 - def set_repr_style(self, mode): + def set_repr_style(self, mode: "Literal['short', 'long']") -> None: assert mode in ("short", "long") self._repr_style = mode @property - def frame(self): - import _pytest._code - - return _pytest._code.Frame(self._rawentry.tb_frame) + def frame(self) -> Frame: + return Frame(self._rawentry.tb_frame) @property - def relline(self): + def relline(self) -> int: return self.lineno - self.frame.code.firstlineno - def __repr__(self): + def __repr__(self) -> str: return "" % (self.frame.code.path, self.lineno + 1) @property - def statement(self): + def statement(self) -> "Source": """ _pytest._code.Source object for the current statement """ source = self.frame.code.fullsource + assert source is not None return source.getstatement(self.lineno) @property @@ -206,14 +215,14 @@ class TracebackEntry: return self.frame.code.path @property - def locals(self): + def locals(self) -> Dict[str, Any]: """ locals of underlying frame """ return self.frame.f_locals - def getfirstlinesource(self): + def getfirstlinesource(self) -> int: return self.frame.code.firstlineno - def getsource(self, astcache=None): + def getsource(self, astcache=None) -> Optional["Source"]: """ return failing source code. """ # we use the passed in astcache to not reparse asttrees # within exception info printing @@ -258,7 +267,7 @@ class TracebackEntry: return tbh(None if self._excinfo is None else self._excinfo()) return tbh - def __str__(self): + def __str__(self) -> str: try: fn = str(self.path) except py.error.Error: @@ -273,31 +282,42 @@ class TracebackEntry: return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line) @property - def name(self): + def name(self) -> str: """ co_name of underlying code """ return self.frame.code.raw.co_name -class Traceback(list): +class Traceback(List[TracebackEntry]): """ Traceback objects encapsulate and offer higher level access to Traceback entries. """ - def __init__(self, tb, excinfo=None): + def __init__( + self, + tb: Union[TracebackType, Iterable[TracebackEntry]], + excinfo: Optional["ReferenceType[ExceptionInfo]"] = None, + ) -> None: """ initialize from given python traceback object and ExceptionInfo """ self._excinfo = excinfo - if hasattr(tb, "tb_next"): + if isinstance(tb, TracebackType): - def f(cur): - while cur is not None: - yield TracebackEntry(cur, excinfo=excinfo) - cur = cur.tb_next + def f(cur: TracebackType) -> Iterable[TracebackEntry]: + cur_ = cur # type: Optional[TracebackType] + while cur_ is not None: + yield TracebackEntry(cur_, excinfo=excinfo) + cur_ = cur_.tb_next - list.__init__(self, f(tb)) + super().__init__(f(tb)) else: - list.__init__(self, tb) + super().__init__(tb) - def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None): + def cut( + self, + path=None, + lineno: Optional[int] = None, + firstlineno: Optional[int] = None, + excludepath=None, + ) -> "Traceback": """ return a Traceback instance wrapping part of this Traceback by providing any combination of path, lineno and firstlineno, the @@ -323,13 +343,25 @@ class Traceback(list): return Traceback(x._rawentry, self._excinfo) return self - def __getitem__(self, key): - val = super().__getitem__(key) - if isinstance(key, type(slice(0))): - val = self.__class__(val) - return val + @overload + def __getitem__(self, key: int) -> TracebackEntry: + raise NotImplementedError() - def filter(self, fn=lambda x: not x.ishidden()): + @overload # noqa: F811 + def __getitem__(self, key: slice) -> "Traceback": # noqa: F811 + raise NotImplementedError() + + def __getitem__( # noqa: F811 + self, key: Union[int, slice] + ) -> Union[TracebackEntry, "Traceback"]: + if isinstance(key, slice): + return self.__class__(super().__getitem__(key)) + else: + return super().__getitem__(key) + + def filter( + self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden() + ) -> "Traceback": """ return a Traceback instance with certain items removed fn is a function that gets a single argument, a TracebackEntry @@ -341,7 +373,7 @@ class Traceback(list): """ return Traceback(filter(fn, self), self._excinfo) - def getcrashentry(self): + def getcrashentry(self) -> TracebackEntry: """ return last non-hidden traceback entry that lead to the exception of a traceback. """ @@ -351,7 +383,7 @@ class Traceback(list): return entry return self[-1] - def recursionindex(self): + def recursionindex(self) -> Optional[int]: """ return the index of the frame/TracebackEntry where recursion originates if appropriate, None if no recursion occurred """ @@ -541,7 +573,7 @@ class ExceptionInfo(Generic[_E]): def getrepr( self, showlocals: bool = False, - style: str = "long", + style: "_TracebackStyle" = "long", abspath: bool = False, tbfilter: bool = True, funcargs: bool = False, @@ -619,16 +651,16 @@ class FormattedExcinfo: flow_marker = ">" fail_marker = "E" - showlocals = attr.ib(default=False) - style = attr.ib(default="long") - abspath = attr.ib(default=True) - tbfilter = attr.ib(default=True) - funcargs = attr.ib(default=False) - truncate_locals = attr.ib(default=True) - chain = attr.ib(default=True) + showlocals = attr.ib(type=bool, default=False) + style = attr.ib(type="_TracebackStyle", default="long") + abspath = attr.ib(type=bool, default=True) + tbfilter = attr.ib(type=bool, default=True) + funcargs = attr.ib(type=bool, default=False) + truncate_locals = attr.ib(type=bool, default=True) + chain = attr.ib(type=bool, default=True) astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False) - def _getindent(self, source): + def _getindent(self, source: "Source") -> int: # figure out indent for given source try: s = str(source.getstatement(len(source) - 1)) @@ -643,20 +675,27 @@ class FormattedExcinfo: return 0 return 4 + (len(s) - len(s.lstrip())) - def _getentrysource(self, entry): + def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]: source = entry.getsource(self.astcache) if source is not None: source = source.deindent() return source - def repr_args(self, entry): + def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]: if self.funcargs: args = [] for argname, argvalue in entry.frame.getargs(var=True): args.append((argname, saferepr(argvalue))) return ReprFuncArgs(args) + return None - def get_source(self, source, line_index=-1, excinfo=None, short=False) -> List[str]: + def get_source( + self, + source: "Source", + line_index: int = -1, + excinfo: Optional[ExceptionInfo] = None, + short: bool = False, + ) -> List[str]: """ return formatted and marked up source lines. """ import _pytest._code @@ -680,19 +719,21 @@ class FormattedExcinfo: lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) return lines - def get_exconly(self, excinfo, indent=4, markall=False): + def get_exconly( + self, excinfo: ExceptionInfo, indent: int = 4, markall: bool = False + ) -> List[str]: lines = [] - indent = " " * indent + indentstr = " " * indent # get the real exception information out exlines = excinfo.exconly(tryshort=True).split("\n") - failindent = self.fail_marker + indent[1:] + failindent = self.fail_marker + indentstr[1:] for line in exlines: lines.append(failindent + line) if not markall: - failindent = indent + failindent = indentstr return lines - def repr_locals(self, locals): + def repr_locals(self, locals: Dict[str, object]) -> Optional["ReprLocals"]: if self.showlocals: lines = [] keys = [loc for loc in locals if loc[0] != "@"] @@ -717,8 +758,11 @@ class FormattedExcinfo: # # XXX # pprint.pprint(value, stream=self.excinfowriter) return ReprLocals(lines) + return None - def repr_traceback_entry(self, entry, excinfo=None): + def repr_traceback_entry( + self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None + ) -> "ReprEntry": import _pytest._code source = self._getentrysource(entry) @@ -729,9 +773,7 @@ class FormattedExcinfo: line_index = entry.lineno - entry.getfirstlinesource() lines = [] # type: List[str] - style = entry._repr_style - if style is None: - style = self.style + style = entry._repr_style if entry._repr_style is not None else self.style if style in ("short", "long"): short = style == "short" reprargs = self.repr_args(entry) if not short else None @@ -761,7 +803,7 @@ class FormattedExcinfo: path = np return path - def repr_traceback(self, excinfo): + def repr_traceback(self, excinfo: ExceptionInfo) -> "ReprTraceback": traceback = excinfo.traceback if self.tbfilter: traceback = traceback.filter() @@ -779,7 +821,9 @@ class FormattedExcinfo: entries.append(reprentry) return ReprTraceback(entries, extraline, style=self.style) - def _truncate_recursive_traceback(self, traceback): + 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. @@ -806,7 +850,9 @@ class FormattedExcinfo: max_frames=max_frames, total=len(traceback), ) # type: Optional[str] - traceback = traceback[:max_frames] + traceback[-max_frames:] + # 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 else: if recursionindex is not None: extraline = "!!! Recursion detected (same locals & position)" @@ -863,7 +909,7 @@ class FormattedExcinfo: class TerminalRepr: - def __str__(self): + def __str__(self) -> str: # FYI this is called from pytest-xdist's serialization of exception # information. io = StringIO() @@ -871,7 +917,7 @@ class TerminalRepr: self.toterminal(tw) return io.getvalue().strip() - def __repr__(self): + def __repr__(self) -> str: return "<{} instance at {:0x}>".format(self.__class__, id(self)) def toterminal(self, tw) -> None: @@ -882,7 +928,7 @@ class ExceptionRepr(TerminalRepr): def __init__(self) -> None: self.sections = [] # type: List[Tuple[str, str, str]] - def addsection(self, name, content, sep="-"): + def addsection(self, name: str, content: str, sep: str = "-") -> None: self.sections.append((name, content, sep)) def toterminal(self, tw) -> None: @@ -892,7 +938,12 @@ class ExceptionRepr(TerminalRepr): class ExceptionChainRepr(ExceptionRepr): - def __init__(self, chain): + def __init__( + self, + chain: Sequence[ + Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]] + ], + ) -> None: super().__init__() self.chain = chain # reprcrash and reprtraceback of the outermost (the newest) exception @@ -910,7 +961,9 @@ class ExceptionChainRepr(ExceptionRepr): class ReprExceptionInfo(ExceptionRepr): - def __init__(self, reprtraceback, reprcrash): + def __init__( + self, reprtraceback: "ReprTraceback", reprcrash: "ReprFileLocation" + ) -> None: super().__init__() self.reprtraceback = reprtraceback self.reprcrash = reprcrash @@ -923,7 +976,12 @@ class ReprExceptionInfo(ExceptionRepr): class ReprTraceback(TerminalRepr): entrysep = "_ " - def __init__(self, reprentries, extraline, style): + def __init__( + self, + reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]], + extraline: Optional[str], + style: "_TracebackStyle", + ) -> None: self.reprentries = reprentries self.extraline = extraline self.style = style @@ -948,16 +1006,16 @@ class ReprTraceback(TerminalRepr): class ReprTracebackNative(ReprTraceback): - def __init__(self, tblines): + def __init__(self, tblines: Sequence[str]) -> None: self.style = "native" self.reprentries = [ReprEntryNative(tblines)] self.extraline = None class ReprEntryNative(TerminalRepr): - style = "native" + style = "native" # type: _TracebackStyle - def __init__(self, tblines): + def __init__(self, tblines: Sequence[str]) -> None: self.lines = tblines def toterminal(self, tw) -> None: @@ -965,7 +1023,14 @@ class ReprEntryNative(TerminalRepr): class ReprEntry(TerminalRepr): - def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): + def __init__( + self, + lines: Sequence[str], + reprfuncargs: Optional["ReprFuncArgs"], + reprlocals: Optional["ReprLocals"], + filelocrepr: Optional["ReprFileLocation"], + style: "_TracebackStyle", + ) -> None: self.lines = lines self.reprfuncargs = reprfuncargs self.reprlocals = reprlocals @@ -974,6 +1039,7 @@ class ReprEntry(TerminalRepr): def toterminal(self, tw) -> None: if self.style == "short": + assert self.reprfileloc is not None self.reprfileloc.toterminal(tw) for line in self.lines: red = line.startswith("E ") @@ -992,14 +1058,14 @@ class ReprEntry(TerminalRepr): tw.line("") self.reprfileloc.toterminal(tw) - def __str__(self): + def __str__(self) -> str: return "{}\n{}\n{}".format( "\n".join(self.lines), self.reprlocals, self.reprfileloc ) class ReprFileLocation(TerminalRepr): - def __init__(self, path, lineno, message): + def __init__(self, path, lineno: int, message: str) -> None: self.path = str(path) self.lineno = lineno self.message = message @@ -1016,7 +1082,7 @@ class ReprFileLocation(TerminalRepr): class ReprLocals(TerminalRepr): - def __init__(self, lines): + def __init__(self, lines: Sequence[str]) -> None: self.lines = lines def toterminal(self, tw) -> None: @@ -1025,7 +1091,7 @@ class ReprLocals(TerminalRepr): class ReprFuncArgs(TerminalRepr): - def __init__(self, args): + def __init__(self, args: Sequence[Tuple[str, object]]) -> None: self.args = args def toterminal(self, tw) -> None: @@ -1047,7 +1113,7 @@ class ReprFuncArgs(TerminalRepr): tw.line("") -def getrawcode(obj, trycall=True): +def getrawcode(obj, trycall: bool = True): """ return code object for given function. """ try: return obj.__code__ @@ -1075,7 +1141,7 @@ _PYTEST_DIR = py.path.local(_pytest.__file__).dirpath() _PY_DIR = py.path.local(py.__file__).dirpath() -def filter_traceback(entry): +def filter_traceback(entry: TracebackEntry) -> bool: """Return True if a TracebackEntry instance should be removed from tracebacks: * dynamically generated code (no code to show up for it); * internal traceback from pytest or its internal libraries, py and pluggy. diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 2f55720b4..f8e1ce17f 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -1,18 +1,19 @@ import sys +from types import FrameType from unittest import mock import _pytest._code import pytest -def test_ne(): +def test_ne() -> None: code1 = _pytest._code.Code(compile('foo = "bar"', "", "exec")) assert code1 == code1 code2 = _pytest._code.Code(compile('foo = "baz"', "", "exec")) assert code2 != code1 -def test_code_gives_back_name_for_not_existing_file(): +def test_code_gives_back_name_for_not_existing_file() -> None: name = "abc-123" co_code = compile("pass\n", name, "exec") assert co_code.co_filename == name @@ -21,68 +22,67 @@ def test_code_gives_back_name_for_not_existing_file(): assert code.fullsource is None -def test_code_with_class(): +def test_code_with_class() -> None: class A: pass pytest.raises(TypeError, _pytest._code.Code, A) -def x(): +def x() -> None: raise NotImplementedError() -def test_code_fullsource(): +def test_code_fullsource() -> None: code = _pytest._code.Code(x) full = code.fullsource assert "test_code_fullsource()" in str(full) -def test_code_source(): +def test_code_source() -> None: code = _pytest._code.Code(x) src = code.source() - expected = """def x(): + expected = """def x() -> None: raise NotImplementedError()""" assert str(src) == expected -def test_frame_getsourcelineno_myself(): - def func(): +def test_frame_getsourcelineno_myself() -> None: + def func() -> FrameType: return sys._getframe(0) - f = func() - f = _pytest._code.Frame(f) + f = _pytest._code.Frame(func()) source, lineno = f.code.fullsource, f.lineno + assert source is not None assert source[lineno].startswith(" return sys._getframe(0)") -def test_getstatement_empty_fullsource(): - def func(): +def test_getstatement_empty_fullsource() -> None: + def func() -> FrameType: return sys._getframe(0) - f = func() - f = _pytest._code.Frame(f) + f = _pytest._code.Frame(func()) with mock.patch.object(f.code.__class__, "fullsource", None): assert f.statement == "" -def test_code_from_func(): +def test_code_from_func() -> None: co = _pytest._code.Code(test_frame_getsourcelineno_myself) assert co.firstlineno assert co.path -def test_unicode_handling(): +def test_unicode_handling() -> None: value = "ąć".encode() - def f(): + def f() -> None: raise Exception(value) excinfo = pytest.raises(Exception, f) str(excinfo) -def test_code_getargs(): +def test_code_getargs() -> None: def f1(x): raise NotImplementedError() @@ -108,26 +108,26 @@ def test_code_getargs(): assert c4.getargs(var=True) == ("x", "y", "z") -def test_frame_getargs(): - def f1(x): +def test_frame_getargs() -> None: + def f1(x) -> FrameType: return sys._getframe(0) fr1 = _pytest._code.Frame(f1("a")) assert fr1.getargs(var=True) == [("x", "a")] - def f2(x, *y): + def f2(x, *y) -> FrameType: return sys._getframe(0) fr2 = _pytest._code.Frame(f2("a", "b", "c")) assert fr2.getargs(var=True) == [("x", "a"), ("y", ("b", "c"))] - def f3(x, **z): + def f3(x, **z) -> FrameType: return sys._getframe(0) fr3 = _pytest._code.Frame(f3("a", b="c")) assert fr3.getargs(var=True) == [("x", "a"), ("z", {"b": "c"})] - def f4(x, *y, **z): + def f4(x, *y, **z) -> FrameType: return sys._getframe(0) fr4 = _pytest._code.Frame(f4("a", "b", c="d")) @@ -135,7 +135,7 @@ def test_frame_getargs(): class TestExceptionInfo: - def test_bad_getsource(self): + def test_bad_getsource(self) -> None: try: if False: pass @@ -145,13 +145,13 @@ class TestExceptionInfo: exci = _pytest._code.ExceptionInfo.from_current() assert exci.getrepr() - def test_from_current_with_missing(self): + def test_from_current_with_missing(self) -> None: with pytest.raises(AssertionError, match="no current exception"): _pytest._code.ExceptionInfo.from_current() class TestTracebackEntry: - def test_getsource(self): + def test_getsource(self) -> None: try: if False: pass @@ -161,12 +161,13 @@ class TestTracebackEntry: exci = _pytest._code.ExceptionInfo.from_current() entry = exci.traceback[0] source = entry.getsource() + assert source is not None assert len(source) == 6 assert "assert False" in source[5] class TestReprFuncArgs: - def test_not_raise_exception_with_mixed_encoding(self, tw_mock): + def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None: from _pytest._code.code import ReprFuncArgs args = [("unicode_string", "São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")] diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index f08808648..997b14e2f 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -3,6 +3,7 @@ import os import queue import sys import textwrap +from typing import Union import py @@ -224,23 +225,25 @@ class TestTraceback_f_g_h: repr = excinfo.getrepr() assert "RuntimeError: hello" in str(repr.reprcrash) - def test_traceback_no_recursion_index(self): - def do_stuff(): + def test_traceback_no_recursion_index(self) -> None: + def do_stuff() -> None: raise RuntimeError - def reraise_me(): + def reraise_me() -> None: import sys exc, val, tb = sys.exc_info() + assert val is not None raise val.with_traceback(tb) - def f(n): + def f(n: int) -> None: try: do_stuff() except: # noqa reraise_me() excinfo = pytest.raises(RuntimeError, f, 8) + assert excinfo is not None traceback = excinfo.traceback recindex = traceback.recursionindex() assert recindex is None @@ -596,7 +599,6 @@ raise ValueError() assert lines[3] == "E world" assert not lines[4:] - loc = repr_entry.reprlocals is not None loc = repr_entry.reprfileloc assert loc.path == mod.__file__ assert loc.lineno == 3 @@ -1286,9 +1288,10 @@ raise ValueError() @pytest.mark.parametrize("style", ["short", "long"]) @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"]) def test_repr_traceback_with_unicode(style, encoding): - msg = "☹" - if encoding is not None: - msg = msg.encode(encoding) + if encoding is None: + msg = "☹" # type: Union[str, bytes] + else: + msg = "☹".encode(encoding) try: raise RuntimeError(msg) except RuntimeError: diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 519344dd4..bf52dccd7 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -4,13 +4,16 @@ import ast import inspect import sys +from typing import Any +from typing import Dict +from typing import Optional import _pytest._code import pytest from _pytest._code import Source -def test_source_str_function(): +def test_source_str_function() -> None: x = Source("3") assert str(x) == "3" @@ -25,7 +28,7 @@ def test_source_str_function(): assert str(x) == "\n3" -def test_unicode(): +def test_unicode() -> None: x = Source("4") assert str(x) == "4" co = _pytest._code.compile('"å"', mode="eval") @@ -33,12 +36,12 @@ def test_unicode(): assert isinstance(val, str) -def test_source_from_function(): +def test_source_from_function() -> None: source = _pytest._code.Source(test_source_str_function) - assert str(source).startswith("def test_source_str_function():") + assert str(source).startswith("def test_source_str_function() -> None:") -def test_source_from_method(): +def test_source_from_method() -> None: class TestClass: def test_method(self): pass @@ -47,13 +50,13 @@ def test_source_from_method(): assert source.lines == ["def test_method(self):", " pass"] -def test_source_from_lines(): +def test_source_from_lines() -> None: lines = ["a \n", "b\n", "c"] source = _pytest._code.Source(lines) assert source.lines == ["a ", "b", "c"] -def test_source_from_inner_function(): +def test_source_from_inner_function() -> None: def f(): pass @@ -63,7 +66,7 @@ def test_source_from_inner_function(): assert str(source).startswith("def f():") -def test_source_putaround_simple(): +def test_source_putaround_simple() -> None: source = Source("raise ValueError") source = source.putaround( "try:", @@ -85,7 +88,7 @@ else: ) -def test_source_putaround(): +def test_source_putaround() -> None: source = Source() source = source.putaround( """ @@ -96,28 +99,29 @@ def test_source_putaround(): assert str(source).strip() == "if 1:\n x=1" -def test_source_strips(): +def test_source_strips() -> None: source = Source("") assert source == Source() assert str(source) == "" assert source.strip() == source -def test_source_strip_multiline(): +def test_source_strip_multiline() -> None: source = Source() source.lines = ["", " hello", " "] source2 = source.strip() assert source2.lines == [" hello"] -def test_syntaxerror_rerepresentation(): +def test_syntaxerror_rerepresentation() -> None: ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz") + assert ex is not None assert ex.value.lineno == 1 assert ex.value.offset in {5, 7} # cpython: 7, pypy3.6 7.1.1: 5 - assert ex.value.text.strip(), "x x" + assert ex.value.text == "xyz xyz\n" -def test_isparseable(): +def test_isparseable() -> None: assert Source("hello").isparseable() assert Source("if 1:\n pass").isparseable() assert Source(" \nif 1:\n pass").isparseable() @@ -127,7 +131,7 @@ def test_isparseable(): class TestAccesses: - def setup_class(self): + def setup_class(self) -> None: self.source = Source( """\ def f(x): @@ -137,26 +141,26 @@ class TestAccesses: """ ) - def test_getrange(self): + def test_getrange(self) -> None: x = self.source[0:2] assert x.isparseable() assert len(x.lines) == 2 assert str(x) == "def f(x):\n pass" - def test_getline(self): + def test_getline(self) -> None: x = self.source[0] assert x == "def f(x):" - def test_len(self): + def test_len(self) -> None: assert len(self.source) == 4 - def test_iter(self): + def test_iter(self) -> None: values = [x for x in self.source] assert len(values) == 4 class TestSourceParsingAndCompiling: - def setup_class(self): + def setup_class(self) -> None: self.source = Source( """\ def f(x): @@ -166,19 +170,19 @@ class TestSourceParsingAndCompiling: """ ).strip() - def test_compile(self): + def test_compile(self) -> None: co = _pytest._code.compile("x=3") - d = {} + d = {} # type: Dict[str, Any] exec(co, d) assert d["x"] == 3 - def test_compile_and_getsource_simple(self): + def test_compile_and_getsource_simple(self) -> None: co = _pytest._code.compile("x=3") exec(co) source = _pytest._code.Source(co) assert str(source) == "x=3" - def test_compile_and_getsource_through_same_function(self): + def test_compile_and_getsource_through_same_function(self) -> None: def gensource(source): return _pytest._code.compile(source) @@ -199,7 +203,7 @@ class TestSourceParsingAndCompiling: source2 = inspect.getsource(co2) assert "ValueError" in source2 - def test_getstatement(self): + def test_getstatement(self) -> None: # print str(self.source) ass = str(self.source[1:]) for i in range(1, 4): @@ -208,7 +212,7 @@ class TestSourceParsingAndCompiling: # x = s.deindent() assert str(s) == ass - def test_getstatementrange_triple_quoted(self): + def test_getstatementrange_triple_quoted(self) -> None: # print str(self.source) source = Source( """hello(''' @@ -219,7 +223,7 @@ class TestSourceParsingAndCompiling: s = source.getstatement(1) assert s == str(source) - def test_getstatementrange_within_constructs(self): + def test_getstatementrange_within_constructs(self) -> None: source = Source( """\ try: @@ -241,7 +245,7 @@ class TestSourceParsingAndCompiling: # assert source.getstatementrange(5) == (0, 7) assert source.getstatementrange(6) == (6, 7) - def test_getstatementrange_bug(self): + def test_getstatementrange_bug(self) -> None: source = Source( """\ try: @@ -255,7 +259,7 @@ class TestSourceParsingAndCompiling: assert len(source) == 6 assert source.getstatementrange(2) == (1, 4) - def test_getstatementrange_bug2(self): + def test_getstatementrange_bug2(self) -> None: source = Source( """\ assert ( @@ -272,7 +276,7 @@ class TestSourceParsingAndCompiling: assert len(source) == 9 assert source.getstatementrange(5) == (0, 9) - def test_getstatementrange_ast_issue58(self): + def test_getstatementrange_ast_issue58(self) -> None: source = Source( """\ @@ -286,38 +290,44 @@ class TestSourceParsingAndCompiling: assert getstatement(2, source).lines == source.lines[2:3] assert getstatement(3, source).lines == source.lines[3:4] - def test_getstatementrange_out_of_bounds_py3(self): + def test_getstatementrange_out_of_bounds_py3(self) -> None: source = Source("if xxx:\n from .collections import something") r = source.getstatementrange(1) assert r == (1, 2) - def test_getstatementrange_with_syntaxerror_issue7(self): + def test_getstatementrange_with_syntaxerror_issue7(self) -> None: source = Source(":") pytest.raises(SyntaxError, lambda: source.getstatementrange(0)) - def test_compile_to_ast(self): + def test_compile_to_ast(self) -> None: source = Source("x = 4") mod = source.compile(flag=ast.PyCF_ONLY_AST) assert isinstance(mod, ast.Module) compile(mod, "", "exec") - def test_compile_and_getsource(self): + def test_compile_and_getsource(self) -> None: co = self.source.compile() exec(co, globals()) - f(7) - excinfo = pytest.raises(AssertionError, f, 6) + f(7) # type: ignore + excinfo = pytest.raises(AssertionError, f, 6) # type: ignore + assert excinfo is not None frame = excinfo.traceback[-1].frame + assert isinstance(frame.code.fullsource, Source) stmt = frame.code.fullsource.getstatement(frame.lineno) assert str(stmt).strip().startswith("assert") @pytest.mark.parametrize("name", ["", None, "my"]) - def test_compilefuncs_and_path_sanity(self, name): + def test_compilefuncs_and_path_sanity(self, name: Optional[str]) -> None: def check(comp, name): co = comp(self.source, name) if not name: - expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2) + expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2) # type: ignore else: - expected = "codegen %r %s:%d>" % (name, mypath, mylineno + 2 + 2) + expected = "codegen %r %s:%d>" % ( + name, + mypath, # type: ignore + mylineno + 2 + 2, # type: ignore + ) # type: ignore fn = co.co_filename assert fn.endswith(expected) @@ -332,9 +342,9 @@ class TestSourceParsingAndCompiling: pytest.raises(SyntaxError, _pytest._code.compile, "lambda a,a: 0", mode="eval") -def test_getstartingblock_singleline(): +def test_getstartingblock_singleline() -> None: class A: - def __init__(self, *args): + def __init__(self, *args) -> None: frame = sys._getframe(1) self.source = _pytest._code.Frame(frame).statement @@ -344,22 +354,22 @@ def test_getstartingblock_singleline(): assert len(values) == 1 -def test_getline_finally(): - def c(): +def test_getline_finally() -> None: + def c() -> None: pass with pytest.raises(TypeError) as excinfo: teardown = None try: - c(1) + c(1) # type: ignore finally: if teardown: teardown() source = excinfo.traceback[-1].statement - assert str(source).strip() == "c(1)" + assert str(source).strip() == "c(1) # type: ignore" -def test_getfuncsource_dynamic(): +def test_getfuncsource_dynamic() -> None: source = """ def f(): raise ValueError @@ -368,11 +378,13 @@ def test_getfuncsource_dynamic(): """ co = _pytest._code.compile(source) exec(co, globals()) - assert str(_pytest._code.Source(f)).strip() == "def f():\n raise ValueError" - assert str(_pytest._code.Source(g)).strip() == "def g(): pass" + f_source = _pytest._code.Source(f) # type: ignore + g_source = _pytest._code.Source(g) # type: ignore + assert str(f_source).strip() == "def f():\n raise ValueError" + assert str(g_source).strip() == "def g(): pass" -def test_getfuncsource_with_multine_string(): +def test_getfuncsource_with_multine_string() -> None: def f(): c = """while True: pass @@ -387,7 +399,7 @@ def test_getfuncsource_with_multine_string(): assert str(_pytest._code.Source(f)) == expected.rstrip() -def test_deindent(): +def test_deindent() -> None: from _pytest._code.source import deindent as deindent assert deindent(["\tfoo", "\tbar"]) == ["foo", "bar"] @@ -401,7 +413,7 @@ def test_deindent(): assert lines == ["def f():", " def g():", " pass"] -def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot): +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( @@ -423,7 +435,7 @@ if True: pass -def test_getsource_fallback(): +def test_getsource_fallback() -> None: from _pytest._code.source import getsource expected = """def x(): @@ -432,7 +444,7 @@ def test_getsource_fallback(): assert src == expected -def test_idem_compile_and_getsource(): +def test_idem_compile_and_getsource() -> None: from _pytest._code.source import getsource expected = "def x(): pass" @@ -441,15 +453,16 @@ def test_idem_compile_and_getsource(): assert src == expected -def test_findsource_fallback(): +def test_findsource_fallback() -> None: from _pytest._code.source import findsource src, lineno = findsource(x) + assert src is not None assert "test_findsource_simple" in str(src) assert src[lineno] == " def x():" -def test_findsource(): +def test_findsource() -> None: from _pytest._code.source import findsource co = _pytest._code.compile( @@ -460,19 +473,21 @@ def test_findsource(): ) src, lineno = findsource(co) + assert src is not None assert "if 1:" in str(src) - d = {} + d = {} # type: Dict[str, Any] eval(co, d) src, lineno = findsource(d["x"]) + assert src is not None assert "if 1:" in str(src) assert src[lineno] == " def x():" -def test_getfslineno(): +def test_getfslineno() -> None: from _pytest._code import getfslineno - def f(x): + def f(x) -> None: pass fspath, lineno = getfslineno(f) @@ -498,40 +513,40 @@ def test_getfslineno(): assert getfslineno(B)[1] == -1 -def test_code_of_object_instance_with_call(): +def test_code_of_object_instance_with_call() -> None: class A: pass pytest.raises(TypeError, lambda: _pytest._code.Source(A())) class WithCall: - def __call__(self): + def __call__(self) -> None: pass code = _pytest._code.Code(WithCall()) assert "pass" in str(code.source()) class Hello: - def __call__(self): + def __call__(self) -> None: pass pytest.raises(TypeError, lambda: _pytest._code.Code(Hello)) -def getstatement(lineno, source): +def getstatement(lineno: int, source) -> Source: from _pytest._code.source import getstatementrange_ast - source = _pytest._code.Source(source, deindent=False) - ast, start, end = getstatementrange_ast(lineno, source) - return source[start:end] + src = _pytest._code.Source(source, deindent=False) + ast, start, end = getstatementrange_ast(lineno, src) + return src[start:end] -def test_oneline(): +def test_oneline() -> None: source = getstatement(0, "raise ValueError") assert str(source) == "raise ValueError" -def test_comment_and_no_newline_at_end(): +def test_comment_and_no_newline_at_end() -> None: from _pytest._code.source import getstatementrange_ast source = Source( @@ -545,12 +560,12 @@ def test_comment_and_no_newline_at_end(): assert end == 2 -def test_oneline_and_comment(): +def test_oneline_and_comment() -> None: source = getstatement(0, "raise ValueError\n#hello") assert str(source) == "raise ValueError" -def test_comments(): +def test_comments() -> None: source = '''def test(): "comment 1" x = 1 @@ -576,7 +591,7 @@ comment 4 assert str(getstatement(line, source)) == '"""\ncomment 4\n"""' -def test_comment_in_statement(): +def test_comment_in_statement() -> None: source = """test(foo=1, # comment 1 bar=2) @@ -588,17 +603,17 @@ def test_comment_in_statement(): ) -def test_single_line_else(): +def test_single_line_else() -> None: source = getstatement(1, "if False: 2\nelse: 3") assert str(source) == "else: 3" -def test_single_line_finally(): +def test_single_line_finally() -> None: source = getstatement(1, "try: 1\nfinally: 3") assert str(source) == "finally: 3" -def test_issue55(): +def test_issue55() -> None: source = ( "def round_trip(dinp):\n assert 1 == dinp\n" 'def test_rt():\n round_trip("""\n""")\n' @@ -607,7 +622,7 @@ def test_issue55(): assert str(s) == ' round_trip("""\n""")' -def test_multiline(): +def test_multiline() -> None: source = getstatement( 0, """\ @@ -621,7 +636,7 @@ x = 3 class TestTry: - def setup_class(self): + def setup_class(self) -> None: self.source = """\ try: raise ValueError @@ -631,25 +646,25 @@ else: raise KeyError() """ - def test_body(self): + def test_body(self) -> None: source = getstatement(1, self.source) assert str(source) == " raise ValueError" - def test_except_line(self): + def test_except_line(self) -> None: source = getstatement(2, self.source) assert str(source) == "except Something:" - def test_except_body(self): + def test_except_body(self) -> None: source = getstatement(3, self.source) assert str(source) == " raise IndexError(1)" - def test_else(self): + def test_else(self) -> None: source = getstatement(5, self.source) assert str(source) == " raise KeyError()" class TestTryFinally: - def setup_class(self): + def setup_class(self) -> None: self.source = """\ try: raise ValueError @@ -657,17 +672,17 @@ finally: raise IndexError(1) """ - def test_body(self): + def test_body(self) -> None: source = getstatement(1, self.source) assert str(source) == " raise ValueError" - def test_finally(self): + def test_finally(self) -> None: source = getstatement(3, self.source) assert str(source) == " raise IndexError(1)" class TestIf: - def setup_class(self): + def setup_class(self) -> None: self.source = """\ if 1: y = 3 @@ -677,24 +692,24 @@ else: y = 7 """ - def test_body(self): + def test_body(self) -> None: source = getstatement(1, self.source) assert str(source) == " y = 3" - def test_elif_clause(self): + def test_elif_clause(self) -> None: source = getstatement(2, self.source) assert str(source) == "elif False:" - def test_elif(self): + def test_elif(self) -> None: source = getstatement(3, self.source) assert str(source) == " y = 5" - def test_else(self): + def test_else(self) -> None: source = getstatement(5, self.source) assert str(source) == " y = 7" -def test_semicolon(): +def test_semicolon() -> None: s = """\ hello ; pytest.skip() """ @@ -702,7 +717,7 @@ hello ; pytest.skip() assert str(source) == s.strip() -def test_def_online(): +def test_def_online() -> None: s = """\ def func(): raise ValueError(42) @@ -713,7 +728,7 @@ def something(): assert str(source) == "def func(): raise ValueError(42)" -def XXX_test_expression_multiline(): +def XXX_test_expression_multiline() -> None: source = """\ something ''' @@ -722,7 +737,7 @@ something assert str(result) == "'''\n'''" -def test_getstartingblock_multiline(): +def test_getstartingblock_multiline() -> None: class A: def __init__(self, *args): frame = sys._getframe(1) From 1b4623a6d16a17d228deda23d4242d944dff2397 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Nov 2019 23:41:12 +0100 Subject: [PATCH 48/95] tests: revisit test_cacheprovider --- testing/test_cacheprovider.py | 203 +++++++++------------------------- 1 file changed, 53 insertions(+), 150 deletions(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index ea78358d6..2d91e234b 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -2,7 +2,6 @@ import os import shutil import stat import sys -import textwrap import py @@ -65,13 +64,7 @@ class TestNewAPI: mode = os.stat(cache_dir)[stat.ST_MODE] testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) try: - testdir.makepyfile( - """ - def test_error(): - raise Exception - - """ - ) + testdir.makepyfile("def test_error(): raise Exception") result = testdir.runpytest("-rw") assert result.ret == 1 # warnings from nodeids, lastfailed, and stepwise @@ -178,12 +171,7 @@ def test_cache_reportheader_external_abspath(testdir, tmpdir_factory): "test_cache_reportheader_external_abspath_abs" ) - testdir.makepyfile( - """ - def test_hello(): - pass - """ - ) + testdir.makepyfile("def test_hello(): pass") testdir.makeini( """ [pytest] @@ -192,7 +180,6 @@ def test_cache_reportheader_external_abspath(testdir, tmpdir_factory): abscache=external_cache ) ) - result = testdir.runpytest("-v") result.stdout.fnmatch_lines( ["cachedir: {abscache}".format(abscache=external_cache)] @@ -256,33 +243,23 @@ class TestLastFailed: monkeypatch.setattr("sys.dont_write_bytecode", True) p = testdir.makepyfile( """ - def test_1(): - assert 0 - def test_2(): - assert 0 - def test_3(): - assert 1 - """ + def test_1(): assert 0 + def test_2(): assert 0 + def test_3(): assert 1 + """ ) - result = testdir.runpytest() + result = testdir.runpytest(str(p)) result.stdout.fnmatch_lines(["*2 failed*"]) - p.write( - textwrap.dedent( - """\ - def test_1(): - assert 1 - - def test_2(): - assert 1 - - def test_3(): - assert 0 - """ - ) + p = testdir.makepyfile( + """ + def test_1(): assert 1 + def test_2(): assert 1 + def test_3(): assert 0 + """ ) - result = testdir.runpytest("--lf") + result = testdir.runpytest(str(p), "--lf") result.stdout.fnmatch_lines(["*2 passed*1 desel*"]) - result = testdir.runpytest("--lf") + result = testdir.runpytest(str(p), "--lf") result.stdout.fnmatch_lines( [ "collected 3 items", @@ -290,7 +267,7 @@ class TestLastFailed: "*1 failed*2 passed*", ] ) - result = testdir.runpytest("--lf", "--cache-clear") + result = testdir.runpytest(str(p), "--lf", "--cache-clear") result.stdout.fnmatch_lines(["*1 failed*2 passed*"]) # Run this again to make sure clear-cache is robust @@ -300,21 +277,9 @@ class TestLastFailed: result.stdout.fnmatch_lines(["*1 failed*2 passed*"]) def test_failedfirst_order(self, testdir): - testdir.tmpdir.join("test_a.py").write( - textwrap.dedent( - """\ - def test_always_passes(): - assert 1 - """ - ) - ) - testdir.tmpdir.join("test_b.py").write( - textwrap.dedent( - """\ - def test_always_fails(): - assert 0 - """ - ) + testdir.makepyfile( + test_a="def test_always_passes(): pass", + test_b="def test_always_fails(): assert 0", ) result = testdir.runpytest() # Test order will be collection order; alphabetical @@ -325,16 +290,8 @@ class TestLastFailed: def test_lastfailed_failedfirst_order(self, testdir): testdir.makepyfile( - **{ - "test_a.py": """\ - def test_always_passes(): - assert 1 - """, - "test_b.py": """\ - def test_always_fails(): - assert 0 - """, - } + test_a="def test_always_passes(): assert 1", + test_b="def test_always_fails(): assert 0", ) result = testdir.runpytest() # Test order will be collection order; alphabetical @@ -347,16 +304,11 @@ class TestLastFailed: def test_lastfailed_difference_invocations(self, testdir, monkeypatch): monkeypatch.setattr("sys.dont_write_bytecode", True) testdir.makepyfile( - test_a="""\ - def test_a1(): - assert 0 - def test_a2(): - assert 1 - """, - test_b="""\ - def test_b1(): - assert 0 + 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") @@ -365,14 +317,8 @@ class TestLastFailed: result.stdout.fnmatch_lines(["*2 failed*"]) result = testdir.runpytest("--lf", p2) result.stdout.fnmatch_lines(["*1 failed*"]) - p2.write( - textwrap.dedent( - """\ - def test_b1(): - assert 1 - """ - ) - ) + + testdir.makepyfile(test_b="def test_b1(): assert 1") result = testdir.runpytest("--lf", p2) result.stdout.fnmatch_lines(["*1 passed*"]) result = testdir.runpytest("--lf", p) @@ -381,20 +327,9 @@ class TestLastFailed: def test_lastfailed_usecase_splice(self, testdir, monkeypatch): monkeypatch.setattr("sys.dont_write_bytecode", True) testdir.makepyfile( - """\ - def test_1(): - assert 0 - """ + "def test_1(): assert 0", test_something="def test_2(): assert 0" ) p2 = testdir.tmpdir.join("test_something.py") - p2.write( - textwrap.dedent( - """\ - def test_2(): - assert 0 - """ - ) - ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*2 failed*"]) result = testdir.runpytest("--lf", p2) @@ -436,18 +371,14 @@ class TestLastFailed: def test_terminal_report_lastfailed(self, testdir): test_a = testdir.makepyfile( test_a=""" - def test_a1(): - pass - def test_a2(): - pass + def test_a1(): pass + def test_a2(): pass """ ) test_b = testdir.makepyfile( test_b=""" - def test_b1(): - assert 0 - def test_b2(): - assert 0 + def test_b1(): assert 0 + def test_b2(): assert 0 """ ) result = testdir.runpytest() @@ -492,10 +423,8 @@ class TestLastFailed: def test_terminal_report_failedfirst(self, testdir): testdir.makepyfile( test_a=""" - def test_a1(): - assert 0 - def test_a2(): - pass + def test_a1(): assert 0 + def test_a2(): pass """ ) result = testdir.runpytest() @@ -542,7 +471,6 @@ class TestLastFailed: assert list(lastfailed) == ["test_maybe.py::test_hello"] def test_lastfailed_failure_subset(self, testdir, monkeypatch): - testdir.makepyfile( test_maybe=""" import os @@ -560,6 +488,7 @@ class TestLastFailed: env = os.environ if '1' == env['FAILIMPORT']: raise ImportError('fail') + def test_hello(): assert '0' == env['FAILTEST'] @@ -613,8 +542,7 @@ class TestLastFailed: """ import pytest @pytest.mark.xfail - def test(): - assert 0 + def test(): assert 0 """ ) result = testdir.runpytest() @@ -626,8 +554,7 @@ class TestLastFailed: """ import pytest @pytest.mark.xfail(strict=True) - def test(): - pass + def test(): pass """ ) result = testdir.runpytest() @@ -641,8 +568,7 @@ class TestLastFailed: testdir.makepyfile( """ import pytest - def test(): - assert 0 + def test(): assert 0 """ ) result = testdir.runpytest() @@ -655,8 +581,7 @@ class TestLastFailed: """ import pytest @pytest.{mark} - def test(): - assert 0 + def test(): assert 0 """.format( mark=mark ) @@ -694,18 +619,14 @@ class TestLastFailed: # 1. initial run test_bar = testdir.makepyfile( test_bar=""" - def test_bar_1(): - pass - def test_bar_2(): - assert 0 + def test_bar_1(): pass + def test_bar_2(): assert 0 """ ) test_foo = testdir.makepyfile( test_foo=""" - def test_foo_3(): - pass - def test_foo_4(): - assert 0 + def test_foo_3(): pass + def test_foo_4(): assert 0 """ ) testdir.runpytest() @@ -717,10 +638,8 @@ class TestLastFailed: # 2. fix test_bar_2, run only test_bar.py testdir.makepyfile( test_bar=""" - def test_bar_1(): - pass - def test_bar_2(): - pass + def test_bar_1(): pass + def test_bar_2(): pass """ ) result = testdir.runpytest(test_bar) @@ -735,10 +654,8 @@ class TestLastFailed: # 3. fix test_foo_4, run only test_foo.py test_foo = testdir.makepyfile( test_foo=""" - def test_foo_3(): - pass - def test_foo_4(): - pass + def test_foo_3(): pass + def test_foo_4(): pass """ ) result = testdir.runpytest(test_foo, "--last-failed") @@ -752,10 +669,8 @@ class TestLastFailed: def test_lastfailed_no_failures_behavior_all_passed(self, testdir): testdir.makepyfile( """ - def test_1(): - assert True - def test_2(): - assert True + def test_1(): pass + def test_2(): pass """ ) result = testdir.runpytest() @@ -777,10 +692,8 @@ class TestLastFailed: def test_lastfailed_no_failures_behavior_empty_cache(self, testdir): testdir.makepyfile( """ - def test_1(): - assert True - def test_2(): - assert False + def test_1(): pass + def test_2(): assert 0 """ ) result = testdir.runpytest("--lf", "--cache-clear") @@ -1022,22 +935,12 @@ class TestReadme: return readme.is_file() def test_readme_passed(self, testdir): - testdir.makepyfile( - """ - def test_always_passes(): - assert 1 - """ - ) + testdir.makepyfile("def test_always_passes(): pass") testdir.runpytest() assert self.check_readme(testdir) is True def test_readme_failed(self, testdir): - testdir.makepyfile( - """ - def test_always_fails(): - assert 0 - """ - ) + testdir.makepyfile("def test_always_fails(): assert 0") testdir.runpytest() assert self.check_readme(testdir) is True From b9a3ba1fe8a02b5093ad72785ab5d908e18e228c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Nov 2019 23:43:54 +0100 Subject: [PATCH 49/95] test_cache_writefail_permissions: ignore any other plugins --- testing/test_cacheprovider.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 2d91e234b..0e1194b02 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -59,7 +59,8 @@ class TestNewAPI: @pytest.mark.filterwarnings( "ignore:could not create cache path:pytest.PytestWarning" ) - def test_cache_failure_warns(self, testdir): + def test_cache_failure_warns(self, testdir, monkeypatch): + 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) From 9e759010d9e9bf6ff10766c54f37bdb6ce1266c2 Mon Sep 17 00:00:00 2001 From: JoshKarpel Date: Sun, 17 Nov 2019 16:45:42 -0600 Subject: [PATCH 50/95] resolve #2049 --- src/_pytest/setupplan.py | 3 +- testing/python/setup_plan.py | 103 +++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 697746f20..6fdd3aed0 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -16,7 +16,8 @@ def pytest_addoption(parser): def pytest_fixture_setup(fixturedef, request): # Will return a dummy fixture if the setuponly option is provided. if request.config.option.setupplan: - fixturedef.cached_result = (None, None, None) + my_cache_key = fixturedef.cache_key(request) + fixturedef.cached_result = (None, my_cache_key, None) return fixturedef.cached_result diff --git a/testing/python/setup_plan.py b/testing/python/setup_plan.py index e323ba240..8ff8bfc45 100644 --- a/testing/python/setup_plan.py +++ b/testing/python/setup_plan.py @@ -17,3 +17,106 @@ def test_show_fixtures_and_test(testdir, dummy_yaml_custom_test): result.stdout.fnmatch_lines( ["*SETUP F arg*", "*test_arg (fixtures used: arg)", "*TEARDOWN F 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 + correctly displays the SETUP/TEARDOWN indicators the right number of times. + + As reported in https://github.com/pytest-dev/pytest/issues/2049 + --setup-plan was showing SETUP/TEARDOWN on every test, even when the fixture + should persist through multiple tests. + + (Note that this bug never affected actual test execution, which used the + 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( + """ + import pytest + + @pytest.fixture(scope = 'class') + def fix(): + return object() + + class TestClass: + def test_one(self, fix): + assert False + + def test_two(self, fix): + assert False + """ + ) + + result = testdir.runpytest("--setup-plan") + assert result.ret == 0 + + setup_fragment = "SETUP C fix" + setup_count = 0 + + teardown_fragment = "TEARDOWN C fix" + teardown_count = 0 + + for line in result.stdout.lines: + if setup_fragment in line: + setup_count += 1 + if teardown_fragment in line: + teardown_count += 1 + + # before the fix this tests, there would have been a setup/teardown + # message for each test, so the counts would each have been 2 + assert setup_count == 1 + assert teardown_count == 1 + + +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. + """ + testdir.makepyfile( + """ + import pytest + + @pytest.fixture(scope = 'session') + def sess(): + return True + + @pytest.fixture(scope = 'module') + def mod(): + return True + + @pytest.fixture(scope = 'class') + def cls(): + return True + + @pytest.fixture(scope = 'function') + def func(): + return True + + + def test_outside(sess, mod, cls, func): + assert True + + + class TestCls: + def test_one(self, sess, mod, cls, func): + assert True + + def test_two(self, sess, mod, cls, func): + assert True + """ + ) + + plan_result = testdir.runpytest("--setup-plan") + show_result = testdir.runpytest("--setup-show") + + # the number and text of these lines should be identical + plan_lines = [ + l for l in plan_result.stdout.lines if "SETUP" in l or "TEARDOWN" in l + ] + show_lines = [ + l for l in show_result.stdout.lines if "SETUP" in l or "TEARDOWN" in l + ] + + assert plan_lines == show_lines From 6dfd683a0c34535c79b6baccdc60d301c64b4f76 Mon Sep 17 00:00:00 2001 From: JoshKarpel Date: Sun, 17 Nov 2019 16:47:09 -0600 Subject: [PATCH 51/95] changelog entry for #2049 --- AUTHORS | 1 + changelog/2049.bugfix.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/2049.bugfix.rst diff --git a/AUTHORS b/AUTHORS index d0e584f63..b9e6f7271 100644 --- a/AUTHORS +++ b/AUTHORS @@ -128,6 +128,7 @@ Jeff Widman Jenni Rinker John Eddie Ayson John Towler +Josh Karpel Jon Sonesen Jonas Obrist Jordan Guymon diff --git a/changelog/2049.bugfix.rst b/changelog/2049.bugfix.rst new file mode 100644 index 000000000..395396bd3 --- /dev/null +++ b/changelog/2049.bugfix.rst @@ -0,0 +1 @@ +Fix ``-setup-plan`` showing inaccurate information about fixture lifetimes. From 1e3be8ada4585ba279cfc24be8c3b09b25213804 Mon Sep 17 00:00:00 2001 From: JoshKarpel Date: Sun, 17 Nov 2019 17:14:17 -0600 Subject: [PATCH 52/95] fix whitespace issues in tests for #2049 --- testing/python/setup_plan.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/testing/python/setup_plan.py b/testing/python/setup_plan.py index 8ff8bfc45..a44474dd1 100644 --- a/testing/python/setup_plan.py +++ b/testing/python/setup_plan.py @@ -35,15 +35,12 @@ def test_show_multi_test_fixture_setup_and_teardown_correctly_simple(testdir): testdir.makepyfile( """ import pytest - @pytest.fixture(scope = 'class') def fix(): return object() - class TestClass: def test_one(self, fix): assert False - def test_two(self, fix): assert False """ @@ -77,32 +74,23 @@ def test_show_multi_test_fixture_setup_and_teardown_same_as_setup_show(testdir): testdir.makepyfile( """ import pytest - @pytest.fixture(scope = 'session') def sess(): return True - @pytest.fixture(scope = 'module') def mod(): return True - @pytest.fixture(scope = 'class') def cls(): return True - @pytest.fixture(scope = 'function') def func(): return True - - def test_outside(sess, mod, cls, func): assert True - - class TestCls: def test_one(self, sess, mod, cls, func): assert True - def test_two(self, sess, mod, cls, func): assert True """ From 46ffdf0e3a98bbb6ecb2393ee706ed8250f56ade Mon Sep 17 00:00:00 2001 From: Josh Karpel Date: Sun, 17 Nov 2019 17:17:47 -0600 Subject: [PATCH 53/95] Update AUTHORS --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index b9e6f7271..1f136d83e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -128,13 +128,13 @@ Jeff Widman Jenni Rinker John Eddie Ayson John Towler -Josh Karpel Jon Sonesen Jonas Obrist Jordan Guymon Jordan Moldow Jordan Speicher Joseph Hunkeler +Josh Karpel Joshua Bronson Jurko Gospodnetić Justyna Janczyszyn From 426a4cdca901606d3ffd716c063e394cec964f9f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 01:08:25 +0100 Subject: [PATCH 54/95] _idval: remove trailing newline from exception --- src/_pytest/python.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 30853de94..e6de5f3c6 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1156,8 +1156,7 @@ def _idval(val, argname, idx, idfn, item, config): if generated_id is not None: val = generated_id except Exception as e: - # See issue https://github.com/pytest-dev/pytest/issues/2169 - msg = "{}: error raised while trying to determine id of parameter '{}' at position {}\n" + msg = "{}: error raised while trying to determine id of parameter '{}' at position {}" msg = msg.format(item.nodeid, argname, idx) raise ValueError(msg) from e elif config: From f3a10245d0cfbad21c8c49d1ce7227afbcb0d716 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 16:20:41 +0100 Subject: [PATCH 55/95] Metafunc: remove unused _ids Forgotten in 40b85d7ee. --- src/_pytest/python.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 30853de94..2ce2f3f3a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -914,7 +914,6 @@ class Metafunc: self.cls = cls self._calls = [] - self._ids = set() self._arg2fixturedefs = fixtureinfo.name2fixturedefs @property From 91dec8e2bf5e4e9c43b040a8b2f286c8275f8141 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 16:35:34 +0100 Subject: [PATCH 56/95] Factor out _validate_parametrize_spelling This makes it easier to read `pytest_generate_tests`. --- src/_pytest/python.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 30853de94..f7436f23b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -119,14 +119,17 @@ def pytest_cmdline_main(config): return 0 -def pytest_generate_tests(metafunc): - # those alternative spellings are common - raise a specific error to alert - # the user - alt_spellings = ["parameterize", "parametrise", "parameterise"] - for mark_name in alt_spellings: +def _validate_parametrize_spelling(metafunc): + """Raise a specific error for common misspellings of "parametrize".""" + for mark_name in ["parameterize", "parametrise", "parameterise"]: if metafunc.definition.get_closest_marker(mark_name): msg = "{0} has '{1}' mark, spelling should be 'parametrize'" fail(msg.format(metafunc.function.__name__, mark_name), pytrace=False) + + +def pytest_generate_tests(metafunc): + _validate_parametrize_spelling(metafunc) + for marker in metafunc.definition.iter_markers(name="parametrize"): metafunc.parametrize(*marker.args, **marker.kwargs) From 5d5f48097922dae26da06a12482a7a74de7ba924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E7=8E=AE?= Date: Mon, 18 Nov 2019 23:46:18 +0800 Subject: [PATCH 57/95] Hardening an existing test for demonstrating this change. --- testing/test_collection.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/testing/test_collection.py b/testing/test_collection.py index 8050e80f9..fe5d66e94 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -685,6 +685,8 @@ class Test_genitems: def test_example_items1(self, testdir): p = testdir.makepyfile( """ + import pytest + def testone(): pass @@ -693,19 +695,24 @@ class Test_genitems: pass class TestY(TestX): - pass + @pytest.mark.parametrize("arg0", [".["]) + def testmethod_two(self, arg0): + pass """ ) items, reprec = testdir.inline_genitems(p) - assert len(items) == 3 + assert len(items) == 4 assert items[0].name == "testone" assert items[1].name == "testmethod_one" assert items[2].name == "testmethod_one" + 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" + # PR #6202: Fix incorrect result of getmodpath method. (Resolves issue #6189) + assert items[3].getmodpath() == "TestY.testmethod_two[.[]" s = items[0].getmodpath(stopatmodule=False) assert s.endswith("test_example_items1.testone") From b461010f32bb60cca1718a0542a4426f73a61758 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 18:12:13 +0100 Subject: [PATCH 58/95] mypy: config: use mypy_path=src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows for checking files inside of "testing" without having "src/…" as an argument also. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 0c0cb4861..42d5b9460 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,6 +63,7 @@ ignore = formats = sdist.tgz,bdist_wheel [mypy] +mypy_path = src ignore_missing_imports = True no_implicit_optional = True strict_equality = True From 2ad2fbc9a222f3ab2cab5379972c40d3f5ad2db1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 17:36:15 +0100 Subject: [PATCH 59/95] Metafunc: remove hack for DefinitionMock Done initially in 99015bfc8. --- src/_pytest/python.py | 16 ++++++++++------ testing/python/metafunc.py | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ebc6895f2..2e8756289 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -9,6 +9,7 @@ from collections import Counter from collections.abc import Sequence from functools import partial from textwrap import dedent +from typing import List from typing import Tuple import py @@ -894,11 +895,14 @@ class Metafunc: test function is defined. """ - def __init__(self, definition, fixtureinfo, config, cls=None, module=None): - assert ( - isinstance(definition, FunctionDefinition) - or type(definition).__name__ == "DefinitionMock" - ) + def __init__( + self, + definition: "FunctionDefinition", + fixtureinfo, + config, + cls=None, + module=None, + ) -> None: self.definition = definition #: access to the :class:`_pytest.config.Config` object for the test session @@ -916,7 +920,7 @@ class Metafunc: #: class object where the test function is defined in or ``None``. self.cls = cls - self._calls = [] + self._calls = [] # type: List[CallSpec2] self._arg2fixturedefs = fixtureinfo.name2fixturedefs @property diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 0c3c2aed3..1c396c4a7 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -12,7 +12,7 @@ from _pytest import python class TestMetafunc: - def Metafunc(self, func, config=None): + def Metafunc(self, func, config=None) -> python.Metafunc: # the unit tests of this class check if things work correctly # on the funcarg level, so we don't need a full blown # initialization @@ -23,7 +23,7 @@ class TestMetafunc: self.names_closure = names @attr.s - class DefinitionMock: + class DefinitionMock(python.FunctionDefinition): obj = attr.ib() names = fixtures.getfuncargnames(func) From f9feef6808c250b0c98d7d35580e3fad75d17439 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 23:13:21 +0100 Subject: [PATCH 60/95] Revert "ci: use tox -vv" `tox -vv` is too verbose, and was only used as a hack to get the output of durations. As for information in logs `-v` could be used maybe still, but I've decided to revert it for now. This reverts commit 56cec5fa79106c0e8c02eb34bd8e5768ec52044d. --- .travis.yml | 2 +- azure-pipelines.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 310d7093b..e3edbfe9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -108,7 +108,7 @@ before_script: export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess fi -script: tox -vv +script: tox after_success: - | diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2ee1604a7..f18ce0887 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -57,7 +57,7 @@ jobs: export COVERAGE_FILE="$PWD/.coverage" export COVERAGE_PROCESS_START="$PWD/.coveragerc" fi - python -m tox -e $(tox.env) -vv + python -m tox -e $(tox.env) displayName: 'Run tests' - task: PublishTestResults@2 From f38f2d402e183130222993f501d92eefe0d398dc Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 20:26:31 +0100 Subject: [PATCH 61/95] minor: visit_Assert: move setting of `negation` out of branches --- src/_pytest/assertion/rewrite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 2f9ca6de0..51ea1801b 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -807,8 +807,9 @@ class AssertionRewriter(ast.NodeVisitor): ) ) + negation = ast.UnaryOp(ast.Not(), top_condition) + if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook - negation = ast.UnaryOp(ast.Not(), top_condition) msg = self.pop_format_context(ast.Str(explanation)) # Failed @@ -860,7 +861,6 @@ class AssertionRewriter(ast.NodeVisitor): else: # Original assertion rewriting # Create failure message. body = self.expl_stmts - negation = ast.UnaryOp(ast.Not(), top_condition) self.statements.append(ast.If(negation, body, [])) if assert_.msg: assertmsg = self.helper("_format_assertmsg", assert_.msg) From 63a23d876cad98d42f03cf6c6f397513e96ffcdb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 18 Nov 2019 16:01:44 -0800 Subject: [PATCH 62/95] Remove check for os.symlink, always there in py3+ --- testing/acceptance_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ad9c37737..284796a42 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -754,7 +754,6 @@ class TestInvocationVariants: result = testdir.runpytest(str(p) + "::test", "--doctest-modules") result.stdout.fnmatch_lines(["*1 passed*"]) - @pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks") def test_cmdline_python_package_symlink(self, testdir, monkeypatch): """ test --pyargs option with packages with path containing symlink can From 4804d4bc9824f7a6fb0153a25e896627dec37b3b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 19 Nov 2019 02:27:04 +0100 Subject: [PATCH 63/95] python: remove unused pytest_make_parametrize_id hookimpl Added in 79927428d initially, but never used. --- src/_pytest/python.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2e8756289..4702e0659 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -241,10 +241,6 @@ def pytest_pycollect_makeitem(collector, name, obj): outcome.force_result(res) -def pytest_make_parametrize_id(config, val, argname=None): - return None - - class PyobjContext: module = pyobj_property("Module") cls = pyobj_property("Class") From 4ad61cbcf6063d5bc414a9a37a5fbb29a3083e73 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 19 Nov 2019 15:58:46 +0100 Subject: [PATCH 64/95] Improve check for misspelling of parametrize - there is no need to do this with `--strict-markers` - it can be done when looking up marks, instead of for every generated test --- changelog/6231.improvement.rst | 1 + src/_pytest/mark/structures.py | 19 ++++++++++++------- src/_pytest/python.py | 10 ---------- testing/python/metafunc.py | 20 ++++++++++++-------- 4 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 changelog/6231.improvement.rst diff --git a/changelog/6231.improvement.rst b/changelog/6231.improvement.rst new file mode 100644 index 000000000..1554a229b --- /dev/null +++ b/changelog/6231.improvement.rst @@ -0,0 +1 @@ +Improve check for misspelling of ``pytest.mark.parametrize``. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 18ebc506a..3002f8abc 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -314,13 +314,18 @@ class MarkGenerator: "{!r} not found in `markers` configuration option".format(name), pytrace=False, ) - else: - warnings.warn( - "Unknown pytest.mark.%s - is this a typo? You can register " - "custom marks to avoid this warning - for details, see " - "https://docs.pytest.org/en/latest/mark.html" % name, - PytestUnknownMarkWarning, - ) + + # 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)) + + warnings.warn( + "Unknown pytest.mark.%s - is this a typo? You can register " + "custom marks to avoid this warning - for details, see " + "https://docs.pytest.org/en/latest/mark.html" % name, + PytestUnknownMarkWarning, + ) return MarkDecorator(Mark(name, (), {})) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2e8756289..cb6c93159 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -120,17 +120,7 @@ def pytest_cmdline_main(config): return 0 -def _validate_parametrize_spelling(metafunc): - """Raise a specific error for common misspellings of "parametrize".""" - for mark_name in ["parameterize", "parametrise", "parameterise"]: - if metafunc.definition.get_closest_marker(mark_name): - msg = "{0} has '{1}' mark, spelling should be 'parametrize'" - fail(msg.format(metafunc.function.__name__, mark_name), pytrace=False) - - def pytest_generate_tests(metafunc): - _validate_parametrize_spelling(metafunc) - for marker in metafunc.definition.iter_markers(name="parametrize"): metafunc.parametrize(*marker.args, **marker.kwargs) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 1c396c4a7..65855f724 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1323,25 +1323,29 @@ class TestMetafuncFunctional: reprec = testdir.runpytest() reprec.assert_outcomes(passed=4) - @pytest.mark.parametrize("attr", ["parametrise", "parameterize", "parameterise"]) - def test_parametrize_misspelling(self, testdir, attr): + def test_parametrize_misspelling(self, testdir): """#463""" testdir.makepyfile( """ import pytest - @pytest.mark.{}("x", range(2)) + @pytest.mark.parametrise("x", range(2)) def test_foo(x): pass - """.format( - attr - ) + """ ) result = testdir.runpytest("--collectonly") result.stdout.fnmatch_lines( [ - "test_foo has '{}' mark, spelling should be 'parametrize'".format(attr), - "*1 error in*", + "collected 0 items / 1 error", + "", + "*= ERRORS =*", + "*_ ERROR collecting test_parametrize_misspelling.py _*", + "test_parametrize_misspelling.py:3: in ", + ' @pytest.mark.parametrise("x", range(2))', + "E Failed: Unknown 'parametrise' mark, did you mean 'parametrize'?", + "*! Interrupted: 1 error during collection !*", + "*= 1 error in *", ] ) From 4b16b93cf57bb58d6dd55fb4b4ffa2a0a2a344db Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 19 Nov 2019 12:43:51 -0300 Subject: [PATCH 65/95] Preparing release version 5.3.0 --- CHANGELOG.rst | 202 ++++++++++++++++++++++++++++++ changelog/2049.bugfix.rst | 1 - changelog/2548.bugfix.rst | 1 - changelog/4488.feature.rst | 10 -- changelog/4730.feature.rst | 3 - changelog/4901.trivial.rst | 2 - changelog/5061.improvement.rst | 1 - changelog/5515.feature.rst | 11 -- changelog/5630.improvement.rst | 1 - changelog/5914.feature.rst | 19 --- changelog/5924.improvement.rst | 34 ----- changelog/5936.improvement.rst | 1 - changelog/5990.improvement.rst | 1 - changelog/6008.improvement.rst | 2 - changelog/6023.improvement.rst | 1 - changelog/6026.improvement.rst | 1 - changelog/6039.bugfix.rst | 3 - changelog/6047.bugfix.rst | 1 - changelog/6057.feature.rst | 3 - changelog/6059.improvement.rst | 1 - changelog/6061.feature.rst | 4 - changelog/6069.improvement.rst | 1 - changelog/6074.bugfix.rst | 1 - changelog/6097.improvement.rst | 1 - changelog/6116.improvement.rst | 1 - changelog/6148.improvement.rst | 1 - changelog/6152.improvement.rst | 1 - changelog/6176.improvement.rst | 1 - changelog/6179.deprecation.rst | 7 -- changelog/6181.improvement.rst | 1 - changelog/6189.bugfix.rst | 1 - changelog/6206.improvement.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-5.3.0.rst | 45 +++++++ doc/en/example/markers.rst | 2 +- doc/en/example/parametrize.rst | 9 +- doc/en/example/simple.rst | 2 +- doc/en/usage.rst | 4 +- doc/en/warnings.rst | 4 +- doc/en/writing_plugins.rst | 2 +- 40 files changed, 260 insertions(+), 129 deletions(-) delete mode 100644 changelog/2049.bugfix.rst delete mode 100644 changelog/2548.bugfix.rst delete mode 100644 changelog/4488.feature.rst delete mode 100644 changelog/4730.feature.rst delete mode 100644 changelog/4901.trivial.rst delete mode 100644 changelog/5061.improvement.rst delete mode 100644 changelog/5515.feature.rst delete mode 100644 changelog/5630.improvement.rst delete mode 100644 changelog/5914.feature.rst delete mode 100644 changelog/5924.improvement.rst delete mode 100644 changelog/5936.improvement.rst delete mode 100644 changelog/5990.improvement.rst delete mode 100644 changelog/6008.improvement.rst delete mode 100644 changelog/6023.improvement.rst delete mode 100644 changelog/6026.improvement.rst delete mode 100644 changelog/6039.bugfix.rst delete mode 100644 changelog/6047.bugfix.rst delete mode 100644 changelog/6057.feature.rst delete mode 100644 changelog/6059.improvement.rst delete mode 100644 changelog/6061.feature.rst delete mode 100644 changelog/6069.improvement.rst delete mode 100644 changelog/6074.bugfix.rst delete mode 100644 changelog/6097.improvement.rst delete mode 100644 changelog/6116.improvement.rst delete mode 100644 changelog/6148.improvement.rst delete mode 100644 changelog/6152.improvement.rst delete mode 100644 changelog/6176.improvement.rst delete mode 100644 changelog/6179.deprecation.rst delete mode 100644 changelog/6181.improvement.rst delete mode 100644 changelog/6189.bugfix.rst delete mode 100644 changelog/6206.improvement.rst create mode 100644 doc/en/announce/release-5.3.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e9ac09c8e..76b6caf1b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,208 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.3.0 (2019-11-19) +========================= + +Deprecations +------------ + +- `#6179 `_: The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given + that this is the version supported by default in modern tools that manipulate this type of file. + + In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option + is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``. + + For more information, `see the docs `__. + + + +Features +-------- + +- `#4488 `_: The pytest team has created the `pytest-reportlog `__ + plugin, which provides a new ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes. + + Each line of the report log contains a self contained JSON object corresponding to a testing event, + such as a collection or a test result report. The file is guaranteed to be flushed after writing + each line, so systems can read and process events in real-time. + + The plugin is meant to replace the ``--resultlog`` option, which is deprecated and meant to be removed + in a future release. If you use ``--resultlog``, please try out ``pytest-reportlog`` and + provide feedback. + + +- `#4730 `_: When ``sys.pycache_prefix`` (Python 3.8+) is set, it will be used by pytest to cache test files changed by the assertion rewriting mechanism. + + This makes it easier to benefit of cached ``.pyc`` files even on file systems without permissions. + + +- `#5515 `_: Allow selective auto-indentation of multiline log messages. + + Adds command line option ``--log-auto-indent``, config option + ``log_auto_indent`` and support for per-entry configuration of + indentation behavior on calls to ``logging.log()``. + + Alters the default for auto-indention from ``on`` to ``off``. This + restores the older behavior that existed prior to v4.6.0. This + reversion to earlier behavior was done because it is better to + activate new features that may lead to broken tests explicitly + rather than implicitly. + + +- `#5914 `_: ``pytester`` learned two new functions, `no_fnmatch_line `_ and + `no_re_match_line `_. + + The functions are used to ensure the captured text *does not* match the given + pattern. + + The previous idiom was to use ``re.match``: + + .. code-block:: python + + assert re.match(pat, result.stdout.str()) is None + + Or the ``in`` operator: + + .. code-block:: python + + assert text in result.stdout.str() + + But the new functions produce best output on failure. + + +- `#6057 `_: Add tolerances to complex values when printing ``pytest.approx``. + + For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle. + + +- `#6061 `_: Adding the pluginmanager as an option ``pytest_addoption`` + so that hooks can be invoked when setting up command line options. This is + useful for having one plugin communicate things to another plugin, + such as default values or which set of command line options to add. + + + +Improvements +------------ + +- `#5061 `_: Use multiple colors with terminal summary statistics. + + +- `#5630 `_: Quitting from debuggers is now properly handled in ``doctest`` items. + + +- `#5924 `_: Improve verbose diff output with sequences. + + Before: + + .. code-block:: + + E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] + E Right contains 3 more items, first extra item: ' ' + E Full diff: + E - ['version', 'version_info', 'sys.version', 'sys.version_info'] + E + ['version', + E + 'version_info', + E + 'sys.version', + E + 'sys.version_info', + E + ' ', + E + 'sys.version', + E + 'sys.version_info'] + + After: + + .. code-block:: + + E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] + E Right contains 3 more items, first extra item: ' ' + E Full diff: + E [ + E 'version', + E 'version_info', + E 'sys.version', + E 'sys.version_info', + E + ' ', + E + 'sys.version', + E + 'sys.version_info', + E ] + + +- `#5936 `_: Display untruncated assertion message with ``-vv``. + + +- `#5990 `_: Fix plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors"). + + +- `#6008 `_: ``Config.InvocationParams.args`` is now always a ``tuple`` to better convey that it should be + immutable and avoid accidental modifications. + + +- `#6023 `_: ``pytest.main`` returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). + + +- `#6026 `_: Align prefixes in output of pytester's ``LineMatcher``. + + +- `#6059 `_: Collection errors are reported as errors (and not failures like before) in the terminal's short test summary. + + +- `#6069 `_: ``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally. + + +- `#6097 `_: The "[XXX%]" indicator in the test summary is colored according to the final (new) multi-colored line's main color. + + +- `#6116 `_: Add ``--co`` as a synonym to ``--collect-only``. + + +- `#6148 `_: ``atomicwrites`` is now only used on Windows, fixing a performance regression with assertion rewriting on Unix. + + +- `#6152 `_: Now parametrization will use the ``__name__`` attribute of any object for the id, if present. Previously it would only use ``__name__`` for functions and classes. + + +- `#6176 `_: Improved failure reporting with pytester's ``Hookrecorder.assertoutcome``. + + +- `#6181 `_: The reason for a stopped session, e.g. with ``--maxfail`` / ``-x`` gets reported. + + +- `#6206 `_: cacheprovider: improved robustness and performance with ``cache.set``. + + + +Bug Fixes +--------- + +- `#2049 `_: Fix ``-setup-plan`` showing inaccurate information about fixture lifetimes. + + +- `#2548 `_: Fix line offset mismatch with skipped tests in terminal summary. + + +- `#6039 `_: The ``PytestDoctestRunner`` is properly invalidated when unconfiguring the doctest plugin. + + This is important when used with ``pytester``'s ``runpytest_inprocess``. + + +- `#6047 `_: BaseExceptions are handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. + + +- `#6074 `_: pytester: fix order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``. + + +- `#6189 `_: Fix incorrect result of ``getmodpath`` method. + + + +Trivial/Internal Changes +------------------------ + +- `#4901 `_: ``RunResult`` from ``pytester`` now displays the mnemonic of the ``ret`` attribute when it is a + valid ``pytest.ExitCode`` value. + + pytest 5.2.4 (2019-11-15) ========================= diff --git a/changelog/2049.bugfix.rst b/changelog/2049.bugfix.rst deleted file mode 100644 index 395396bd3..000000000 --- a/changelog/2049.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``-setup-plan`` showing inaccurate information about fixture lifetimes. diff --git a/changelog/2548.bugfix.rst b/changelog/2548.bugfix.rst deleted file mode 100644 index 8ee3b6462..000000000 --- a/changelog/2548.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix line offset mismatch with skipped tests in terminal summary. diff --git a/changelog/4488.feature.rst b/changelog/4488.feature.rst deleted file mode 100644 index 1e0387f44..000000000 --- a/changelog/4488.feature.rst +++ /dev/null @@ -1,10 +0,0 @@ -The pytest team has created the `pytest-reportlog `__ -plugin, which provides a new ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes. - -Each line of the report log contains a self contained JSON object corresponding to a testing event, -such as a collection or a test result report. The file is guaranteed to be flushed after writing -each line, so systems can read and process events in real-time. - -The plugin is meant to replace the ``--resultlog`` option, which is deprecated and meant to be removed -in a future release. If you use ``--resultlog``, please try out ``pytest-reportlog`` and -provide feedback. diff --git a/changelog/4730.feature.rst b/changelog/4730.feature.rst deleted file mode 100644 index 80d1c4a38..000000000 --- a/changelog/4730.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -When ``sys.pycache_prefix`` (Python 3.8+) is set, it will be used by pytest to cache test files changed by the assertion rewriting mechanism. - -This makes it easier to benefit of cached ``.pyc`` files even on file systems without permissions. diff --git a/changelog/4901.trivial.rst b/changelog/4901.trivial.rst deleted file mode 100644 index f6609ddf1..000000000 --- a/changelog/4901.trivial.rst +++ /dev/null @@ -1,2 +0,0 @@ -``RunResult`` from ``pytester`` now displays the mnemonic of the ``ret`` attribute when it is a -valid ``pytest.ExitCode`` value. diff --git a/changelog/5061.improvement.rst b/changelog/5061.improvement.rst deleted file mode 100644 index 9eb0c1cd3..000000000 --- a/changelog/5061.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Use multiple colors with terminal summary statistics. diff --git a/changelog/5515.feature.rst b/changelog/5515.feature.rst deleted file mode 100644 index b53097c43..000000000 --- a/changelog/5515.feature.rst +++ /dev/null @@ -1,11 +0,0 @@ -Allow selective auto-indentation of multiline log messages. - -Adds command line option ``--log-auto-indent``, config option -``log_auto_indent`` and support for per-entry configuration of -indentation behavior on calls to ``logging.log()``. - -Alters the default for auto-indention from ``on`` to ``off``. This -restores the older behavior that existed prior to v4.6.0. This -reversion to earlier behavior was done because it is better to -activate new features that may lead to broken tests explicitly -rather than implicitly. diff --git a/changelog/5630.improvement.rst b/changelog/5630.improvement.rst deleted file mode 100644 index 45d49bdae..000000000 --- a/changelog/5630.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Quitting from debuggers is now properly handled in ``doctest`` items. diff --git a/changelog/5914.feature.rst b/changelog/5914.feature.rst deleted file mode 100644 index 68cd66f99..000000000 --- a/changelog/5914.feature.rst +++ /dev/null @@ -1,19 +0,0 @@ -``pytester`` learned two new functions, `no_fnmatch_line `_ and -`no_re_match_line `_. - -The functions are used to ensure the captured text *does not* match the given -pattern. - -The previous idiom was to use ``re.match``: - -.. code-block:: python - - assert re.match(pat, result.stdout.str()) is None - -Or the ``in`` operator: - -.. code-block:: python - - assert text in result.stdout.str() - -But the new functions produce best output on failure. diff --git a/changelog/5924.improvement.rst b/changelog/5924.improvement.rst deleted file mode 100644 index a03eb4704..000000000 --- a/changelog/5924.improvement.rst +++ /dev/null @@ -1,34 +0,0 @@ -Improve verbose diff output with sequences. - -Before: - -.. code-block:: - - E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] - E Right contains 3 more items, first extra item: ' ' - E Full diff: - E - ['version', 'version_info', 'sys.version', 'sys.version_info'] - E + ['version', - E + 'version_info', - E + 'sys.version', - E + 'sys.version_info', - E + ' ', - E + 'sys.version', - E + 'sys.version_info'] - -After: - -.. code-block:: - - E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] - E Right contains 3 more items, first extra item: ' ' - E Full diff: - E [ - E 'version', - E 'version_info', - E 'sys.version', - E 'sys.version_info', - E + ' ', - E + 'sys.version', - E + 'sys.version_info', - E ] diff --git a/changelog/5936.improvement.rst b/changelog/5936.improvement.rst deleted file mode 100644 index c5cd924bb..000000000 --- a/changelog/5936.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Display untruncated assertion message with ``-vv``. diff --git a/changelog/5990.improvement.rst b/changelog/5990.improvement.rst deleted file mode 100644 index 6f5ad648e..000000000 --- a/changelog/5990.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Fix plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors"). diff --git a/changelog/6008.improvement.rst b/changelog/6008.improvement.rst deleted file mode 100644 index 22ef35cc8..000000000 --- a/changelog/6008.improvement.rst +++ /dev/null @@ -1,2 +0,0 @@ -``Config.InvocationParams.args`` is now always a ``tuple`` to better convey that it should be -immutable and avoid accidental modifications. diff --git a/changelog/6023.improvement.rst b/changelog/6023.improvement.rst deleted file mode 100644 index 6cf81002e..000000000 --- a/changelog/6023.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``pytest.main`` returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). diff --git a/changelog/6026.improvement.rst b/changelog/6026.improvement.rst deleted file mode 100644 index 34dfb278d..000000000 --- a/changelog/6026.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Align prefixes in output of pytester's ``LineMatcher``. diff --git a/changelog/6039.bugfix.rst b/changelog/6039.bugfix.rst deleted file mode 100644 index b13a677c8..000000000 --- a/changelog/6039.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -The ``PytestDoctestRunner`` is properly invalidated when unconfiguring the doctest plugin. - -This is important when used with ``pytester``'s ``runpytest_inprocess``. diff --git a/changelog/6047.bugfix.rst b/changelog/6047.bugfix.rst deleted file mode 100644 index 11a997f71..000000000 --- a/changelog/6047.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -BaseExceptions are handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. diff --git a/changelog/6057.feature.rst b/changelog/6057.feature.rst deleted file mode 100644 index b7334e7fe..000000000 --- a/changelog/6057.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Add tolerances to complex values when printing ``pytest.approx``. - -For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle. diff --git a/changelog/6059.improvement.rst b/changelog/6059.improvement.rst deleted file mode 100644 index 39ffff99b..000000000 --- a/changelog/6059.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Collection errors are reported as errors (and not failures like before) in the terminal's short test summary. diff --git a/changelog/6061.feature.rst b/changelog/6061.feature.rst deleted file mode 100644 index 11f548625..000000000 --- a/changelog/6061.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -Adding the pluginmanager as an option ``pytest_addoption`` -so that hooks can be invoked when setting up command line options. This is -useful for having one plugin communicate things to another plugin, -such as default values or which set of command line options to add. diff --git a/changelog/6069.improvement.rst b/changelog/6069.improvement.rst deleted file mode 100644 index e60d154bb..000000000 --- a/changelog/6069.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally. diff --git a/changelog/6074.bugfix.rst b/changelog/6074.bugfix.rst deleted file mode 100644 index 624cf5d1c..000000000 --- a/changelog/6074.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -pytester: fix order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``. diff --git a/changelog/6097.improvement.rst b/changelog/6097.improvement.rst deleted file mode 100644 index 32eb84906..000000000 --- a/changelog/6097.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -The "[XXX%]" indicator in the test summary is colored according to the final (new) multi-colored line's main color. diff --git a/changelog/6116.improvement.rst b/changelog/6116.improvement.rst deleted file mode 100644 index 4fc96ec77..000000000 --- a/changelog/6116.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``--co`` as a synonym to ``--collect-only``. diff --git a/changelog/6148.improvement.rst b/changelog/6148.improvement.rst deleted file mode 100644 index 3d77ab528..000000000 --- a/changelog/6148.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``atomicwrites`` is now only used on Windows, fixing a performance regression with assertion rewriting on Unix. diff --git a/changelog/6152.improvement.rst b/changelog/6152.improvement.rst deleted file mode 100644 index 8e5f4d52a..000000000 --- a/changelog/6152.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Now parametrization will use the ``__name__`` attribute of any object for the id, if present. Previously it would only use ``__name__`` for functions and classes. diff --git a/changelog/6176.improvement.rst b/changelog/6176.improvement.rst deleted file mode 100644 index 39787da2e..000000000 --- a/changelog/6176.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Improved failure reporting with pytester's ``Hookrecorder.assertoutcome``. diff --git a/changelog/6179.deprecation.rst b/changelog/6179.deprecation.rst deleted file mode 100644 index 97f7ec74b..000000000 --- a/changelog/6179.deprecation.rst +++ /dev/null @@ -1,7 +0,0 @@ -The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given -that this is the version supported by default in modern tools that manipulate this type of file. - -In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option -is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``. - -For more information, `see the docs `__. diff --git a/changelog/6181.improvement.rst b/changelog/6181.improvement.rst deleted file mode 100644 index 0960f6203..000000000 --- a/changelog/6181.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -The reason for a stopped session, e.g. with ``--maxfail`` / ``-x`` gets reported. diff --git a/changelog/6189.bugfix.rst b/changelog/6189.bugfix.rst deleted file mode 100644 index 060a2260a..000000000 --- a/changelog/6189.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix incorrect result of ``getmodpath`` method. diff --git a/changelog/6206.improvement.rst b/changelog/6206.improvement.rst deleted file mode 100644 index 67d8363b3..000000000 --- a/changelog/6206.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -cacheprovider: improved robustness and performance with ``cache.set``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index e7c011411..6e6914f2d 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.3.0 release-5.2.4 release-5.2.3 release-5.2.2 diff --git a/doc/en/announce/release-5.3.0.rst b/doc/en/announce/release-5.3.0.rst new file mode 100644 index 000000000..9855a7a2d --- /dev/null +++ b/doc/en/announce/release-5.3.0.rst @@ -0,0 +1,45 @@ +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 +against itself, passing on many different interpreters and platforms. + +This release contains a number of bugs fixes and improvements, so users are encouraged +to take a look at the CHANGELOG: + + https://docs.pytest.org/en/latest/changelog.html + +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: + +* AnjoMan +* Anthony Sottile +* Anton Lodder +* Bruno Oliveira +* Daniel Hahler +* Gregory Lee +* Josh Karpel +* JoshKarpel +* Joshua Storck +* Kale Kundert +* MarcoGorelli +* Michael Krebs +* NNRepos +* Ran Benita +* TH3CHARLie +* Tibor Arpas +* Zac Hatfield-Dodds +* 林玮 + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index ccddb1f66..8143b3fd4 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -622,7 +622,7 @@ then you will see two tests skipped and two executed tests as expected: test_plat.py s.s. [100%] ========================= short test summary info ========================== - SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux + SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:12: cannot run on platform linux ======================= 2 passed, 2 skipped in 0.12s ======================= Note that if you specify a platform via the marker-command line option like this: diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 0e131dace..c420761a4 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -475,10 +475,11 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ssssssssssss......sss...... [100%] + ssssssssssss...ssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found - 12 passed, 15 skipped in 0.12s + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.7' not found + 3 passed, 24 skipped in 0.12s Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- @@ -546,7 +547,7 @@ If you run this with reporting for skips enabled: test_module.py .s [100%] ========================= short test summary info ========================== - SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:13: could not import 'opt2': No module named 'opt2' + SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:12: could not import 'opt2': No module named 'opt2' ======================= 1 passed, 1 skipped in 0.12s ======================= You'll see that we don't have an ``opt2`` module and thus the second test run diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 05ccbc9b2..1570850fc 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -443,7 +443,7 @@ Now we can profile which test functions execute the slowest: ========================= slowest 3 test durations ========================= 0.30s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 - 0.10s call test_some_are_slow.py::test_funcfast + 0.11s call test_some_are_slow.py::test_funcfast ============================ 3 passed in 0.12s ============================= incremental testing - test steps diff --git a/doc/en/usage.rst b/doc/en/usage.rst index ea849c1a7..245a67b68 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -241,7 +241,7 @@ Example: test_example.py:14: AssertionError ========================= short test summary info ========================== - SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:22: skipping this test XFAIL test_example.py::test_xfail reason: xfailing this test XPASS test_example.py::test_xpass always xfail @@ -296,7 +296,7 @@ More than one character can be used, so for example to only see failed and skipp test_example.py:14: AssertionError ========================= short test summary info ========================== FAILED test_example.py::test_fail - assert 0 - SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:22: skipping this test == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 54bb60da1..4b8be4469 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -41,7 +41,7 @@ Running pytest now produces this output: warnings.warn(UserWarning("api v1, should use functions from v2")) -- Docs: https://docs.pytest.org/en/latest/warnings.html - ====================== 1 passed, 1 warnings in 0.12s ======================= + ======================= 1 passed, 1 warning in 0.12s ======================= The ``-W`` flag can be passed to control which warnings will be displayed or even turn them into errors: @@ -407,7 +407,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta class Test: -- Docs: https://docs.pytest.org/en/latest/warnings.html - 1 warnings in 0.12s + 1 warning in 0.12s These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings. diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 8660746bd..2f7283791 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -442,7 +442,7 @@ additionally it is possible to copy examples for an example folder before runnin testdir.copy_example("test_example.py") -- Docs: https://docs.pytest.org/en/latest/warnings.html - ====================== 2 passed, 1 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 be59827216612fd416818702be7e2b8448f4089d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 19 Nov 2019 13:56:22 -0300 Subject: [PATCH 66/95] Small fixes in the CHANGELOG for 5.3.0 --- CHANGELOG.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 76b6caf1b..a63e6f838 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -88,12 +88,12 @@ Features But the new functions produce best output on failure. -- `#6057 `_: Add tolerances to complex values when printing ``pytest.approx``. +- `#6057 `_: Added tolerances to complex values when printing ``pytest.approx``. For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle. -- `#6061 `_: Adding the pluginmanager as an option ``pytest_addoption`` +- `#6061 `_: Added the pluginmanager as an argument to ``pytest_addoption`` so that hooks can be invoked when setting up command line options. This is useful for having one plugin communicate things to another plugin, such as default values or which set of command line options to add. @@ -109,7 +109,7 @@ Improvements - `#5630 `_: Quitting from debuggers is now properly handled in ``doctest`` items. -- `#5924 `_: Improve verbose diff output with sequences. +- `#5924 `_: Improved verbose diff output with sequences. Before: @@ -148,14 +148,14 @@ Improvements - `#5936 `_: Display untruncated assertion message with ``-vv``. -- `#5990 `_: Fix plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors"). +- `#5990 `_: Fixed plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors"). - `#6008 `_: ``Config.InvocationParams.args`` is now always a ``tuple`` to better convey that it should be immutable and avoid accidental modifications. -- `#6023 `_: ``pytest.main`` returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). +- `#6023 `_: ``pytest.main`` now returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). - `#6026 `_: Align prefixes in output of pytester's ``LineMatcher``. @@ -167,10 +167,10 @@ Improvements - `#6069 `_: ``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally. -- `#6097 `_: The "[XXX%]" indicator in the test summary is colored according to the final (new) multi-colored line's main color. +- `#6097 `_: The "[XXX%]" indicator in the test summary is now colored according to the final (new) multi-colored line's main color. -- `#6116 `_: Add ``--co`` as a synonym to ``--collect-only``. +- `#6116 `_: Added ``--co`` as a synonym to ``--collect-only``. - `#6148 `_: ``atomicwrites`` is now only used on Windows, fixing a performance regression with assertion rewriting on Unix. @@ -182,34 +182,34 @@ Improvements - `#6176 `_: Improved failure reporting with pytester's ``Hookrecorder.assertoutcome``. -- `#6181 `_: The reason for a stopped session, e.g. with ``--maxfail`` / ``-x`` gets reported. +- `#6181 `_: The reason for a stopped session, e.g. with ``--maxfail`` / ``-x``, now gets reported in the test summary. -- `#6206 `_: cacheprovider: improved robustness and performance with ``cache.set``. +- `#6206 `_: Improved ``cache.set`` robustness and performance. Bug Fixes --------- -- `#2049 `_: Fix ``-setup-plan`` showing inaccurate information about fixture lifetimes. +- `#2049 `_: Fixed ``--setup-plan`` showing inaccurate information about fixture lifetimes. -- `#2548 `_: Fix line offset mismatch with skipped tests in terminal summary. +- `#2548 `_: Fixed line offset mismatch of skipped tests in terminal summary. -- `#6039 `_: The ``PytestDoctestRunner`` is properly invalidated when unconfiguring the doctest plugin. +- `#6039 `_: The ``PytestDoctestRunner`` is now properly invalidated when unconfiguring the doctest plugin. This is important when used with ``pytester``'s ``runpytest_inprocess``. -- `#6047 `_: BaseExceptions are handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. +- `#6047 `_: BaseExceptions are now handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. -- `#6074 `_: pytester: fix order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``. +- `#6074 `_: pytester: fixed order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``. -- `#6189 `_: Fix incorrect result of ``getmodpath`` method. +- `#6189 `_: Fixed result of ``getmodpath`` method. From 36ef545b2dcfdbfd1bf10fd65aa54f229b199349 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 19 Nov 2019 14:04:21 -0300 Subject: [PATCH 67/95] Improve instructions on how to write CHANGELOG entries This makes easier for contributors to get the CHANGELOG entry right the first time. --- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++++ changelog/README.rst | 8 +++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7436f7146..7f9aa9556 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,5 +11,13 @@ Here is a quick checklist that should be present in PRs. Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please: - [ ] Create a new changelog file in the `changelog` folder, with a name like `..rst`. See [changelog/README.rst](https://github.com/pytest-dev/pytest/blob/master/changelog/README.rst) for details. + + Write sentences in the **past or present tense**, examples: + + * *Improved verbose diff output with sequences.* + * *Terminal summary statistics now use multiple colors.* + + Also make sure to end the sentence with a `.`. + - [ ] Add yourself to `AUTHORS` in alphabetical order. --> diff --git a/changelog/README.rst b/changelog/README.rst index 5c182758b..3e464508a 100644 --- a/changelog/README.rst +++ b/changelog/README.rst @@ -1,12 +1,14 @@ This directory contains "newsfragments" which are short files that contain a small **ReST**-formatted text that will be added to the next ``CHANGELOG``. -The ``CHANGELOG`` will be read by users, so this description should be aimed to pytest users +The ``CHANGELOG`` will be read by **users**, so this description should be aimed to pytest users instead of describing internal changes which are only relevant to the developers. -Make sure to use full sentences with correct case and punctuation, for example:: +Make sure to use full sentences in the **past or present tense** and use punctuation, examples:: - Fix issue with non-ascii messages from the ``warnings`` module. + Improved verbose diff output with sequences. + + Terminal summary statistics now use multiple colors. Each file should be named like ``..rst``, where ```` is an issue number, and ```` is one of: From 2228ccbfb412e58138e88101876187008bb22346 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 3 Nov 2019 23:02:30 +0100 Subject: [PATCH 68/95] pytester: reset log output in _match_lines (#70) This is necessary for when using e.g. `no_fnmatch_line` after it. Factor it out into `_fail`. (cherry picked from commit aade7ed0045ba32557ef8565cbab28a2c91053a7) Ref: https://github.com/pytest-dev/pytest/pull/5914#issuecomment-549182242 --- changelog/5914.bugfix.rst | 1 + src/_pytest/pytester.py | 38 +++++++++++++++++++++----------------- testing/test_pytester.py | 10 +++++++++- 3 files changed, 31 insertions(+), 18 deletions(-) create mode 100644 changelog/5914.bugfix.rst diff --git a/changelog/5914.bugfix.rst b/changelog/5914.bugfix.rst new file mode 100644 index 000000000..b62b0b3c0 --- /dev/null +++ b/changelog/5914.bugfix.rst @@ -0,0 +1 @@ +pytester: fix ``no_fnmatch_line`` when used after positive matching. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 02414a299..bacb1c23d 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1438,8 +1438,10 @@ class LineMatcher: self._log("{:>{width}}".format("and:", width=wnick), repr(nextline)) extralines.append(nextline) else: - self._log("remains unmatched: {!r}".format(line)) - pytest.fail(self._log_text.lstrip()) + msg = "remains unmatched: {!r}".format(line) + self._log(msg) + self._fail(msg) + self._log_output = [] def no_fnmatch_line(self, pat): """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``. @@ -1465,18 +1467,20 @@ class LineMatcher: __tracebackhide__ = True nomatch_printed = False wnick = len(match_nickname) + 1 - try: - for line in self.lines: - if match_func(line, pat): - self._log("%s:" % match_nickname, repr(pat)) - self._log("{:>{width}}".format("with:", width=wnick), repr(line)) - pytest.fail(self._log_text.lstrip()) - else: - if not nomatch_printed: - self._log( - "{:>{width}}".format("nomatch:", width=wnick), repr(pat) - ) - nomatch_printed = True - self._log("{:>{width}}".format("and:", width=wnick), repr(line)) - finally: - self._log_output = [] + for line in self.lines: + if match_func(line, pat): + msg = "{}: {!r}".format(match_nickname, pat) + self._log(msg) + self._log("{:>{width}}".format("with:", width=wnick), repr(line)) + self._fail(msg) + else: + if not nomatch_printed: + self._log("{:>{width}}".format("nomatch:", width=wnick), repr(pat)) + nomatch_printed = True + self._log("{:>{width}}".format("and:", width=wnick), repr(line)) + self._log_output = [] + + def _fail(self, msg): + log_text = self._log_text + self._log_output = [] + pytest.fail(log_text) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 758e999dc..5bdbacdd0 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -530,7 +530,7 @@ def test_no_matching(function): ] else: assert obtained == [ - "nomatch: '{}'".format(good_pattern), + " nomatch: '{}'".format(good_pattern), " and: 'cachedir: .pytest_cache'", " and: 'collecting ... collected 1 item'", " and: ''", @@ -542,6 +542,14 @@ def test_no_matching(function): func(bad_pattern) # bad pattern does not match any line: passes +def test_no_matching_after_match(): + lm = LineMatcher(["1", "2", "3"]) + lm.fnmatch_lines(["1", "3"]) + with pytest.raises(pytest.fail.Exception) as e: + lm.no_fnmatch_line("*") + assert str(e.value).splitlines() == ["fnmatch: '*'", " with: '1'"] + + def test_pytester_addopts(request, monkeypatch): monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused") From 8c65eae5f4cb6d023b1720e3fcf65e03cc0d858a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 19 Nov 2019 21:12:30 -0800 Subject: [PATCH 69/95] Fix rendering of Before/After in changelog Apparently the version of sphinx that rtd uses is a little more strict about whether an anonymous `code-block` can happen --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a63e6f838..9ab3c871e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -113,7 +113,7 @@ Improvements Before: - .. code-block:: + .. code-block:: pytest E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] E Right contains 3 more items, first extra item: ' ' @@ -129,7 +129,7 @@ Improvements After: - .. code-block:: + .. code-block:: pytest E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] E Right contains 3 more items, first extra item: ' ' From af9dfc604deb490431a790d89fba7dfae107a079 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 20 Nov 2019 07:05:31 -0300 Subject: [PATCH 70/95] Introduce 5934 in CHANGELOG and fix "pytest" blocks --- CHANGELOG.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9ab3c871e..0f53fe33a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -113,7 +113,7 @@ Improvements Before: - .. code-block:: pytest + :: E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] E Right contains 3 more items, first extra item: ' ' @@ -129,7 +129,7 @@ Improvements After: - .. code-block:: pytest + :: E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] E Right contains 3 more items, first extra item: ' ' @@ -145,6 +145,8 @@ Improvements E ] +- `#5934 `_: ``repr`` of ``ExceptionInfo`` objects has been improved to honor the ``__repr__`` method of the underlying exception. + - `#5936 `_: Display untruncated assertion message with ``-vv``. From fe69a2cfb7d834ca6bf25bc69b9331f49ca3d30d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 20 Nov 2019 07:06:11 -0300 Subject: [PATCH 71/95] Delete 5934.feature.rst included in the wrong folder by accident --- doc/5934.feature.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 doc/5934.feature.rst diff --git a/doc/5934.feature.rst b/doc/5934.feature.rst deleted file mode 100644 index 17c0b1737..000000000 --- a/doc/5934.feature.rst +++ /dev/null @@ -1 +0,0 @@ -``repr`` of ``ExceptionInfo`` objects has been improved to honor the ``__repr__`` method of the underlying exception. From 51f9cd0e02371d1a4770625aff178a3f8ab6db5d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 20 Nov 2019 16:03:32 +0200 Subject: [PATCH 72/95] argparsing: remove "map_long_option" Action attribute support This feature was added in commit 007a77c2ba14b3df8790efb433a2f849edf4f5d2, but was never used in pytest itself. A GitHub code search doesn't find any users either (only pytest repo copies). It seems safe to clean up. --- src/_pytest/config/argparsing.py | 16 +++++----------- testing/test_parseopt.py | 6 +++--- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 7cbb676bd..8366a8d66 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -409,8 +409,6 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): """shorten help for long options that differ only in extra hyphens - collapse **long** options that are the same except for extra hyphens - - special action attribute map_long_option allows suppressing additional - long options - 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 """ @@ -434,9 +432,6 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): action._formatted_action_invocation = orgstr return orgstr return_list = [] - option_map = getattr(action, "map_long_option", {}) - if option_map is None: - option_map = {} short_long = {} # type: Dict[str, str] for option in options: if len(option) == 2 or option[2] == " ": @@ -446,12 +441,11 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): 'long optional argument without "--": [%s]' % (option), self ) xxoption = option[2:] - if xxoption.split()[0] not in option_map: - shortened = xxoption.replace("-", "") - if shortened not in short_long or len(short_long[shortened]) < len( - xxoption - ): - short_long[shortened] = xxoption + shortened = xxoption.replace("-", "") + if shortened not in short_long or len(short_long[shortened]) < len( + xxoption + ): + short_long[shortened] = xxoption # now short_long has been filled out to the longest with dashes # **and** we keep the right option ordering from add_argument for option in options: diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 915747378..5f7d5222b 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -205,11 +205,11 @@ class TestParser: ) parser.add_argument( "-t", "--twoword", "--duo", "--two-word", "--two", help="foo" - ).map_long_option = {"two": "two-word"} + ) # throws error on --deux only! parser.add_argument( "-d", "--deuxmots", "--deux-mots", action="store_true", help="foo" - ).map_long_option = {"deux": "deux-mots"} + ) parser.add_argument("-s", action="store_true", help="single short") parser.add_argument("--abc", "-a", action="store_true", help="bar") parser.add_argument("--klm", "-k", "--kl-m", action="store_true", help="bar") @@ -221,7 +221,7 @@ class TestParser: ) parser.add_argument( "-x", "--exit-on-first", "--exitfirst", action="store_true", help="spam" - ).map_long_option = {"exitfirst": "exit-on-first"} + ) parser.add_argument("files_and_dirs", nargs="*") args = parser.parse_args(["-k", "--duo", "hallo", "--exitfirst"]) assert args.twoword == "hallo" From c0b1a39192a998b4368ac859677b7e22f8ee56f2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 20 Nov 2019 17:56:39 +0100 Subject: [PATCH 73/95] minor: move internal _pformat_dispatch function --- src/_pytest/_io/saferepr.py | 21 +++++++++++++++++++++ src/_pytest/assertion/util.py | 22 +--------------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 7fded872d..884f0a21e 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -80,3 +80,24 @@ def saferepr(obj: Any, maxsize: int = 240) -> str: around the Repr/reprlib functionality of the standard 2.6 lib. """ return SafeRepr(maxsize).repr(obj) + + +class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter): + """PrettyPrinter that always dispatches (regardless of width).""" + + def _format(self, object, stream, indent, allowance, context, level): + p = self._dispatch.get(type(object).__repr__, None) + + objid = id(object) + if objid in context or p is None: + return super()._format(object, stream, indent, allowance, context, level) + + context[objid] = 1 + p(self, object, stream, indent, allowance, context, level + 1) + del context[objid] + + +def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False): + return AlwaysDispatchingPrettyPrinter( + indent=1, width=80, depth=None, compact=False + ).pformat(object) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 4af35bd57..67f8d4618 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -13,6 +13,7 @@ from typing import Tuple import _pytest._code from _pytest import outcomes +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 @@ -28,27 +29,6 @@ _reprcompare = None # type: Optional[Callable[[str, object, object], Optional[s _assertion_pass = None # type: Optional[Callable[[int, str, str], None]] -class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter): - """PrettyPrinter that always dispatches (regardless of width).""" - - def _format(self, object, stream, indent, allowance, context, level): - p = self._dispatch.get(type(object).__repr__, None) - - objid = id(object) - if objid in context or p is None: - return super()._format(object, stream, indent, allowance, context, level) - - context[objid] = 1 - p(self, object, stream, indent, allowance, context, level + 1) - del context[objid] - - -def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False): - return AlwaysDispatchingPrettyPrinter( - indent=1, width=80, depth=None, compact=False - ).pformat(object) - - def format_explanation(explanation: str) -> str: """This formats an explanation From ccb3ef3b33fcf419d03260c1d18f352da373725d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 20 Nov 2019 18:02:00 +0100 Subject: [PATCH 74/95] testing/python/metafunc.py: import _idval once --- testing/python/metafunc.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 65855f724..d3be5504e 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -9,6 +9,7 @@ from hypothesis import strategies import pytest from _pytest import fixtures from _pytest import python +from _pytest.python import _idval class TestMetafunc: @@ -209,8 +210,6 @@ class TestMetafunc: deadline=400.0 ) # very close to std deadline and CI boxes are not reliable in CPU power def test_idval_hypothesis(self, value): - from _pytest.python import _idval - escaped = _idval(value, "a", 6, None, item=None, config=None) assert isinstance(escaped, str) escaped.encode("ascii") @@ -221,8 +220,6 @@ class TestMetafunc: escapes if they're not. """ - from _pytest.python import _idval - values = [ ("", ""), ("ascii", "ascii"), @@ -242,7 +239,6 @@ class TestMetafunc: disable_test_id_escaping_and_forfeit_all_rights_to_community_support option. (#5294) """ - from _pytest.python import _idval class MockConfig: def __init__(self, config): @@ -274,8 +270,6 @@ class TestMetafunc: "binary escape", where any byte < 127 is escaped into its hex form. - python3: bytes objects are always escaped using "binary escape". """ - from _pytest.python import _idval - values = [ (b"", ""), (b"\xc3\xb4\xff\xe4", "\\xc3\\xb4\\xff\\xe4"), @@ -289,7 +283,6 @@ class TestMetafunc: """unittest for the expected behavior to obtain ids for parametrized values that are classes or functions: their __name__. """ - from _pytest.python import _idval class TestClass: pass From 2c941b5d13cc8688a1d655fc0ef41a4de8c4e251 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 12 Nov 2019 14:52:19 +0100 Subject: [PATCH 75/95] parametrized: ids: support generator/iterator Fixes https://github.com/pytest-dev/pytest/issues/759 - Adjust test_parametrized_ids_invalid_type, create list to convert tuples Ref: https://github.com/pytest-dev/pytest/issues/1857#issuecomment-552922498 - Changelog for int to str conversion Ref: https://github.com/pytest-dev/pytest/issues/1857#issuecomment-552932952 --- changelog/1857.improvement.rst | 1 + changelog/759.improvement.rst | 1 + src/_pytest/mark/structures.py | 26 +++++++- src/_pytest/python.py | 93 ++++++++++++++++++++------- testing/python/metafunc.py | 112 +++++++++++++++++++++++++++++++-- 5 files changed, 206 insertions(+), 27 deletions(-) create mode 100644 changelog/1857.improvement.rst create mode 100644 changelog/759.improvement.rst diff --git a/changelog/1857.improvement.rst b/changelog/1857.improvement.rst new file mode 100644 index 000000000..9a8ce90f5 --- /dev/null +++ b/changelog/1857.improvement.rst @@ -0,0 +1 @@ +``pytest.mark.parametrize`` accepts integers for ``ids`` again, converting it to strings. diff --git a/changelog/759.improvement.rst b/changelog/759.improvement.rst new file mode 100644 index 000000000..83ace7485 --- /dev/null +++ b/changelog/759.improvement.rst @@ -0,0 +1 @@ +``pytest.mark.parametrize`` supports iterators and generators for ``ids``. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 3002f8abc..a4ec9665c 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -2,6 +2,8 @@ import inspect import warnings from collections import namedtuple from collections.abc import MutableMapping +from typing import List +from typing import Optional from typing import Set import attr @@ -144,7 +146,15 @@ class Mark: #: keyword arguments of the mark decorator kwargs = attr.ib() # Dict[str, object] - def combined_with(self, other): + #: source Mark for ids with parametrize Marks + _param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False) + #: resolved/generated ids with parametrize Marks + _param_ids_generated = attr.ib(type=Optional[List[str]], default=None, repr=False) + + def _has_param_ids(self): + return "ids" in self.kwargs or len(self.args) >= 4 + + def combined_with(self, other: "Mark") -> "Mark": """ :param other: the mark to combine with :type other: Mark @@ -153,8 +163,20 @@ class Mark: combines by appending args and merging the mappings """ assert self.name == other.name + + # Remember source of ids with parametrize Marks. + param_ids_from = None # type: Optional[Mark] + if self.name == "parametrize": + if other._has_param_ids(): + param_ids_from = other + elif self._has_param_ids(): + param_ids_from = self + return Mark( - self.name, self.args + other.args, dict(self.kwargs, **other.kwargs) + self.name, + self.args + other.args, + dict(self.kwargs, **other.kwargs), + param_ids_from=param_ids_from, ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index d787638c9..4e3a68867 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -10,6 +10,7 @@ from collections.abc import Sequence from functools import partial from textwrap import dedent from typing import List +from typing import Optional from typing import Tuple import py @@ -36,6 +37,7 @@ from _pytest.deprecated import FUNCARGNAMES from _pytest.main import FSHookProxy from _pytest.mark import MARK_GEN from _pytest.mark.structures import get_unpacked_marks +from _pytest.mark.structures import Mark from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail from _pytest.outcomes import skip @@ -122,7 +124,7 @@ def pytest_cmdline_main(config): def pytest_generate_tests(metafunc): for marker in metafunc.definition.iter_markers(name="parametrize"): - metafunc.parametrize(*marker.args, **marker.kwargs) + metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) def pytest_configure(config): @@ -914,7 +916,16 @@ class Metafunc: warnings.warn(FUNCARGNAMES, stacklevel=2) return self.fixturenames - def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None): + def parametrize( + self, + argnames, + argvalues, + indirect=False, + ids=None, + scope=None, + *, + _param_mark: Optional[Mark] = None + ): """ 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 @@ -937,13 +948,22 @@ class Metafunc: function so that it can perform more expensive setups during the setup phase of a test rather than at collection time. - :arg ids: list of string ids, or a callable. - If strings, each is corresponding to the argvalues so that they are - part of the test id. If None is given as id of specific test, the - automatically generated id for that argument will be used. - If callable, it should take one argument (a single argvalue) and return - a string or return None. If None, the automatically generated id for that - argument will be used. + :arg 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``, + ``bool``, or ``None``. + They are mapped to the corresponding index in ``argvalues``. + ``None`` means to use the auto-generated id. + + If it is a callable it will be called for each entry in + ``argvalues``, and the return value is used as part of the + auto-generated id for the whole set (where parts are joined with + dashes ("-")). + This is useful to provide more specific ids for certain items, e.g. + dates. Returning ``None`` will use an auto-generated id. + If no ids are provided they will be generated automatically from the argvalues. @@ -977,8 +997,18 @@ class Metafunc: arg_values_types = self._resolve_arg_value_types(argnames, indirect) + # Use any already (possibly) generated ids with parametrize Marks. + if _param_mark and _param_mark._param_ids_from: + generated_ids = _param_mark._param_ids_from._param_ids_generated + if generated_ids is not None: + ids = generated_ids + ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition) + # Store used (possibly generated) ids with parametrize Marks. + if _param_mark and _param_mark._param_ids_from and generated_ids is None: + object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids) + scopenum = scope2index( scope, descr="parametrize() call in {}".format(self.function.__name__) ) @@ -1013,27 +1043,48 @@ class Metafunc: :rtype: List[str] :return: the list of ids for each argname given """ - from _pytest._io.saferepr import saferepr - idfn = None if callable(ids): idfn = ids ids = None if ids: func_name = self.function.__name__ - if len(ids) != len(parameters): - msg = "In {}: {} parameter sets specified, with different number of ids: {}" - fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False) - for id_value in ids: - if id_value is not None and not isinstance(id_value, str): - msg = "In {}: ids must be list of strings, found: {} (type: {!r})" - fail( - msg.format(func_name, saferepr(id_value), type(id_value)), - pytrace=False, - ) + ids = self._validate_ids(ids, parameters, func_name) ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item) return ids + def _validate_ids(self, ids, parameters, func_name): + try: + len(ids) + except TypeError: + try: + it = iter(ids) + except TypeError: + raise TypeError("ids must be a callable, sequence or generator") + else: + import itertools + + new_ids = list(itertools.islice(it, len(parameters))) + else: + new_ids = list(ids) + + if len(new_ids) != len(parameters): + msg = "In {}: {} parameter sets specified, with different number of ids: {}" + fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False) + for idx, id_value in enumerate(new_ids): + if id_value is not None: + if isinstance(id_value, (float, int, bool)): + new_ids[idx] = str(id_value) + elif not isinstance(id_value, str): + from _pytest._io.saferepr import saferepr + + msg = "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, + ) + return new_ids + def _resolve_arg_value_types(self, argnames, indirect): """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. diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index d3be5504e..9a1e1f968 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -9,6 +9,7 @@ from hypothesis import strategies import pytest from _pytest import fixtures from _pytest import python +from _pytest.outcomes import fail from _pytest.python import _idval @@ -62,6 +63,39 @@ class TestMetafunc: pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5, 6])) pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5, 6])) + with pytest.raises( + TypeError, match="^ids must be a callable, sequence or generator$" + ): + metafunc.parametrize("y", [5, 6], ids=42) + + def test_parametrize_error_iterator(self): + def func(x): + raise NotImplementedError() + + class Exc(Exception): + def __repr__(self): + return "Exc(from_gen)" + + def gen(): + yield 0 + yield None + yield Exc() + + metafunc = self.Metafunc(func) + metafunc.parametrize("x", [1, 2], ids=gen()) + assert [(x.funcargs, x.id) for x in metafunc._calls] == [ + ({"x": 1}, "0"), + ({"x": 2}, "2"), + ] + with pytest.raises( + fail.Exception, + match=( + r"In func: ids must be list of string/float/int/bool, found:" + r" Exc\(from_gen\) \(type: \) at index 2" + ), + ): + metafunc.parametrize("x", [1, 2, 3], ids=gen()) + def test_parametrize_bad_scope(self, testdir): def func(x): pass @@ -168,6 +202,26 @@ class TestMetafunc: ("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"] ) + def test_parametrize_ids_iterator_without_mark(self): + import itertools + + def func(x, y): + pass + + it = itertools.count() + + metafunc = self.Metafunc(func) + metafunc.parametrize("x", [1, 2], ids=it) + metafunc.parametrize("y", [3, 4], ids=it) + ids = [x.id for x in metafunc._calls] + assert ids == ["0-2", "0-3", "1-2", "1-3"] + + metafunc = self.Metafunc(func) + metafunc.parametrize("x", [1, 2], ids=it) + metafunc.parametrize("y", [3, 4], ids=it) + ids = [x.id for x in metafunc._calls] + assert ids == ["4-6", "4-7", "5-6", "5-7"] + def test_parametrize_empty_list(self): """#510""" @@ -527,9 +581,22 @@ class TestMetafunc: @pytest.mark.parametrize("arg", ({1: 2}, {3, 4}), ids=ids) def test(arg): assert arg + + @pytest.mark.parametrize("arg", (1, 2.0, True), ids=ids) + def test_int(arg): + assert arg """ ) - assert testdir.runpytest().ret == 0 + result = testdir.runpytest("-vv", "-s") + result.stdout.fnmatch_lines( + [ + "test_parametrize_ids_returns_non_string.py::test[arg0] PASSED", + "test_parametrize_ids_returns_non_string.py::test[arg1] PASSED", + "test_parametrize_ids_returns_non_string.py::test_int[1] PASSED", + "test_parametrize_ids_returns_non_string.py::test_int[2.0] PASSED", + "test_parametrize_ids_returns_non_string.py::test_int[True] PASSED", + ] + ) def test_idmaker_with_ids(self): from _pytest.python import idmaker @@ -1179,12 +1246,12 @@ class TestMetafuncFunctional: result.stdout.fnmatch_lines(["* 1 skipped *"]) def test_parametrized_ids_invalid_type(self, testdir): - """Tests parametrized with ids as non-strings (#1857).""" + """Test error with non-strings/non-ints, without generator (#1857).""" testdir.makepyfile( """ import pytest - @pytest.mark.parametrize("x, expected", [(10, 20), (40, 80)], ids=(None, 2)) + @pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, type)) def test_ids_numbers(x,expected): assert x * 2 == expected """ @@ -1192,7 +1259,8 @@ class TestMetafuncFunctional: result = testdir.runpytest() result.stdout.fnmatch_lines( [ - "*In test_ids_numbers: ids must be list of strings, found: 2 (type: *'int'>)*" + "In test_ids_numbers: ids must be list of string/float/int/bool," + " found: (type: ) at index 2" ] ) @@ -1773,3 +1841,39 @@ class TestMarkersWithParametrization: ) result = testdir.runpytest() result.assert_outcomes(passed=1) + + def test_parametrize_iterator(self, testdir): + testdir.makepyfile( + """ + import itertools + import pytest + + id_parametrize = pytest.mark.parametrize( + ids=("param%d" % i for i in itertools.count()) + ) + + @id_parametrize('y', ['a', 'b']) + def test1(y): + pass + + @id_parametrize('y', ['a', 'b']) + def test2(y): + pass + + @pytest.mark.parametrize("a, b", [(1, 2), (3, 4)], ids=itertools.count()) + def test_converted_to_str(a, b): + pass + """ + ) + result = testdir.runpytest("-vv", "-s") + result.stdout.fnmatch_lines( + [ + "test_parametrize_iterator.py::test1[param0] PASSED", + "test_parametrize_iterator.py::test1[param1] PASSED", + "test_parametrize_iterator.py::test2[param0] PASSED", + "test_parametrize_iterator.py::test2[param1] PASSED", + "test_parametrize_iterator.py::test_converted_to_str[0] PASSED", + "test_parametrize_iterator.py::test_converted_to_str[1] PASSED", + "*= 6 passed in *", + ] + ) From dac16cd9e5113a5b769d89557e9dcdc5001fe205 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 Nov 2019 14:35:33 +0200 Subject: [PATCH 76/95] Add type annotations to _pytest.config.argparsing and corresponding Config code --- src/_pytest/_argcomplete.py | 11 +- src/_pytest/config/__init__.py | 24 +++-- src/_pytest/config/argparsing.py | 168 ++++++++++++++++++++----------- testing/test_parseopt.py | 65 ++++++------ 4 files changed, 161 insertions(+), 107 deletions(-) diff --git a/src/_pytest/_argcomplete.py b/src/_pytest/_argcomplete.py index 688c9077d..7ca216ecf 100644 --- a/src/_pytest/_argcomplete.py +++ b/src/_pytest/_argcomplete.py @@ -53,19 +53,22 @@ If things do not work right away: which should throw a KeyError: 'COMPLINE' (which is properly set by the global argcomplete script). """ +import argparse import os import sys from glob import glob +from typing import Any +from typing import List from typing import Optional class FastFilesCompleter: "Fast file completer class" - def __init__(self, directories=True): + def __init__(self, directories: bool = True) -> None: self.directories = directories - def __call__(self, prefix, **kwargs): + def __call__(self, prefix: str, **kwargs: Any) -> List[str]: """only called on non option completions""" if os.path.sep in prefix[1:]: prefix_dir = len(os.path.dirname(prefix) + os.path.sep) @@ -94,13 +97,13 @@ if os.environ.get("_ARGCOMPLETE"): sys.exit(-1) filescompleter = FastFilesCompleter() # type: Optional[FastFilesCompleter] - def try_argcomplete(parser): + def try_argcomplete(parser: argparse.ArgumentParser) -> None: argcomplete.autocomplete(parser, always_complete_options=False) else: - def try_argcomplete(parser): + def try_argcomplete(parser: argparse.ArgumentParser) -> None: pass filescompleter = None diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 1c98f0266..4e3323b0c 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -45,6 +45,8 @@ from _pytest.warning_types import PytestConfigWarning if False: # TYPE_CHECKING from typing import Type + from .argparsing import Argument + hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") @@ -679,7 +681,7 @@ class Config: plugins = attr.ib() dir = attr.ib(type=Path) - def __init__(self, pluginmanager, *, invocation_params=None): + def __init__(self, pluginmanager, *, invocation_params=None) -> None: from .argparsing import Parser, FILE_OR_DIR if invocation_params is None: @@ -792,11 +794,11 @@ class Config: config.pluginmanager.consider_pluginarg(x) return config - def _processopt(self, opt): + def _processopt(self, opt: "Argument") -> None: for name in opt._short_opts + opt._long_opts: self._opt2dest[name] = opt.dest - if hasattr(opt, "default") and opt.dest: + if hasattr(opt, "default"): if not hasattr(self.option, opt.dest): setattr(self.option, opt.dest, opt.default) @@ -804,7 +806,7 @@ class Config: def pytest_load_initial_conftests(self, early_config): self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) - def _initini(self, args) -> None: + def _initini(self, args: Sequence[str]) -> None: ns, unknown_args = self._parser.parse_known_and_unknown_args( args, namespace=copy.copy(self.option) ) @@ -821,7 +823,7 @@ class Config: self._parser.addini("minversion", "minimally required pytest version") self._override_ini = ns.override_ini or () - def _consider_importhook(self, args): + def _consider_importhook(self, args: Sequence[str]) -> None: """Install the PEP 302 import hook if using assertion rewriting. Needs to parse the --assert= option from the commandline @@ -861,19 +863,19 @@ class Config: for name in _iter_rewritable_modules(package_files): hook.mark_rewrite(name) - def _validate_args(self, args, via): + def _validate_args(self, args: List[str], via: str) -> List[str]: """Validate known args.""" - self._parser._config_source_hint = via + self._parser._config_source_hint = via # type: ignore try: self._parser.parse_known_and_unknown_args( args, namespace=copy.copy(self.option) ) finally: - del self._parser._config_source_hint + del self._parser._config_source_hint # type: ignore return args - def _preparse(self, args, addopts=True): + def _preparse(self, args: List[str], addopts: bool = True) -> None: if addopts: env_addopts = os.environ.get("PYTEST_ADDOPTS", "") if len(env_addopts): @@ -937,7 +939,7 @@ class Config: ) ) - def parse(self, args, addopts=True): + def parse(self, args: List[str], addopts: bool = True) -> None: # parse given cmdline arguments into this config object. assert not hasattr( self, "args" @@ -948,7 +950,7 @@ class Config: self._preparse(args, addopts=addopts) # XXX deprecated hook: self.hook.pytest_cmdline_preparse(config=self, args=args) - self._parser.after_preparse = True + self._parser.after_preparse = True # type: ignore try: args = self._parser.parse_setoption( args, self.option, namespace=self.option diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 8366a8d66..d0870ed56 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -3,15 +3,24 @@ import sys import warnings from gettext import gettext from typing import Any +from typing import Callable +from typing import cast from typing import Dict from typing import List +from typing import Mapping from typing import Optional +from typing import Sequence from typing import Tuple +from typing import Union import py from _pytest.config.exceptions import UsageError +if False: # TYPE_CHECKING + from typing import NoReturn + from typing_extensions import Literal # noqa: F401 + FILE_OR_DIR = "file_or_dir" @@ -22,9 +31,13 @@ class Parser: there's an error processing the command line arguments. """ - prog = None + prog = None # type: Optional[str] - def __init__(self, usage=None, processopt=None): + def __init__( + self, + usage: Optional[str] = None, + processopt: Optional[Callable[["Argument"], None]] = None, + ) -> None: self._anonymous = OptionGroup("custom options", parser=self) self._groups = [] # type: List[OptionGroup] self._processopt = processopt @@ -33,12 +46,14 @@ class Parser: self._ininames = [] # type: List[str] self.extra_info = {} # type: Dict[str, Any] - def processoption(self, option): + def processoption(self, option: "Argument") -> None: if self._processopt: if option.dest: self._processopt(option) - def getgroup(self, name, description="", after=None): + def getgroup( + self, name: str, description: str = "", after: Optional[str] = None + ) -> "OptionGroup": """ get (or create) a named option Group. :name: name of the option group. @@ -61,13 +76,13 @@ class Parser: self._groups.insert(i + 1, group) return group - def addoption(self, *opts, **attrs): + def addoption(self, *opts: str, **attrs: Any) -> None: """ register a command line option. :opts: option names, can be short or long options. - :attrs: same attributes which the ``add_option()`` function of the + :attrs: same attributes which the ``add_argument()`` function of the `argparse library - `_ + `_ accepts. After command line parsing options are available on the pytest config @@ -77,7 +92,11 @@ class Parser: """ self._anonymous.addoption(*opts, **attrs) - def parse(self, args, namespace=None): + def parse( + self, + args: Sequence[Union[str, py.path.local]], + namespace: Optional[argparse.Namespace] = None, + ) -> argparse.Namespace: from _pytest._argcomplete import try_argcomplete self.optparser = self._getparser() @@ -98,27 +117,37 @@ class Parser: n = option.names() a = option.attrs() arggroup.add_argument(*n, **a) + file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*") # bash like autocompletion for dirs (appending '/') # Type ignored because typeshed doesn't know about argcomplete. - optparser.add_argument( # type: ignore - FILE_OR_DIR, nargs="*" - ).completer = filescompleter + file_or_dir_arg.completer = filescompleter # type: ignore return optparser - def parse_setoption(self, args, option, namespace=None): + def parse_setoption( + self, + args: Sequence[Union[str, py.path.local]], + option: argparse.Namespace, + namespace: Optional[argparse.Namespace] = None, + ) -> List[str]: parsedoption = self.parse(args, namespace=namespace) for name, value in parsedoption.__dict__.items(): setattr(option, name, value) - return getattr(parsedoption, FILE_OR_DIR) + return cast(List[str], getattr(parsedoption, FILE_OR_DIR)) - def parse_known_args(self, args, namespace=None) -> argparse.Namespace: + def parse_known_args( + self, + 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. """ return self.parse_known_and_unknown_args(args, namespace=namespace)[0] def parse_known_and_unknown_args( - self, args, namespace=None + self, + 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. @@ -127,7 +156,13 @@ class Parser: args = [str(x) if isinstance(x, py.path.local) else x for x in args] return optparser.parse_known_args(args, namespace=namespace) - def addini(self, name, help, type=None, default=None): + def addini( + self, + name: str, + help: str, + type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None, + default=None, + ) -> None: """ register an ini-file option. :name: name of the ini-variable @@ -149,11 +184,11 @@ class ArgumentError(Exception): inconsistent arguments. """ - def __init__(self, msg, option): + def __init__(self, msg: str, option: Union["Argument", str]) -> None: self.msg = msg self.option_id = str(option) - def __str__(self): + def __str__(self) -> str: if self.option_id: return "option {}: {}".format(self.option_id, self.msg) else: @@ -170,12 +205,11 @@ class Argument: _typ_map = {"int": int, "string": str, "float": float, "complex": complex} - def __init__(self, *names, **attrs): + 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.dest = attrs.get("dest") if "%default" in (attrs.get("help") or ""): warnings.warn( 'pytest now uses argparse. "%default" should be' @@ -221,23 +255,25 @@ class Argument: except KeyError: pass self._set_opt_strings(names) - if not self.dest: - if self._long_opts: - self.dest = self._long_opts[0][2:].replace("-", "_") - else: - try: - self.dest = self._short_opts[0][1:] - except IndexError: - raise ArgumentError("need a long or short option", self) + dest = attrs.get("dest") # type: Optional[str] + if dest: + self.dest = dest + elif self._long_opts: + self.dest = self._long_opts[0][2:].replace("-", "_") + else: + try: + self.dest = self._short_opts[0][1:] + except IndexError: + self.dest = "???" # Needed for the error repr. + raise ArgumentError("need a long or short option", self) - def names(self): + def names(self) -> List[str]: return self._short_opts + self._long_opts - def attrs(self): + def attrs(self) -> Mapping[str, Any]: # update any attributes set by processopt attrs = "default dest help".split() - if self.dest: - attrs.append(self.dest) + attrs.append(self.dest) for attr in attrs: try: self._attrs[attr] = getattr(self, attr) @@ -250,7 +286,7 @@ class Argument: self._attrs["help"] = a return self._attrs - def _set_opt_strings(self, opts): + def _set_opt_strings(self, opts: Sequence[str]) -> None: """directly from optparse might not be necessary as this is passed to argparse later on""" @@ -293,13 +329,15 @@ class Argument: class OptionGroup: - def __init__(self, name, description="", parser=None): + def __init__( + self, name: str, description: str = "", parser: Optional[Parser] = None + ) -> None: self.name = name self.description = description self.options = [] # type: List[Argument] self.parser = parser - def addoption(self, *optnames, **attrs): + def addoption(self, *optnames: str, **attrs: Any) -> None: """ add an option to this group. if a shortened version of a long option is specified it will @@ -315,11 +353,11 @@ class OptionGroup: option = Argument(*optnames, **attrs) self._addoption_instance(option, shortupper=False) - def _addoption(self, *optnames, **attrs): + def _addoption(self, *optnames: str, **attrs: Any) -> None: option = Argument(*optnames, **attrs) self._addoption_instance(option, shortupper=True) - def _addoption_instance(self, option, shortupper=False): + def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None: if not shortupper: for opt in option._short_opts: if opt[0] == "-" and opt[1].islower(): @@ -330,9 +368,12 @@ class OptionGroup: class MyOptionParser(argparse.ArgumentParser): - def __init__(self, parser, extra_info=None, prog=None): - if not extra_info: - extra_info = {} + def __init__( + self, + parser: Parser, + extra_info: Optional[Dict[str, Any]] = None, + prog: Optional[str] = None, + ) -> None: self._parser = parser argparse.ArgumentParser.__init__( self, @@ -344,34 +385,42 @@ class MyOptionParser(argparse.ArgumentParser): ) # extra_info is a dict of (param -> value) to display if there's # an usage error to provide more contextual information to the user - self.extra_info = extra_info + self.extra_info = extra_info if extra_info else {} - def error(self, message): + def error(self, message: str) -> "NoReturn": """Transform argparse error message into UsageError.""" msg = "{}: error: {}".format(self.prog, message) if hasattr(self._parser, "_config_source_hint"): - msg = "{} ({})".format(msg, self._parser._config_source_hint) + # Type ignored because the attribute is set dynamically. + msg = "{} ({})".format(msg, self._parser._config_source_hint) # type: ignore raise UsageError(self.format_usage() + msg) - def parse_args(self, args=None, namespace=None): + # Type ignored because typeshed has a very complex type in the superclass. + def parse_args( # type: ignore + self, + args: Optional[Sequence[str]] = None, + namespace: Optional[argparse.Namespace] = None, + ) -> argparse.Namespace: """allow splitting of positional arguments""" - args, argv = self.parse_known_args(args, namespace) - if argv: - for arg in argv: + parsed, unrecognized = self.parse_known_args(args, namespace) + if unrecognized: + for arg in unrecognized: if arg and arg[0] == "-": - lines = ["unrecognized arguments: %s" % (" ".join(argv))] + lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))] for k, v in sorted(self.extra_info.items()): lines.append(" {}: {}".format(k, v)) self.error("\n".join(lines)) - getattr(args, FILE_OR_DIR).extend(argv) - return args + getattr(parsed, FILE_OR_DIR).extend(unrecognized) + return parsed if sys.version_info[:2] < (3, 9): # pragma: no cover # Backport of https://github.com/python/cpython/pull/14316 so we can # disable long --argument abbreviations without breaking short flags. - def _parse_optional(self, arg_string): + def _parse_optional( + self, arg_string: str + ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]: if not arg_string: return None if not arg_string[0] in self.prefix_chars: @@ -413,23 +462,25 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): - cache result on action object as this is called at least 2 times """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Use more accurate terminal width via pylib.""" if "width" not in kwargs: kwargs["width"] = py.io.get_terminal_width() super().__init__(*args, **kwargs) - def _format_action_invocation(self, action): + 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) + res = getattr( + action, "_formatted_action_invocation", None + ) # type: Optional[str] if res: return res options = orgstr.split(", ") if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2): # a shortcut for '-h, --help' or '--abc', '-a' - action._formatted_action_invocation = orgstr + action._formatted_action_invocation = orgstr # type: ignore return orgstr return_list = [] short_long = {} # type: Dict[str, str] @@ -438,7 +489,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): continue if not option.startswith("--"): raise ArgumentError( - 'long optional argument without "--": [%s]' % (option), self + 'long optional argument without "--": [%s]' % (option), option ) xxoption = option[2:] shortened = xxoption.replace("-", "") @@ -453,5 +504,6 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): return_list.append(option) if option[2:] == short_long.get(option.replace("-", "")): return_list.append(option.replace(" ", "=", 1)) - action._formatted_action_invocation = ", ".join(return_list) - return action._formatted_action_invocation + formatted_action_invocation = ", ".join(return_list) + action._formatted_action_invocation = formatted_action_invocation # type: ignore + return formatted_action_invocation diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 5f7d5222b..cdccc240e 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -12,22 +12,22 @@ from _pytest.config.exceptions import UsageError @pytest.fixture -def parser(): +def parser() -> parseopt.Parser: return parseopt.Parser() class TestParser: - def test_no_help_by_default(self): + def test_no_help_by_default(self) -> None: parser = parseopt.Parser(usage="xyz") pytest.raises(UsageError, lambda: parser.parse(["-h"])) - def test_custom_prog(self, parser): + def test_custom_prog(self, parser: parseopt.Parser) -> None: """Custom prog can be set for `argparse.ArgumentParser`.""" assert parser._getparser().prog == os.path.basename(sys.argv[0]) parser.prog = "custom-prog" assert parser._getparser().prog == "custom-prog" - def test_argument(self): + def test_argument(self) -> None: with pytest.raises(parseopt.ArgumentError): # need a short or long option argument = parseopt.Argument() @@ -45,7 +45,7 @@ class TestParser: "Argument(_short_opts: ['-t'], _long_opts: ['--test'], dest: 'abc')" ) - def test_argument_type(self): + def test_argument_type(self) -> None: argument = parseopt.Argument("-t", dest="abc", type=int) assert argument.type is int argument = parseopt.Argument("-t", dest="abc", type=str) @@ -60,7 +60,7 @@ class TestParser: ) assert argument.type is str - def test_argument_processopt(self): + def test_argument_processopt(self) -> None: argument = parseopt.Argument("-t", type=int) argument.default = 42 argument.dest = "abc" @@ -68,19 +68,19 @@ class TestParser: assert res["default"] == 42 assert res["dest"] == "abc" - def test_group_add_and_get(self, parser): + def test_group_add_and_get(self, parser: parseopt.Parser) -> None: group = parser.getgroup("hello", description="desc") assert group.name == "hello" assert group.description == "desc" - def test_getgroup_simple(self, parser): + def test_getgroup_simple(self, parser: parseopt.Parser) -> None: group = parser.getgroup("hello", description="desc") assert group.name == "hello" assert group.description == "desc" group2 = parser.getgroup("hello") assert group2 is group - def test_group_ordering(self, parser): + def test_group_ordering(self, parser: parseopt.Parser) -> None: parser.getgroup("1") parser.getgroup("2") parser.getgroup("3", after="1") @@ -88,20 +88,20 @@ class TestParser: groups_names = [x.name for x in groups] assert groups_names == list("132") - def test_group_addoption(self): + def test_group_addoption(self) -> None: group = parseopt.OptionGroup("hello") group.addoption("--option1", action="store_true") assert len(group.options) == 1 assert isinstance(group.options[0], parseopt.Argument) - def test_group_addoption_conflict(self): + def test_group_addoption_conflict(self) -> None: group = parseopt.OptionGroup("hello again") group.addoption("--option1", "--option-1", action="store_true") with pytest.raises(ValueError) as err: group.addoption("--option1", "--option-one", action="store_true") assert str({"--option1"}) in str(err.value) - def test_group_shortopt_lowercase(self, parser): + def test_group_shortopt_lowercase(self, parser: parseopt.Parser) -> None: group = parser.getgroup("hello") with pytest.raises(ValueError): group.addoption("-x", action="store_true") @@ -109,30 +109,30 @@ class TestParser: group._addoption("-x", action="store_true") assert len(group.options) == 1 - def test_parser_addoption(self, parser): + def test_parser_addoption(self, parser: parseopt.Parser) -> None: group = parser.getgroup("custom options") assert len(group.options) == 0 group.addoption("--option1", action="store_true") assert len(group.options) == 1 - def test_parse(self, parser): + def test_parse(self, parser: parseopt.Parser) -> None: parser.addoption("--hello", dest="hello", action="store") args = parser.parse(["--hello", "world"]) assert args.hello == "world" assert not getattr(args, parseopt.FILE_OR_DIR) - def test_parse2(self, parser): + 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() - def test_parse_known_args(self, parser): + def test_parse_known_args(self, parser: parseopt.Parser) -> None: parser.parse_known_args([py.path.local()]) parser.addoption("--hello", action="store_true") ns = parser.parse_known_args(["x", "--y", "--hello", "this"]) assert ns.hello assert ns.file_or_dir == ["x"] - def test_parse_known_and_unknown_args(self, parser): + def test_parse_known_and_unknown_args(self, parser: parseopt.Parser) -> None: parser.addoption("--hello", action="store_true") ns, unknown = parser.parse_known_and_unknown_args( ["x", "--y", "--hello", "this"] @@ -141,7 +141,7 @@ class TestParser: assert ns.file_or_dir == ["x"] assert unknown == ["--y", "this"] - def test_parse_will_set_default(self, parser): + def test_parse_will_set_default(self, parser: parseopt.Parser) -> None: parser.addoption("--hello", dest="hello", default="x", action="store") option = parser.parse([]) assert option.hello == "x" @@ -149,25 +149,22 @@ class TestParser: parser.parse_setoption([], option) assert option.hello == "x" - def test_parse_setoption(self, parser): + def test_parse_setoption(self, parser: parseopt.Parser) -> None: parser.addoption("--hello", dest="hello", action="store") parser.addoption("--world", dest="world", default=42) - class A: - pass - - option = A() + option = argparse.Namespace() args = parser.parse_setoption(["--hello", "world"], option) assert option.hello == "world" assert option.world == 42 assert not args - def test_parse_special_destination(self, parser): + def test_parse_special_destination(self, parser: parseopt.Parser) -> None: parser.addoption("--ultimate-answer", type=int) args = parser.parse(["--ultimate-answer", "42"]) assert args.ultimate_answer == 42 - def test_parse_split_positional_arguments(self, parser): + def test_parse_split_positional_arguments(self, parser: parseopt.Parser) -> None: parser.addoption("-R", action="store_true") parser.addoption("-S", action="store_false") args = parser.parse(["-R", "4", "2", "-S"]) @@ -181,7 +178,7 @@ class TestParser: assert args.R is True assert args.S is False - def test_parse_defaultgetter(self): + def test_parse_defaultgetter(self) -> None: def defaultget(option): if not hasattr(option, "type"): return @@ -199,7 +196,7 @@ class TestParser: assert option.this == 42 assert option.no is False - def test_drop_short_helper(self): + def test_drop_short_helper(self) -> None: parser = argparse.ArgumentParser( formatter_class=parseopt.DropShorterLongHelpFormatter, allow_abbrev=False ) @@ -236,32 +233,32 @@ class TestParser: args = parser.parse_args(["file", "dir"]) assert "|".join(args.files_and_dirs) == "file|dir" - def test_drop_short_0(self, parser): + def test_drop_short_0(self, parser: parseopt.Parser) -> None: parser.addoption("--funcarg", "--func-arg", action="store_true") parser.addoption("--abc-def", "--abc-def", action="store_true") parser.addoption("--klm-hij", action="store_true") with pytest.raises(UsageError): parser.parse(["--funcarg", "--k"]) - def test_drop_short_2(self, parser): + def test_drop_short_2(self, parser: parseopt.Parser) -> None: parser.addoption("--func-arg", "--doit", action="store_true") args = parser.parse(["--doit"]) assert args.func_arg is True - def test_drop_short_3(self, parser): + def test_drop_short_3(self, parser: parseopt.Parser) -> None: parser.addoption("--func-arg", "--funcarg", "--doit", action="store_true") args = parser.parse(["abcd"]) assert args.func_arg is False assert args.file_or_dir == ["abcd"] - def test_drop_short_help0(self, parser, capsys): + def test_drop_short_help0(self, parser: parseopt.Parser, capsys) -> None: parser.addoption("--func-args", "--doit", help="foo", action="store_true") parser.parse([]) help = parser.optparser.format_help() assert "--func-args, --doit foo" in help # testing would be more helpful with all help generated - def test_drop_short_help1(self, parser, capsys): + def test_drop_short_help1(self, parser: parseopt.Parser, capsys) -> None: group = parser.getgroup("general") group.addoption("--doit", "--func-args", action="store_true", help="foo") group._addoption( @@ -275,7 +272,7 @@ class TestParser: help = parser.optparser.format_help() assert "-doit, --func-args foo" in help - def test_multiple_metavar_help(self, parser): + def test_multiple_metavar_help(self, parser: parseopt.Parser) -> None: """ Help text for options with a metavar tuple should display help in the form "--preferences=value1 value2 value3" (#2004). @@ -290,7 +287,7 @@ class TestParser: assert "--preferences=value1 value2 value3" in help -def test_argcomplete(testdir, monkeypatch): +def test_argcomplete(testdir, monkeypatch) -> None: if not distutils.spawn.find_executable("bash"): pytest.skip("bash not available") script = str(testdir.tmpdir.join("test_argcomplete")) From 2d449e95e4c270f1b3a15239b72cd3c5338f798b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 20 Nov 2019 18:58:02 +0100 Subject: [PATCH 77/95] Respect --fulltrace with collection errors --- changelog/6247.improvement.rst | 1 + src/_pytest/nodes.py | 6 ++++-- testing/python/collect.py | 22 ++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 changelog/6247.improvement.rst diff --git a/changelog/6247.improvement.rst b/changelog/6247.improvement.rst new file mode 100644 index 000000000..6634d6b80 --- /dev/null +++ b/changelog/6247.improvement.rst @@ -0,0 +1 @@ +``--fulltrace`` is honored with collection errors. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 22c2ce337..33067334c 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -365,12 +365,14 @@ class Collector(Node): def repr_failure(self, excinfo): """ represent a collection failure. """ - if excinfo.errisinstance(self.CollectError): + if excinfo.errisinstance(self.CollectError) and not self.config.getoption( + "fulltrace", False + ): exc = excinfo.value return str(exc.args[0]) # Respect explicit tbstyle option, but default to "short" - # (None._repr_failure_py defaults to "long" without "fulltrace" option). + # (_repr_failure_py uses "long" with "fulltrace" option always). tbstyle = self.config.getoption("tbstyle", "auto") if tbstyle == "auto": tbstyle = "short" diff --git a/testing/python/collect.py b/testing/python/collect.py index 30f9841b5..e036cb7d9 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1210,6 +1210,28 @@ def test_syntax_error_with_non_ascii_chars(testdir): result.stdout.fnmatch_lines(["*ERROR collecting*", "*SyntaxError*", "*1 error in*"]) +def test_collecterror_with_fulltrace(testdir): + testdir.makepyfile("assert 0") + result = testdir.runpytest("--fulltrace") + result.stdout.fnmatch_lines( + [ + "collected 0 items / 1 error", + "", + "*= ERRORS =*", + "*_ ERROR collecting test_collecterror_with_fulltrace.py _*", + "", + "*/_pytest/python.py:*: ", + "_ _ _ _ _ _ _ _ *", + "", + "> assert 0", + "E assert 0", + "", + "test_collecterror_with_fulltrace.py:1: AssertionError", + "*! Interrupted: 1 error during collection !*", + ] + ) + + def test_skip_duplicates_by_default(testdir): """Test for issue https://github.com/pytest-dev/pytest/issues/1609 (#1609) From be722652f0b63411a6c9e9e5a193cac129404d24 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 20 Nov 2019 23:20:29 +0100 Subject: [PATCH 78/95] docs: configure default_role=literal This configures the default role for interpreted text (single backticks), avoiding the need to check for / enforce double backticks. Fixes also one instance in the existing changelog: - Detect `pytest_` prefixed hooks using the internal plugin manager since ``pluggy`` is deprecating the ``implprefix`` argument to ``PluginManager``. (`#3487 `_) --- .pre-commit-config.yaml | 4 ---- doc/en/conf.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8210ef5d5..d4196152f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,10 +37,6 @@ repos: hooks: - id: pyupgrade args: [--py3-plus] -- repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.4.0 - hooks: - - id: rst-backticks - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.740 hooks: diff --git a/doc/en/conf.py b/doc/en/conf.py index 1a6ef7ca8..008499c0e 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -92,7 +92,7 @@ exclude_patterns = [ # The reST default role (used for this markup: `text`) to use for all documents. -# default_role = None +default_role = "literal" # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True From b96e0a71a6ce4130b480b6c601befadbeec5dfe5 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 21 Nov 2019 01:23:36 +0100 Subject: [PATCH 79/95] pytester: LineMatcher: __tracebackhide__ with _fail Follow-up to 2228ccb (gone lost in resolving the conflict). --- src/_pytest/pytester.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index bacb1c23d..f44a69a95 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1481,6 +1481,7 @@ class LineMatcher: self._log_output = [] def _fail(self, msg): + __tracebackhide__ = True log_text = self._log_text self._log_output = [] pytest.fail(log_text) From 6659fe0edcff7fdedd8b28509f4e5e050c42edfa Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 21 Nov 2019 06:20:39 +0100 Subject: [PATCH 80/95] CHANGELOG: two minor fixes/improvements --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0f53fe33a..527d54316 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -157,7 +157,7 @@ Improvements immutable and avoid accidental modifications. -- `#6023 `_: ``pytest.main`` now returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). +- `#6023 `_: ``pytest.main`` returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). - `#6026 `_: Align prefixes in output of pytester's ``LineMatcher``. @@ -169,7 +169,7 @@ Improvements - `#6069 `_: ``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally. -- `#6097 `_: The "[XXX%]" indicator in the test summary is now colored according to the final (new) multi-colored line's main color. +- `#6097 `_: The "[...%]" indicator in the test summary is now colored according to the final (new) multi-colored line's main color. - `#6116 `_: Added ``--co`` as a synonym to ``--collect-only``. From 64eb9ea670fd4d31e46e41ee397933fcad05886d Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Thu, 21 Nov 2019 11:50:40 +0000 Subject: [PATCH 81/95] Modify test for new expected behaviour --- testing/test_runner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/test_runner.py b/testing/test_runner.py index 86e9bddff..301e11898 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -900,9 +900,9 @@ def test_store_except_info_on_error(): # The next run should clear the exception info stored by the previous run ItemMightRaise.raise_error = False runner.pytest_runtest_call(ItemMightRaise()) - assert sys.last_type is None - assert sys.last_value is None - assert sys.last_traceback is None + assert not hasattr(sys, "last_type") + assert not hasattr(sys, "last_value") + assert not hasattr(sys, "last_traceback") def test_current_test_env_var(testdir, monkeypatch): From 8f2fd8ffc039f45a8daaedd62e6a2ca9af2542f6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 20 Nov 2019 20:26:45 -0300 Subject: [PATCH 82/95] Add develop instructions to CONTRIBUTING From: https://github.com/pytest-dev/pytest/pull/6244 --- .gitignore | 1 + CONTRIBUTING.rst | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/.gitignore b/.gitignore index 27bd93c7b..fc61c6ee6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ dist/ issue/ env/ .env/ +.venv/ 3rdparty/ .tox .cache diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8e59191ab..a3ae731e4 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -262,6 +262,19 @@ Here is a simple overview, with pytest-specific bits: When committing, ``pre-commit`` will re-format the files if necessary. +#. If instead of using ``tox`` you prefer to run the tests directly, then we suggest to create a virtual environment and use + an editable install with the ``testing`` extra:: + + $ python3 -m venv .venv + $ source .venv/bin/activate # Linux + $ .venv/Scripts/activate.bat # Windows + $ pip install -e ".[testing]" + + Afterwards, you can edit the files and run pytest normally:: + + $ pytest testing/test_config.py + + #. Commit and push once your tests pass and you are happy with your change(s):: $ git commit -a -m "" From 2ffbe41ae526b5367fca798e405fceab9bc57228 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Thu, 21 Nov 2019 13:06:47 +0000 Subject: [PATCH 83/95] clear sys.last_traceback via del instead of = None --- src/_pytest/runner.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index c383146c3..67e28e905 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -121,7 +121,12 @@ def pytest_runtest_setup(item): def pytest_runtest_call(item): _update_current_test_var(item, "call") - sys.last_type, sys.last_value, sys.last_traceback = (None, None, None) + try: + del sys.last_type + del sys.last_value + del sys.last_traceback + except AttributeError: + pass try: item.runtest() except Exception: From 9d1082bd302832fe7289dfbed53a81a17eccf05c Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Thu, 21 Nov 2019 13:13:36 +0000 Subject: [PATCH 84/95] Add changelog file. --- changelog/6255.bugfix.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/6255.bugfix.rst diff --git a/changelog/6255.bugfix.rst b/changelog/6255.bugfix.rst new file mode 100644 index 000000000..831187feb --- /dev/null +++ b/changelog/6255.bugfix.rst @@ -0,0 +1,3 @@ +Clear the ``sys.last_traceback``, ``sys.last_type`` and ``sys.last_value`` +attributes by deleting them instead of setting them to ``None``. This better +matches the behaviour of the Python standard library. From 8d686a8e460fe8dd513efe2632481587ce0d4ab9 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Thu, 21 Nov 2019 13:14:19 +0000 Subject: [PATCH 85/95] Add self to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 14c465571..6e2f472fe 100644 --- a/AUTHORS +++ b/AUTHORS @@ -165,6 +165,7 @@ Marcelo Duarte Trevisani Marcin Bachry Marco Gorelli Mark Abramowitz +Mark Dickinson Markus Unterwaditzer Martijn Faassen Martin Altmayer From dbb8c146f041b4ffaa704097007d8bb1f160d990 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Thu, 21 Nov 2019 13:22:34 +0000 Subject: [PATCH 86/95] Use proper reST attribute markup. --- changelog/6255.bugfix.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/changelog/6255.bugfix.rst b/changelog/6255.bugfix.rst index 831187feb..6eb064ef3 100644 --- a/changelog/6255.bugfix.rst +++ b/changelog/6255.bugfix.rst @@ -1,3 +1,4 @@ -Clear the ``sys.last_traceback``, ``sys.last_type`` and ``sys.last_value`` -attributes by deleting them instead of setting them to ``None``. This better -matches the behaviour of the Python standard library. +Clear the :py:attr:`sys.last_traceback`, :py:attr:`sys.last_type` +and :py:attr:`sys.last_value` attributes by deleting them instead +of setting them to ``None``. This better matches the behaviour of +the Python standard library. From 82424c92708ed6fe9fba83ff4cbc3ce128e13494 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Thu, 21 Nov 2019 13:37:17 +0000 Subject: [PATCH 87/95] Fix reST markup. --- changelog/6255.bugfix.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/changelog/6255.bugfix.rst b/changelog/6255.bugfix.rst index 6eb064ef3..831187feb 100644 --- a/changelog/6255.bugfix.rst +++ b/changelog/6255.bugfix.rst @@ -1,4 +1,3 @@ -Clear the :py:attr:`sys.last_traceback`, :py:attr:`sys.last_type` -and :py:attr:`sys.last_value` attributes by deleting them instead -of setting them to ``None``. This better matches the behaviour of -the Python standard library. +Clear the ``sys.last_traceback``, ``sys.last_type`` and ``sys.last_value`` +attributes by deleting them instead of setting them to ``None``. This better +matches the behaviour of the Python standard library. From a02310a1401a185676f3421c6598434f2d73594a Mon Sep 17 00:00:00 2001 From: Philipp Loose Date: Fri, 22 Nov 2019 19:18:07 +0100 Subject: [PATCH 88/95] Add stacklevel tests for warnings, 'location' to pytest_warning_captured Resolves #4445 and #5928 (thanks to allanlewis) Add CHANGELOG for location parameter --- AUTHORS | 1 + changelog/4445.bugfix.rst | 1 + changelog/5928.bugfix.rst | 1 + changelog/5984.improvement.rst | 1 + src/_pytest/config/__init__.py | 10 +-- src/_pytest/hookspec.py | 6 +- src/_pytest/mark/structures.py | 1 + src/_pytest/warnings.py | 6 +- testing/test_warnings.py | 158 +++++++++++++++++++++++++++++++++ 9 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 changelog/4445.bugfix.rst create mode 100644 changelog/5928.bugfix.rst create mode 100644 changelog/5984.improvement.rst diff --git a/AUTHORS b/AUTHORS index 6e2f472fe..a3e526c5a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -207,6 +207,7 @@ Oscar Benjamin Patrick Hayes Paweł Adamczak Pedro Algarvio +Philipp Loose Pieter Mulder Piotr Banaszkiewicz Pulkit Goyal diff --git a/changelog/4445.bugfix.rst b/changelog/4445.bugfix.rst new file mode 100644 index 000000000..f7583b2bf --- /dev/null +++ b/changelog/4445.bugfix.rst @@ -0,0 +1 @@ +Fixed some warning reports produced by pytest to point to the correct location of the warning in the user's code. diff --git a/changelog/5928.bugfix.rst b/changelog/5928.bugfix.rst new file mode 100644 index 000000000..fbc53757d --- /dev/null +++ b/changelog/5928.bugfix.rst @@ -0,0 +1 @@ +Report ``PytestUnknownMarkWarning`` at the level of the user's code, not ``pytest``'s. diff --git a/changelog/5984.improvement.rst b/changelog/5984.improvement.rst new file mode 100644 index 000000000..1a0ad66f7 --- /dev/null +++ b/changelog/5984.improvement.rst @@ -0,0 +1 @@ +The ``pytest_warning_captured`` hook now receives a ``location`` parameter with the code location that generated the warning. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4e3323b0c..070d24bf5 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -133,13 +133,7 @@ def directory_arg(path, optname): # Plugins that cannot be disabled via "-p no:X" currently. -essential_plugins = ( - "mark", - "main", - "runner", - "fixtures", - "helpconfig", # Provides -p. -) +essential_plugins = ("mark", "main", "runner", "fixtures", "helpconfig") # Provides -p. default_plugins = essential_plugins + ( "python", @@ -589,7 +583,7 @@ class PytestPluginManager(PluginManager): _issue_warning_captured( PytestConfigWarning("skipped plugin {!r}: {}".format(modname, e.msg)), self.hook, - stacklevel=1, + stacklevel=2, ) else: mod = sys.modules[importspec] diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 03e060eb8..74dff1e82 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -562,7 +562,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): @hookspec(historic=True) -def pytest_warning_captured(warning_message, when, item): +def pytest_warning_captured(warning_message, when, item, location): """ Process a warning captured by the internal pytest warnings plugin. @@ -582,6 +582,10 @@ def pytest_warning_captured(warning_message, when, item): in a future release. The item being executed if ``when`` is ``"runtest"``, otherwise ``None``. + + :param tuple location: + Holds information about the execution context of the captured warning (filename, linenumber, function). + ``function`` evaluates to when the execution context is at the module level. """ diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index a4ec9665c..020260dd5 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -347,6 +347,7 @@ class MarkGenerator: "custom marks to avoid this warning - for details, see " "https://docs.pytest.org/en/latest/mark.html" % name, PytestUnknownMarkWarning, + 2, ) return MarkDecorator(Mark(name, (), {})) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 8ac1ee225..b6ee049ec 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -149,6 +149,10 @@ def _issue_warning_captured(warning, hook, stacklevel): warnings.warn(warning, stacklevel=stacklevel) # Mypy can't infer that record=True means records is not None; help it. assert records is not None + frame = sys._getframe(stacklevel - 1) + location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name hook.pytest_warning_captured.call_historic( - kwargs=dict(warning_message=records[0], when="config", item=None) + kwargs=dict( + warning_message=records[0], when="config", item=None, location=location + ) ) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index c4af14dac..8a9cc618f 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -1,3 +1,4 @@ +import os import warnings import pytest @@ -641,3 +642,160 @@ def test_pytest_configure_warning(testdir, recwarn): assert "INTERNALERROR" not in result.stderr.str() warning = recwarn.pop() assert str(warning.message) == "from pytest_configure" + + +class TestStackLevel: + @pytest.fixture + def capwarn(self, testdir): + class CapturedWarnings: + captured = [] + + @classmethod + def pytest_warning_captured(cls, warning_message, when, item, location): + cls.captured.append((warning_message, location)) + + testdir.plugins = [CapturedWarnings()] + + return CapturedWarnings + + def test_issue4445_rewrite(self, testdir, capwarn): + """#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( + """ + import some_mod + import pytest + + pytest.register_assert_rewrite("some_mod") + """ + ) + testdir.parseconfig() + + # with stacklevel=5 the warning originates from register_assert_rewrite + # function in the created conftest.py + assert len(capwarn.captured) == 1 + warning, location = capwarn.captured.pop() + file, lineno, func = location + + assert "Module already imported" in str(warning.message) + assert file == str(conftest) + assert func == "" # the above conftest.py + assert lineno == 4 + + def test_issue4445_preparse(self, testdir, capwarn): + """#4445: Make sure the warning points to a reasonable location + See origin of _issue_warning_captured at: _pytest.config.__init__.py:910 + """ + testdir.makeconftest( + """ + import nothing + """ + ) + testdir.parseconfig("--help") + + # with stacklevel=2 the warning should originate from config._preparse and is + # thrown by an errorneous conftest.py + assert len(capwarn.captured) == 1 + warning, location = capwarn.captured.pop() + 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 func == "_preparse" + + 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 + """ + testdir.makepyfile( + some_plugin=""" + import pytest + pytest.skip("thing", allow_module_level=True) + """ + ) + testdir.syspathinsert() + testdir.parseconfig("-p", "some_plugin") + + # with stacklevel=2 the warning should originate from + # config.PytestPluginManager.import_plugin is thrown by a skipped plugin + + # During config parsing the the pluginargs are checked in a while loop + # that as a result of the argument count runs import_plugin twice, hence + # two identical warnings are captured (is this intentional?). + assert len(capwarn.captured) == 2 + warning, location = capwarn.captured.pop() + 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 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_cacheprovider_set(self, testdir, capwarn): + """#4445: Make sure the warning points to a reasonable location + See origin of _issue_warning_captured at: _pytest.cacheprovider.py:59 + """ + testdir.tmpdir.join(".pytest_cache").write("something wrong") + testdir.runpytest(plugins=[capwarn()]) + + # with stacklevel=3 the warning originates from one stacklevel above + # _issue_warning_captured in cacheprovider.Cache.set and is thrown + # when there are errors during cache folder creation + + # set is called twice (in module stepwise and in cacheprovider) so emits + # two warnings when there are errors during cache folder creation. (is this intentional?) + assert len(capwarn.captured) == 2 + warning, location = capwarn.captured.pop() + file, lineno, func = location + + assert "could not create cache path" in str(warning.message) + assert "cacheprovider.py" in file + assert func == "set" + + 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. + """ + testfile = testdir.makepyfile( + """ + import pytest + + @pytest.mark.unknown + def test_it(): + pass + """ + ) + result = testdir.runpytest_subprocess() + # with stacklevel=2 the warning should originate from the above created test file + result.stdout.fnmatch_lines_random( + [ + "*{testfile}:3*".format(testfile=str(testfile)), + "*Unknown pytest.mark.unknown*", + ] + ) From b0ebcfb7857ef9e14064ca22baad4f6623f0251a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 16:00:01 +0100 Subject: [PATCH 89/95] pytester: remove special handling of env during inner runs Closes https://github.com/pytest-dev/pytest/issues/6213. --- changelog/6213.improvement.rst | 1 + src/_pytest/pytester.py | 16 +++------------- testing/test_pdb.py | 2 +- testing/test_pytester.py | 24 +++++++++--------------- 4 files changed, 14 insertions(+), 29 deletions(-) create mode 100644 changelog/6213.improvement.rst diff --git a/changelog/6213.improvement.rst b/changelog/6213.improvement.rst new file mode 100644 index 000000000..735d4455f --- /dev/null +++ b/changelog/6213.improvement.rst @@ -0,0 +1 @@ +pytester: the ``testdir`` fixture respects environment settings from the ``monkeypatch`` fixture for inner runs. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index f44a69a95..97ad953a3 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -547,7 +547,8 @@ class Testdir: # Environment (updates) for inner runs. tmphome = str(self.tmpdir) - self._env_run_update = {"HOME": tmphome, "USERPROFILE": tmphome} + mp.setenv("HOME", tmphome) + mp.setenv("USERPROFILE", tmphome) def __repr__(self): return "".format(self.tmpdir) @@ -853,12 +854,6 @@ class Testdir: plugins = list(plugins) finalizers = [] try: - # Do not load user config (during runs only). - mp_run = MonkeyPatch() - for k, v in self._env_run_update.items(): - mp_run.setenv(k, v) - finalizers.append(mp_run.undo) - # Any sys.module or sys.path changes done while running pytest # inline should be reverted after the test run completes to avoid # clashing with later inline tests run within the same pytest test, @@ -1091,7 +1086,6 @@ class Testdir: env["PYTHONPATH"] = os.pathsep.join( filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) ) - env.update(self._env_run_update) kw["env"] = env if stdin is Testdir.CLOSE_STDIN: @@ -1261,11 +1255,7 @@ class Testdir: pytest.skip("pexpect.spawn not available") logfile = self.tmpdir.join("spawn.out").open("wb") - # Do not load user config. - env = os.environ.copy() - env.update(self._env_run_update) - - child = pexpect.spawn(cmd, logfile=logfile, env=env) + child = pexpect.spawn(cmd, logfile=logfile) self.request.addfinalizer(logfile.close) child.timeout = expect_timeout return child diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 25d2292e9..0f7816dbe 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -22,7 +22,7 @@ def pdb_env(request): if "testdir" in request.fixturenames: # Disable pdb++ with inner tests. testdir = request.getfixturevalue("testdir") - testdir._env_run_update["PDBPP_HIJACK_PDB"] = "0" + testdir.monkeypatch.setenv("PDBPP_HIJACK_PDB", "0") def runpdb_and_get_report(testdir, source): diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 5bdbacdd0..3dab13b4b 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -550,17 +550,15 @@ def test_no_matching_after_match(): assert str(e.value).splitlines() == ["fnmatch: '*'", " with: '1'"] -def test_pytester_addopts(request, monkeypatch): +def test_pytester_addopts_before_testdir(request, monkeypatch): + orig = os.environ.get("PYTEST_ADDOPTS", None) monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused") - testdir = request.getfixturevalue("testdir") - - try: - assert "PYTEST_ADDOPTS" not in os.environ - finally: - testdir.finalize() - - assert os.environ["PYTEST_ADDOPTS"] == "--orig-unused" + assert "PYTEST_ADDOPTS" not in os.environ + testdir.finalize() + assert os.environ.get("PYTEST_ADDOPTS") == "--orig-unused" + monkeypatch.undo() + assert os.environ.get("PYTEST_ADDOPTS") == orig def test_run_stdin(testdir): @@ -640,14 +638,10 @@ def test_popen_default_stdin_stderr_and_stdin_None(testdir): def test_spawn_uses_tmphome(testdir): - import os - tmphome = str(testdir.tmpdir) + assert os.environ.get("HOME") == tmphome - # Does use HOME only during run. - assert os.environ.get("HOME") != tmphome - - testdir._env_run_update["CUSTOMENV"] = "42" + testdir.monkeypatch.setenv("CUSTOMENV", "42") p1 = testdir.makepyfile( """ From c99c7d0f95170a5e308ed6c37e63b2d90794a4f9 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 16 Oct 2019 21:52:04 +0200 Subject: [PATCH 90/95] deprecate direct node construction and introduce Node.from_parent --- changelog/5975.deprecation.rst | 6 ++++++ src/_pytest/deprecated.py | 6 ++++++ src/_pytest/doctest.py | 16 ++++++++++++---- src/_pytest/main.py | 6 +++++- src/_pytest/nodes.py | 20 +++++++++++++++++++- src/_pytest/pytester.py | 4 ++-- src/_pytest/python.py | 28 ++++++++++++++++++---------- src/_pytest/unittest.py | 14 ++++++++------ testing/deprecated_test.py | 17 +++++++++++++++++ testing/python/collect.py | 6 +++--- testing/python/integration.py | 4 ++-- testing/python/metafunc.py | 2 +- testing/test_collection.py | 6 +++--- testing/test_mark.py | 6 +++++- testing/test_pluginmanager.py | 2 +- 15 files changed, 108 insertions(+), 35 deletions(-) create mode 100644 changelog/5975.deprecation.rst diff --git a/changelog/5975.deprecation.rst b/changelog/5975.deprecation.rst new file mode 100644 index 000000000..6e5dbc2ac --- /dev/null +++ b/changelog/5975.deprecation.rst @@ -0,0 +1,6 @@ +Deprecate using direct constructors for ``Nodes``. + +Instead they are new constructed via ``Node.from_parent``. + +This transitional mechanism enables us to detangle the very intensely +entangled ``Node`` relationships by enforcing more controlled creation/configruation patterns. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 5a7066041..1fdc37c04 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -9,6 +9,7 @@ All constants defined in this module should be either PytestWarning instances or in case of warnings which need to format their messages. """ from _pytest.warning_types import PytestDeprecationWarning +from _pytest.warning_types import UnformattedWarning # set of plugins which have been integrated into the core; we use this list to ignore # them during registration to avoid conflicts @@ -35,6 +36,11 @@ FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning( "as a keyword argument instead." ) +NODE_USE_FROM_PARENT = UnformattedWarning( + PytestDeprecationWarning, + "direct construction of {name} has been deprecated, please use {name}.from_parent", +) + JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning( "The 'junit_family' default value will change to 'xunit2' in pytest 6.0.\n" "Add 'junit_family=legacy' to your pytest.ini file to silence this warning and make your suite compatible." diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index f7d96257e..66fbf8396 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -108,9 +108,9 @@ def pytest_collect_file(path, parent): config = parent.config if path.ext == ".py": if config.option.doctestmodules and not _is_setup_py(config, path, parent): - return DoctestModule(path, parent) + return DoctestModule.from_parent(parent, fspath=path) elif _is_doctest(config, path, parent): - return DoctestTextfile(path, parent) + return DoctestTextfile.from_parent(parent, fspath=path) def _is_setup_py(config, path, parent): @@ -215,6 +215,10 @@ class DoctestItem(pytest.Item): self.obj = None self.fixture_request = None + @classmethod + def from_parent(cls, parent, *, name, runner, dtest): + return cls._create(name=name, parent=parent, runner=runner, dtest=dtest) + def setup(self): if self.dtest is not None: self.fixture_request = _setup_fixtures(self) @@ -370,7 +374,9 @@ class DoctestTextfile(pytest.Module): parser = doctest.DocTestParser() test = parser.get_doctest(text, globs, name, filename, 0) if test.examples: - yield DoctestItem(test.name, self, runner, test) + yield DoctestItem.from_parent( + self, name=test.name, runner=runner, dtest=test + ) def _check_all_skipped(test): @@ -467,7 +473,9 @@ class DoctestModule(pytest.Module): for test in finder.find(module, module.__name__): if test.examples: # skip empty doctests - yield DoctestItem(test.name, self, runner, test) + yield DoctestItem.from_parent( + self, name=test.name, runner=runner, dtest=test + ) def _setup_fixtures(doctest_item): diff --git a/src/_pytest/main.py b/src/_pytest/main.py index b4261c188..e46f54d9c 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -184,7 +184,7 @@ def pytest_addoption(parser): def wrap_session(config, doit): """Skeleton command line program""" - session = Session(config) + session = Session.from_config(config) session.exitstatus = ExitCode.OK initstate = 0 try: @@ -395,6 +395,10 @@ class Session(nodes.FSCollector): self.config.pluginmanager.register(self, name="session") + @classmethod + def from_config(cls, config): + return cls._create(config) + def __repr__(self): return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % ( self.__class__.__name__, diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 33067334c..3eaafa91d 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -18,6 +18,7 @@ from _pytest._code.code import ReprExceptionInfo from _pytest.compat import cached_property from _pytest.compat import getfslineno from _pytest.config import Config +from _pytest.deprecated import NODE_USE_FROM_PARENT from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureLookupErrorRepr @@ -73,7 +74,16 @@ def ischildnode(baseid, nodeid): return node_parts[: len(base_parts)] == base_parts -class Node: +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) + + def _create(self, *k, **kw): + return super().__call__(*k, **kw) + + +class Node(metaclass=NodeMeta): """ base class for Collector and Item the test collection tree. Collector subclasses have children, Items are terminal nodes.""" @@ -133,6 +143,10 @@ class Node: if self.name != "()": self._nodeid += "::" + self.name + @classmethod + def from_parent(cls, parent, *, name): + return cls._create(parent=parent, name=name) + @property def ihook(self): """ fspath sensitive hook proxy used to call pytest hooks""" @@ -418,6 +432,10 @@ class FSCollector(Collector): super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath) + @classmethod + def from_parent(cls, parent, *, fspath): + return cls._create(parent=parent, fspath=fspath) + class File(FSCollector): """ base class for collecting tests from a file. """ diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 97ad953a3..a1acf747e 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -744,7 +744,7 @@ class Testdir: :param arg: a :py:class:`py.path.local` instance of the file """ - session = Session(config) + session = Session.from_config(config) assert "::" not in str(arg) p = py.path.local(arg) config.hook.pytest_sessionstart(session=session) @@ -762,7 +762,7 @@ class Testdir: """ config = self.parseconfigure(path) - session = Session(config) + session = Session.from_config(config) x = session.fspath.bestrelpath(path) config.hook.pytest_sessionstart(session=session) res = session.perform_collect([x], genitems=False)[0] diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 4e3a68867..95bae5a23 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -190,8 +190,8 @@ def path_matches_patterns(path, patterns): def pytest_pycollect_makemodule(path, parent): if path.basename == "__init__.py": - return Package(path, parent) - return Module(path, parent) + return Package.from_parent(parent, fspath=path) + return Module.from_parent(parent, fspath=path) @hookimpl(hookwrapper=True) @@ -203,7 +203,7 @@ def pytest_pycollect_makeitem(collector, name, obj): # nothing was collected elsewhere, let's do it here if safe_isclass(obj): if collector.istestclass(obj, name): - outcome.force_result(Class(name, parent=collector)) + outcome.force_result(Class.from_parent(collector, name=name, obj=obj)) elif collector.istestfunction(obj, name): # mock seems to store unbound methods (issue473), normalize it obj = getattr(obj, "__func__", obj) @@ -222,7 +222,7 @@ def pytest_pycollect_makeitem(collector, name, obj): ) elif getattr(obj, "__test__", True): if is_generator(obj): - res = Function(name, parent=collector) + res = Function.from_parent(collector, name=name) reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format( name=name ) @@ -381,7 +381,7 @@ class PyCollector(PyobjMixin, nodes.Collector): cls = clscol and clscol.obj or None fm = self.session._fixturemanager - definition = FunctionDefinition(name=name, parent=self, callobj=funcobj) + definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj) fixtureinfo = fm.getfixtureinfo(definition, funcobj, cls) metafunc = Metafunc( @@ -396,7 +396,7 @@ class PyCollector(PyobjMixin, nodes.Collector): self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) if not metafunc._calls: - yield Function(name, parent=self, fixtureinfo=fixtureinfo) + yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) else: # add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm) @@ -408,9 +408,9 @@ class PyCollector(PyobjMixin, nodes.Collector): for callspec in metafunc._calls: subname = "{}[{}]".format(name, callspec.id) - yield Function( + yield Function.from_parent( + self, name=subname, - parent=self, callspec=callspec, callobj=funcobj, fixtureinfo=fixtureinfo, @@ -626,7 +626,7 @@ class Package(Module): if init_module.check(file=1) and path_matches_patterns( init_module, self.config.getini("python_files") ): - yield Module(init_module, self) + yield Module.from_parent(self, fspath=init_module) pkg_prefixes = set() for path in this_path.visit(rec=self._recurse, bf=True, sort=True): # We will visit our own __init__.py file, in which case we skip it. @@ -677,6 +677,10 @@ def _get_first_non_fixture_func(obj, names): class Class(PyCollector): """ Collector for test methods. """ + @classmethod + def from_parent(cls, parent, *, name, obj=None): + return cls._create(name=name, parent=parent) + def collect(self): if not safe_getattr(self.obj, "__test__", True): return [] @@ -702,7 +706,7 @@ class Class(PyCollector): self._inject_setup_class_fixture() self._inject_setup_method_fixture() - return [Instance(name="()", parent=self)] + return [Instance.from_parent(self, name="()")] def _inject_setup_class_fixture(self): """Injects a hidden autouse, class scoped fixture into the collected class object @@ -1454,6 +1458,10 @@ class Function(FunctionMixin, nodes.Item): #: .. versionadded:: 3.0 self.originalname = originalname + @classmethod + def from_parent(cls, parent, **kw): + return cls._create(parent=parent, **kw) + def _initrequest(self): self.funcargs = {} self._request = fixtures.FixtureRequest(self) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 11dc77cc4..1ddb9c867 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -23,7 +23,7 @@ def pytest_pycollect_makeitem(collector, name, obj): except Exception: return # yes, so let's collect it - return UnitTestCase(name, parent=collector) + return UnitTestCase.from_parent(collector, name=name, obj=obj) class UnitTestCase(Class): @@ -51,7 +51,7 @@ class UnitTestCase(Class): if not getattr(x, "__test__", True): continue funcobj = getimfunc(x) - yield TestCaseFunction(name, parent=self, callobj=funcobj) + yield TestCaseFunction.from_parent(self, name=name, callobj=funcobj) foundsomething = True if not foundsomething: @@ -59,7 +59,8 @@ class UnitTestCase(Class): if runtest is not None: ut = sys.modules.get("twisted.trial.unittest", None) if ut is None or runtest != ut.TestCase.runTest: - yield TestCaseFunction("runTest", parent=self) + # TODO: callobj consistency + yield TestCaseFunction.from_parent(self, name="runTest") def _inject_setup_teardown_fixtures(self, cls): """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding @@ -190,20 +191,21 @@ class TestCaseFunction(Function): def _handle_skip(self): # implements the skipping machinery (see #2137) # analog to pythons Lib/unittest/case.py:run - testMethod = getattr(self._testcase, self._testcase._testMethodName) + test_method = getattr(self._testcase, self._testcase._testMethodName) if getattr(self._testcase.__class__, "__unittest_skip__", False) or getattr( - testMethod, "__unittest_skip__", False + test_method, "__unittest_skip__", False ): # If the class or method was skipped. skip_why = getattr( self._testcase.__class__, "__unittest_skip_why__", "" - ) or getattr(testMethod, "__unittest_skip_why__", "") + ) or getattr(test_method, "__unittest_skip_why__", "") self._testcase._addSkip(self, self._testcase, skip_why) return True return False def runtest(self): if self.config.pluginmanager.get_plugin("pdbinvoke") is None: + # TODO: move testcase reporter into separate class, this shouldnt be on item self._testcase(result=self) else: # disables tearDown and cleanups for post mortem debugging (see #1890) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 5390d038d..59cb69a00 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,5 +1,8 @@ +import inspect + import pytest from _pytest import deprecated +from _pytest import nodes @pytest.mark.filterwarnings("default") @@ -73,3 +76,17 @@ def test_warn_about_imminent_junit_family_default_change(testdir, junit_family): result.stdout.no_fnmatch_line(warning_msg) else: result.stdout.fnmatch_lines([warning_msg]) + + +def test_node_direct_ctor_warning(): + 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") + assert w[0].lineno == inspect.currentframe().f_lineno - 1 + assert w[0].filename == __file__ diff --git a/testing/python/collect.py b/testing/python/collect.py index e036cb7d9..9ac1c9d31 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -281,10 +281,10 @@ class TestFunction: from _pytest.fixtures import FixtureManager config = testdir.parseconfigure() - session = testdir.Session(config) + session = testdir.Session.from_config(config) session._fixturemanager = FixtureManager(session) - return pytest.Function(config=config, parent=session, **kwargs) + return pytest.Function.from_parent(config=config, parent=session, **kwargs) def test_function_equality(self, testdir, tmpdir): def func1(): @@ -1024,7 +1024,7 @@ class TestReportInfo: return "ABCDE", 42, "custom" def pytest_pycollect_makeitem(collector, name, obj): if name == "test_func": - return MyFunction(name, parent=collector) + return MyFunction.from_parent(name=name, parent=collector) """ ) item = testdir.getitem("def test_func(): pass") diff --git a/testing/python/integration.py b/testing/python/integration.py index 73419eef4..35e86e6b9 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -10,7 +10,7 @@ class TestOEJSKITSpecials: import pytest def pytest_pycollect_makeitem(collector, name, obj): if name == "MyClass": - return MyCollector(name, parent=collector) + return MyCollector.from_parent(collector, name=name) class MyCollector(pytest.Collector): def reportinfo(self): return self.fspath, 3, "xyz" @@ -40,7 +40,7 @@ class TestOEJSKITSpecials: import pytest def pytest_pycollect_makeitem(collector, name, obj): if name == "MyClass": - return MyCollector(name, parent=collector) + return MyCollector.from_parent(collector, name=name) class MyCollector(pytest.Collector): def reportinfo(self): return self.fspath, 3, "xyz" diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 9a1e1f968..9b6471cdc 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -30,7 +30,7 @@ class TestMetafunc: names = fixtures.getfuncargnames(func) fixtureinfo = FixtureInfo(names) - definition = DefinitionMock(func) + definition = DefinitionMock._create(func) return python.Metafunc(definition, fixtureinfo, config) def test_no_funcargs(self, testdir): diff --git a/testing/test_collection.py b/testing/test_collection.py index b791ac6f9..624e9dd4e 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -75,7 +75,7 @@ class TestCollector: pass def pytest_collect_file(path, parent): if path.ext == ".xxx": - return CustomFile(path, parent=parent) + return CustomFile.from_parent(fspath=path, parent=parent) """ ) node = testdir.getpathnode(hello) @@ -446,7 +446,7 @@ class TestSession: p.move(target) subdir.chdir() config = testdir.parseconfig(p.basename) - rcol = Session(config=config) + rcol = Session.from_config(config) assert rcol.fspath == subdir parts = rcol._parsearg(p.basename) @@ -463,7 +463,7 @@ class TestSession: # XXX migrate to collectonly? (see below) config = testdir.parseconfig(id) topdir = testdir.tmpdir - rcol = Session(config) + rcol = Session.from_config(config) assert topdir == rcol.fspath # rootid = rcol.nodeid # root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0] diff --git a/testing/test_mark.py b/testing/test_mark.py index 0e4422025..33276b63c 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -962,7 +962,11 @@ def test_mark_expressions_no_smear(testdir): def test_addmarker_order(): - node = Node("Test", config=mock.Mock(), session=mock.Mock(), nodeid="Test") + session = mock.Mock() + session.own_markers = [] + session.parent = None + session.nodeid = "" + node = Node.from_parent(session, name="Test") node.add_marker("foo") node.add_marker("bar") node.add_marker("baz", append=False) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 836b458c6..56d5a7625 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -122,7 +122,7 @@ class TestPytestPluginInteractions: def test_hook_proxy(self, testdir): """Test the gethookproxy function(#2016)""" config = testdir.parseconfig() - session = Session(config) + session = Session.from_config(config) testdir.makepyfile(**{"tests/conftest.py": "", "tests/subdir/conftest.py": ""}) conftest1 = testdir.tmpdir.join("tests/conftest.py") From 15ffe6320498f37a57dd23a45866e5903d61f56c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 23 Nov 2019 01:08:56 +0100 Subject: [PATCH 91/95] update doc examples **untested** --- doc/en/deprecations.rst | 10 ++++++++++ doc/en/example/nonpython/conftest.py | 4 ++-- doc/en/example/py2py3/conftest.py | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 748d3ac65..88112b12a 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:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. + +Node Construction changed to ``Node.from_parent`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.3 + +The construction of nodes new should use the named constructor ``from_parent``. +This limitation in api surface intends to enable better/simpler refactoring of the collection tree. + + ``junit_family`` default value change to "xunit2" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py index 93d8285bf..d30ab3841 100644 --- a/doc/en/example/nonpython/conftest.py +++ b/doc/en/example/nonpython/conftest.py @@ -4,7 +4,7 @@ import pytest def pytest_collect_file(parent, path): if path.ext == ".yaml" and path.basename.startswith("test"): - return YamlFile(path, parent) + return YamlFile.from_parent(parent, fspath=path) class YamlFile(pytest.File): @@ -13,7 +13,7 @@ class YamlFile(pytest.File): raw = yaml.safe_load(self.fspath.open()) for name, spec in sorted(raw.items()): - yield YamlItem(name, self, spec) + yield YamlItem.from_parent(self, name=name, spec=spec) class YamlItem(pytest.Item): diff --git a/doc/en/example/py2py3/conftest.py b/doc/en/example/py2py3/conftest.py index 844510a25..0291b37b4 100644 --- a/doc/en/example/py2py3/conftest.py +++ b/doc/en/example/py2py3/conftest.py @@ -13,4 +13,4 @@ class DummyCollector(pytest.collect.File): def pytest_pycollect_makemodule(path, parent): bn = path.basename if "py3" in bn and not py3 or ("py2" in bn and py3): - return DummyCollector(path, parent=parent) + return DummyCollector.from_parent(parent, fspath=path) From bc7282576f0490a67795b1844ed8e6c6ab497223 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Nov 2019 23:20:03 +0100 Subject: [PATCH 92/95] typing: minor improvements --- src/_pytest/fixtures.py | 10 ++++------ src/_pytest/main.py | 2 ++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 34ecf2e21..44802e000 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -36,6 +36,7 @@ if False: # TYPE_CHECKING from typing import Type from _pytest import nodes + from _pytest.main import Session @attr.s(frozen=True) @@ -44,7 +45,7 @@ class PseudoFixtureDef: scope = attr.ib() -def pytest_sessionstart(session): +def pytest_sessionstart(session: "Session"): import _pytest.python import _pytest.nodes @@ -510,13 +511,11 @@ class FixtureRequest: values.append(fixturedef) current = current._parent_request - def _compute_fixture_value(self, fixturedef): + 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. - - :param FixtureDef fixturedef: """ # prepare a subrequest object before calling fixture function # (latter managed by fixturedef) @@ -544,9 +543,8 @@ class FixtureRequest: if has_params: frame = inspect.stack()[3] frameinfo = inspect.getframeinfo(frame[0]) - source_path = frameinfo.filename + source_path = py.path.local(frameinfo.filename) source_lineno = frameinfo.lineno - source_path = py.path.local(source_path) if source_path.relto(funcitem.config.rootdir): source_path = source_path.relto(funcitem.config.rootdir) msg = ( diff --git a/src/_pytest/main.py b/src/_pytest/main.py index e46f54d9c..53bfbeb5a 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -15,6 +15,7 @@ from _pytest import nodes from _pytest.config import directory_arg from _pytest.config import hookimpl from _pytest.config import UsageError +from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit from _pytest.runner import collect_one_node from _pytest.runner import SetupState @@ -372,6 +373,7 @@ class Session(nodes.FSCollector): Interrupted = Interrupted Failed = Failed _setupstate = None # type: SetupState + _fixturemanager = None # type: FixtureManager def __init__(self, config): nodes.FSCollector.__init__( From a1219ab8fc4522cc22e8b17a90e9c0e44612d998 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 2 Nov 2019 13:13:33 +0100 Subject: [PATCH 93/95] pytester: use no colors with inline runs by default Sets `PY_COLORS=0` in the environment by default, which is used by pylib. Via https://github.com/blueyed/pytest/pull/58 (initially cherry picked from commit f153ad33d10) --- src/_pytest/pytester.py | 5 +++-- testing/test_pdb.py | 14 +++++++------- testing/test_terminal.py | 2 ++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a1acf747e..d5744167c 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -544,11 +544,12 @@ class Testdir: mp.delenv("TOX_ENV_DIR", raising=False) # Discard outer pytest options. mp.delenv("PYTEST_ADDOPTS", raising=False) - - # Environment (updates) for inner runs. + # Ensure no user config is used. tmphome = str(self.tmpdir) 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): return "".format(self.tmpdir) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 0f7816dbe..8949b0de8 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -193,7 +193,7 @@ class TestPDB: ) child = testdir.spawn_pytest("-rs --pdb %s" % p1) child.expect("Skipping also with pdb active") - child.expect_exact("= \x1b[33m\x1b[1m1 skipped\x1b[0m\x1b[33m in") + child.expect_exact("= 1 skipped in") child.sendeof() self.flush(child) @@ -221,7 +221,7 @@ class TestPDB: child.sendeof() rest = child.read().decode("utf8") assert "Exit: Quitting debugger" in rest - assert "= \x1b[31m\x1b[1m1 failed\x1b[0m\x1b[31m in" in rest + assert "= 1 failed in" in rest assert "def test_1" not in rest assert "get rekt" not in rest self.flush(child) @@ -506,7 +506,7 @@ class TestPDB: rest = child.read().decode("utf8") assert "! _pytest.outcomes.Exit: Quitting debugger !" in rest - assert "= \x1b[33mno tests ran\x1b[0m\x1b[33m in" in rest + assert "= no tests ran in" in rest assert "BdbQuit" not in rest assert "UNEXPECTED EXCEPTION" not in rest @@ -725,7 +725,7 @@ class TestPDB: assert "> PDB continue (IO-capturing resumed) >" in rest else: assert "> PDB continue >" in rest - assert "= \x1b[32m\x1b[1m1 passed\x1b[0m\x1b[32m in" in rest + assert "= 1 passed in" in rest def test_pdb_used_outside_test(self, testdir): p1 = testdir.makepyfile( @@ -1041,7 +1041,7 @@ class TestTraceOption: child.sendline("q") child.expect_exact("Exit: Quitting debugger") rest = child.read().decode("utf8") - assert "= \x1b[32m\x1b[1m2 passed\x1b[0m\x1b[32m in" in rest + assert "= 2 passed in" in rest assert "reading from stdin while output" not in rest # Only printed once - not on stderr. assert "Exit: Quitting debugger" not in child.before.decode("utf8") @@ -1086,7 +1086,7 @@ class TestTraceOption: child.sendline("c") child.expect_exact("> PDB continue (IO-capturing resumed) >") rest = child.read().decode("utf8") - assert "= \x1b[32m\x1b[1m6 passed\x1b[0m\x1b[32m in" in rest + assert "= 6 passed in" in rest assert "reading from stdin while output" not in rest # Only printed once - not on stderr. assert "Exit: Quitting debugger" not in child.before.decode("utf8") @@ -1197,7 +1197,7 @@ def test_pdb_suspends_fixture_capturing(testdir, fixture): TestPDB.flush(child) assert child.exitstatus == 0 - assert "= \x1b[32m\x1b[1m1 passed\x1b[0m\x1b[32m in" in rest + assert "= 1 passed in" in rest assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest diff --git a/testing/test_terminal.py b/testing/test_terminal.py index d31033197..fab13b07e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -154,6 +154,8 @@ class TestTerminal: "test2.py": "def test_2(): pass", } ) + # Explicitly test colored output. + testdir.monkeypatch.setenv("PY_COLORS", "1") child = testdir.spawn_pytest("-v test1.py test2.py") child.expect(r"collecting \.\.\.") From 8feeb093984fcb17491bc1b6d6dcaf94a4cbdae0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 27 Nov 2019 11:25:23 +0100 Subject: [PATCH 94/95] fixes #5065 --- testing/{test_pdb.py => test_debugging.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename testing/{test_pdb.py => test_debugging.py} (100%) diff --git a/testing/test_pdb.py b/testing/test_debugging.py similarity index 100% rename from testing/test_pdb.py rename to testing/test_debugging.py From 16ff9f591e38d1f2a79441f177130b1d89098c6e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 2 Dec 2019 18:01:08 +0200 Subject: [PATCH 95/95] Update mypy 0.740 -> 0.750 Release notes: https://mypy-lang.blogspot.com/2019/11/mypy-0.html --- .pre-commit-config.yaml | 2 +- src/_pytest/_code/code.py | 2 +- src/_pytest/_code/source.py | 4 +++- src/_pytest/assertion/rewrite.py | 5 ++--- src/_pytest/compat.py | 7 ++----- src/_pytest/recwarn.py | 4 +++- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d4196152f..9548cd079 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: - id: pyupgrade args: [--py3-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.740 + rev: v0.750 hooks: - id: mypy files: ^(src/|testing/) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index a8f117366..d1a8ec2f1 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -479,7 +479,7 @@ class ExceptionInfo(Generic[_E]): assert tup[1] is not None, "no current exception" assert tup[2] is not None, "no current exception" exc_info = (tup[0], tup[1], tup[2]) - return cls.from_exc_info(exc_info, exprinfo) + return ExceptionInfo.from_exc_info(exc_info, exprinfo) @classmethod def for_later(cls) -> "ExceptionInfo[_E]": diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index d7cef683d..ac3ee231e 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -339,7 +339,9 @@ def getstatementrange_ast( block_finder.started = source.lines[start][0].isspace() it = ((x + "\n") for x in source.lines[start:end]) try: - for tok in tokenize.generate_tokens(lambda: next(it)): + # Type ignored until next mypy release. + # https://github.com/python/typeshed/commit/c0d46a20353b733befb85d8b9cc24e5b0bcd8f9a + for tok in tokenize.generate_tokens(lambda: next(it)): # type: ignore block_finder.tokeneater(*tok) except (inspect.EndOfBlock, IndentationError): end = block_finder.last + start diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 51ea1801b..6bfb876e4 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -1074,14 +1074,13 @@ def try_makedirs(cache_dir) -> bool: def get_cache_dir(file_path: Path) -> Path: """Returns the cache directory to write .pyc files for the given .py file path""" - # Type ignored until added in next mypy release. - if sys.version_info >= (3, 8) and sys.pycache_prefix: # type: ignore + if sys.version_info >= (3, 8) and sys.pycache_prefix: # given: # prefix = '/tmp/pycs' # path = '/home/user/proj/test_app.py' # we want: # '/tmp/pycs/home/user/proj' - return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) # type: ignore + return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) else: # classic pycache directory return file_path.parent / "__pycache__" diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index fc810b3e5..8dd74b577 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -43,8 +43,7 @@ MODULE_NOT_FOUND_ERROR = ( if sys.version_info >= (3, 8): - # Type ignored until next mypy release. - from importlib import metadata as importlib_metadata # type: ignore + from importlib import metadata as importlib_metadata else: import importlib_metadata # noqa: F401 @@ -385,9 +384,7 @@ else: if sys.version_info >= (3, 8): - # TODO: Remove type ignore on next mypy update. - # https://github.com/python/typeshed/commit/add0b5e930a1db16560fde45a3b710eefc625709 - from functools import cached_property # type: ignore + from functools import cached_property else: class cached_property(Generic[_S, _T]): diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 5cf32c894..e3d7b72ec 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -129,7 +129,9 @@ def warns( # noqa: F811 return func(*args[1:], **kwargs) -class WarningsRecorder(warnings.catch_warnings): +# Type ignored until next mypy release. Regression fixed by: +# https://github.com/python/typeshed/commit/41bf6a19822d6694973449d795f8bfe1d50d5a03 +class WarningsRecorder(warnings.catch_warnings): # type: ignore """A context manager to record raised warnings. Adapted from `warnings.catch_warnings`.