diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 590cc9958..f516959bc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,7 +6,7 @@ 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 and removals/deprecations. +- [ ] Target the `features` branch for new features, improvements, and removals/deprecations. - [ ] Include documentation when adding new features. - [ ] Include new tests or update existing tests when applicable. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 13cdb8551..4d127d3c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,15 +42,10 @@ repos: hooks: - id: rst-backticks - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.711 + rev: v0.720 hooks: - id: mypy - name: mypy (src) - files: ^src/ - args: [] - - id: mypy - name: mypy (testing) - files: ^testing/ + files: ^(src/|testing/) args: [] - repo: local hooks: @@ -64,7 +59,7 @@ repos: name: changelog filenames language: fail entry: 'changelog files must be named ####.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst' - exclude: changelog/(\d+\.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst) + exclude: changelog/(\d+\.(feature|improvement|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst) files: ^changelog/ - id: py-deprecated name: py library is deprecated diff --git a/.travis.yml b/.travis.yml index af33d672e..5de40f3a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -72,8 +72,17 @@ jobs: - stage: deploy python: '3.6' - install: pip install -U setuptools setuptools_scm + install: pip install -U setuptools setuptools_scm tox script: skip + # token to upload github release notes: GH_RELEASE_NOTES_TOKEN + env: + - secure: "OjOeL7/0JUDkV00SsTs732e8vQjHynpbG9FKTNtZZJ+1Zn4Cib+hAlwmlBnvVukML0X60YpcfjnC4quDOIGLPsh5zeXnvJmYtAIIUNQXjWz8NhcGYrhyzuP1rqV22U68RTCdmOq3lMYU/W2acwHP7T49PwJtOiUM5kF120UAQ0Zi5EmkqkIvH8oM5mO9Dlver+/U7Htpz9rhKrHBXQNCMZI6yj2aUyukqB2PN2fjAlDbCF//+FmvYw9NjT4GeFOSkTCf4ER9yfqs7yglRfwiLtOCZ2qKQhWZNsSJDB89rxIRXWavJUjJKeY2EW2/NkomYJDpqJLIF4JeFRw/HhA47CYPeo6BJqyyNV+0CovL1frpWfi9UQw2cMbgFUkUIUk3F6DD59PHNIOX2R/HX56dQsw7WKl3QuHlCOkICXYg8F7Ta684IoKjeTX03/6QNOkURfDBwfGszY0FpbxrjCSWKom6RyZdyidnESaxv9RzjcIRZVh1rp8KMrwS1OrwRSdG0zjlsPr49hWMenN/8fKgcHTV4/r1Tj6mip0dorSRCrgUNIeRBKgmui6FS8642ab5JNKOxMteVPVR2sFuhjOQ0Jy+PmvceYY9ZMWc3+/B/KVh0dZ3hwvLGZep/vxDS2PwCA5/xw31714vT5LxidKo8yECjBynMU/wUTTS695D3NY=" + addons: + apt: + packages: + # required by publish_gh_release_notes + - pandoc + after_deploy: tox -e publish_gh_release_notes deploy: provider: pypi user: nicoddemus diff --git a/AUTHORS b/AUTHORS index 8e545e126..88bbfe352 100644 --- a/AUTHORS +++ b/AUTHORS @@ -71,6 +71,7 @@ Danielle Jenkins Dave Hunt David Díaz-Barquero David Mohr +David Paul Röthlisberger David Szotten David Vierra Daw-Ran Liou diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 390fe84ca..6c4479897 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,164 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.1.0 (2019-08-15) +========================= + +Removals +-------- + +- `#5180 `_: As per our policy, the following features have been deprecated in the 4.X series and are now + removed: + + * ``Request.getfuncargvalue``: use ``Request.getfixturevalue`` instead. + + * ``pytest.raises`` and ``pytest.warns`` no longer support strings as the second argument. + + * ``message`` parameter of ``pytest.raises``. + + * ``pytest.raises``, ``pytest.warns`` and ``ParameterSet.param`` now use native keyword-only + syntax. This might change the exception message from previous versions, but they still raise + ``TypeError`` on unknown keyword arguments as before. + + * ``pytest.config`` global variable. + + * ``tmpdir_factory.ensuretemp`` method. + + * ``pytest_logwarning`` hook. + + * ``RemovedInPytest4Warning`` warning type. + + * ``request`` is now a reserved name for fixtures. + + + For more information consult + `Deprecations and Removals `__ in the docs. + + +- `#5565 `_: Removed unused support code for `unittest2 `__. + + The ``unittest2`` backport module is no longer + necessary since Python 3.3+, and the small amount of code in pytest to support it also doesn't seem + to be used: after removed, all tests still pass unchanged. + + Although our policy is to introduce a deprecation period before removing any features or support + for third party libraries, because this code is apparently not used + at all (even if ``unittest2`` is used by a test suite executed by pytest), it was decided to + remove it in this release. + + If you experience a regression because of this, please + `file an issue `__. + + +- `#5615 `_: ``pytest.fail``, ``pytest.xfail`` and ``pytest.skip`` no longer support bytes for the message argument. + + This was supported for Python 2 where it was tempting to use ``"message"`` + instead of ``u"message"``. + + Python 3 code is unlikely to pass ``bytes`` to these functions. If you do, + please decode it to an ``str`` beforehand. + + + +Features +-------- + +- `#5564 `_: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. + + +- `#5576 `_: New `NUMBER `__ + option for doctests to ignore irrelevant differences in floating-point numbers. + Inspired by Sébastien Boisgérault's `numtest `__ + extension for doctest. + + + +Improvements +------------ + +- `#5471 `_: JUnit XML now includes a timestamp and hostname in the testsuite tag. + + +- `#5707 `_: Time taken to run the test suite now includes a human-readable representation when it takes over + 60 seconds, for example:: + + ===== 2 failed in 102.70s (0:01:42) ===== + + + +Bug Fixes +--------- + +- `#4344 `_: Fix RuntimeError/StopIteration when trying to collect package with "__init__.py" only. + + +- `#5115 `_: Warnings issued during ``pytest_configure`` are explicitly not treated as errors, even if configured as such, because it otherwise completely breaks pytest. + + +- `#5477 `_: The XML file produced by ``--junitxml`` now correctly contain a ```` root element. + + +- `#5523 `_: Fixed using multiple short options together in the command-line (for example ``-vs``) in Python 3.8+. + + +- `#5524 `_: Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only, + which could lead to pytest crashing when executed a second time with the ``--basetemp`` option. + + +- `#5537 `_: Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the + standard library on Python 3.8+. + + +- `#5578 `_: Improve type checking for some exception-raising functions (``pytest.xfail``, ``pytest.skip``, etc) + so they provide better error messages when users meant to use marks (for example ``@pytest.xfail`` + instead of ``@pytest.mark.xfail``). + + +- `#5606 `_: Fixed internal error when test functions were patched with objects that cannot be compared + for truth values against others, like ``numpy`` arrays. + + +- `#5634 `_: ``pytest.exit`` is now correctly handled in ``unittest`` cases. + This makes ``unittest`` cases handle ``quit`` from pytest's pdb correctly. + + +- `#5650 `_: Improved output when parsing an ini configuration file fails. + + +- `#5701 `_: Fix collection of ``staticmethod`` objects defined with ``functools.partial``. + + +- `#5734 `_: Skip async generator test functions, and update the warning message to refer to ``async def`` functions. + + + +Improved Documentation +---------------------- + +- `#5669 `_: Add docstring for ``Testdir.copy_example``. + + + +Trivial/Internal Changes +------------------------ + +- `#5095 `_: XML files of the ``xunit2`` family are now validated against the schema by pytest's own test suite + to avoid future regressions. + + +- `#5516 `_: Cache node splitting function which can improve collection performance in very large test suites. + + +- `#5603 `_: Simplified internal ``SafeRepr`` class and removed some dead code. + + +- `#5664 `_: When invoking pytest's own testsuite with ``PYTHONDONTWRITEBYTECODE=1``, + the ``test_xfail_handling`` test no longer fails. + + +- `#5684 `_: Replace manual handling of ``OSError.errno`` in the codebase by new ``OSError`` subclasses (``PermissionError``, ``FileNotFoundError``, etc.). + + pytest 5.0.1 (2019-07-04) ========================= diff --git a/changelog/4344.bugfix.rst b/changelog/4344.bugfix.rst deleted file mode 100644 index 644a6f030..000000000 --- a/changelog/4344.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix RuntimeError/StopIteration when trying to collect package with "__init__.py" only. diff --git a/changelog/5115.bugfix.rst b/changelog/5115.bugfix.rst deleted file mode 100644 index af75499a3..000000000 --- a/changelog/5115.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Warnings issued during ``pytest_configure`` are explicitly not treated as errors, even if configured as such, because it otherwise completely breaks pytest. diff --git a/changelog/5516.trivial.rst b/changelog/5516.trivial.rst deleted file mode 100644 index 2f6b4e35e..000000000 --- a/changelog/5516.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Cache node splitting function which can improve collection performance in very large test suites. diff --git a/changelog/5524.bugfix.rst b/changelog/5524.bugfix.rst deleted file mode 100644 index 96ebbd43e..000000000 --- a/changelog/5524.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only, -which could lead to pytest crashing when executed a second time with the ``--basetemp`` option. diff --git a/changelog/5578.bugfix.rst b/changelog/5578.bugfix.rst deleted file mode 100644 index 5f6c39185..000000000 --- a/changelog/5578.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Improve type checking for some exception-raising functions (``pytest.xfail``, ``pytest.skip``, etc) -so they provide better error messages when users meant to use marks (for example ``@pytest.xfail`` -instead of ``@pytest.mark.xfail``). diff --git a/changelog/5606.bugfix.rst b/changelog/5606.bugfix.rst deleted file mode 100644 index 82332ba99..000000000 --- a/changelog/5606.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed internal error when test functions were patched with objects that cannot be compared -for truth values against others, like ``numpy`` arrays. diff --git a/changelog/5634.bugfix.rst b/changelog/5634.bugfix.rst deleted file mode 100644 index a2a282f93..000000000 --- a/changelog/5634.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -``pytest.exit`` is now correctly handled in ``unittest`` cases. -This makes ``unittest`` cases handle ``quit`` from pytest's pdb correctly. diff --git a/changelog/5650.bugfix.rst b/changelog/5650.bugfix.rst deleted file mode 100644 index db57a40b9..000000000 --- a/changelog/5650.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Improved output when parsing an ini configuration file fails. diff --git a/changelog/5664.trivial.rst b/changelog/5664.trivial.rst deleted file mode 100644 index 3928454ef..000000000 --- a/changelog/5664.trivial.rst +++ /dev/null @@ -1,2 +0,0 @@ -When invoking pytest's own testsuite with ``PYTHONDONTWRITEBYTECODE=1``, -the ``test_xfail_handling`` test no longer fails. diff --git a/changelog/5669.doc.rst b/changelog/5669.doc.rst deleted file mode 100644 index 0ec9626ae..000000000 --- a/changelog/5669.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add docstring for ``Testdir.copy_example``. diff --git a/changelog/5684.trivial.rst b/changelog/5684.trivial.rst deleted file mode 100644 index 393fa3205..000000000 --- a/changelog/5684.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Replace manual handling of ``OSError.errno`` in the codebase by new ``OSError`` subclasses (``PermissionError``, ``FileNotFoundError``, etc.). diff --git a/changelog/5701.bugfix.rst b/changelog/5701.bugfix.rst deleted file mode 100644 index b654e7447..000000000 --- a/changelog/5701.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix collection of ``staticmethod`` objects defined with ``functools.partial``. diff --git a/changelog/5734.bugfix.rst b/changelog/5734.bugfix.rst deleted file mode 100644 index dc20e6b52..000000000 --- a/changelog/5734.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Skip async generator test functions, and update the warning message to refer to ``async def`` functions. diff --git a/changelog/README.rst b/changelog/README.rst index e471409b0..5c182758b 100644 --- a/changelog/README.rst +++ b/changelog/README.rst @@ -12,6 +12,7 @@ Each file should be named like ``..rst``, where ```` is an issue number, and ```` is one of: * ``feature``: new user facing features, like new command-line options and new behavior. +* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc). * ``bugfix``: fixes a reported bug. * ``doc``: documentation improvement, like rewording an entire session or adding missing docs. * ``deprecation``: feature deprecation. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index d372c92fa..7c6220b24 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.1.0 release-5.0.1 release-5.0.0 release-4.6.5 diff --git a/doc/en/announce/release-5.1.0.rst b/doc/en/announce/release-5.1.0.rst new file mode 100644 index 000000000..73e956d77 --- /dev/null +++ b/doc/en/announce/release-5.1.0.rst @@ -0,0 +1,56 @@ +pytest-5.1.0 +======================================= + +The pytest team is proud to announce the 5.1.0 release! + +pytest is a mature Python testing tool with more than a 2000 tests +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: + +* Albert Tugushev +* Alexey Zankevich +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* David Röthlisberger +* Florian Bruhin +* Ilya Stepin +* Jon Dufresne +* Kaiqi +* Max R +* Miro Hrončok +* Oliver Bestwalter +* Ran Benita +* Ronny Pfannschmidt +* Samuel Searles-Bryant +* Semen Zhydenko +* Steffen Schroeder +* Thomas Grainger +* Tim Hoffmann +* William Woodall +* Wojtek Erbetowski +* Xixi Zhao +* Yash Todi +* boris +* dmitry.dygalo +* helloocc +* martbln +* mei-li + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index bc7e75256..bd6e9b3b3 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -47,7 +47,7 @@ you will see the return value of the function call: E + where 3 = f() test_assert1.py:6: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + ============================ 1 failed in 0.05s ============================= ``pytest`` has support for showing the values of the most common subexpressions including calls, attributes, comparisons, and binary and unary @@ -208,7 +208,7 @@ if you run this module: E Use -v to get the full diff test_assert2.py:6: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + ============================ 1 failed in 0.05s ============================= Special comparisons are done for a number of cases: @@ -279,7 +279,7 @@ the conftest file: E vals: 1 != 2 test_foocompare.py:12: AssertionError - 1 failed in 0.12 seconds + 1 failed in 0.05s .. _assert-details: .. _`assert introspection`: diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 4309a16ea..4638cf784 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -160,7 +160,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a in python < 3.6 this is a pathlib2.Path - no tests ran in 0.12 seconds + no tests ran in 0.01s You can also interactively ask for help, e.g. by typing on the Python interactive prompt something like: diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 84b3fa009..c6b3e3c47 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -60,10 +60,10 @@ If you run this for the first time you will see two failures: @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed + test_50.py:7: Failed _______________________________ test_num[25] _______________________________ i = 25 @@ -71,11 +71,11 @@ If you run this for the first time you will see two failures: @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed - 2 failed, 48 passed in 0.12 seconds + test_50.py:7: Failed + 2 failed, 48 passed in 0.16s If you then run it with ``--lf``: @@ -99,10 +99,10 @@ If you then run it with ``--lf``: @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed + test_50.py:7: Failed _______________________________ test_num[25] _______________________________ i = 25 @@ -110,11 +110,11 @@ If you then run it with ``--lf``: @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed - ================= 2 failed, 48 deselected in 0.12 seconds ================== + test_50.py:7: Failed + ===================== 2 failed, 48 deselected in 0.07s ===================== You have run only the two failing tests from the last run, while the 48 passing tests have not been run ("deselected"). @@ -143,10 +143,10 @@ of ``FF`` and dots): @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed + test_50.py:7: Failed _______________________________ test_num[25] _______________________________ i = 25 @@ -154,11 +154,11 @@ of ``FF`` and dots): @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed - =================== 2 failed, 48 passed in 0.12 seconds ==================== + test_50.py:7: Failed + ======================= 2 failed, 48 passed in 0.15s ======================= .. _`config.cache`: @@ -227,10 +227,10 @@ If you run this command for the first time, you can see the print statement: > assert mydata == 23 E assert 42 == 23 - test_caching.py:17: AssertionError + test_caching.py:20: AssertionError -------------------------- Captured stdout setup --------------------------- running expensive computation... - 1 failed in 0.12 seconds + 1 failed in 0.05s If you run it a second time, the value will be retrieved from the cache and nothing will be printed: @@ -248,8 +248,8 @@ the cache and nothing will be printed: > assert mydata == 23 E assert 42 == 23 - test_caching.py:17: AssertionError - 1 failed in 0.12 seconds + test_caching.py:20: AssertionError + 1 failed in 0.05s See the :ref:`cache-api` for more details. @@ -283,7 +283,7 @@ You can always peek at the content of the cache using the example/value contains: 42 - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.00s =========================== ``--cache-show`` takes an optional argument to specify a glob pattern for filtering: @@ -300,7 +300,7 @@ filtering: example/value contains: 42 - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== Clearing Cache content ---------------------- diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 55714c25b..72bdea983 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -88,10 +88,10 @@ of the failing function and hide the other one: > assert False E assert False - test_module.py:9: AssertionError + test_module.py:12: AssertionError -------------------------- Captured stdout setup --------------------------- setting up - ==================== 1 failed, 1 passed in 0.12 seconds ==================== + ======================= 1 failed, 1 passed in 0.05s ======================== Accessing captured output from a test function --------------------------------------------------- diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 4c7170910..9d01e5f23 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -20,8 +20,8 @@ Below is a complete list of all pytest features which are considered deprecated. :ref:`standard warning filters `. -Removal of ``funcargnames`` alias for ``fixturenames`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``funcargnames`` alias for ``fixturenames`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 5.0 @@ -34,12 +34,47 @@ in places where we or plugin authors must distinguish between fixture names and names supplied by non-fixture things such as ``pytest.mark.parametrize``. +Result log (``--result-log``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 4.0 + +The ``--result-log`` option produces a stream of test reports which can be +analysed at runtime. It uses a custom format which requires users to implement their own +parser, but the team believes using a line-based format that can be parsed using standard +tools would provide a suitable and better alternative. + +The current plan is to provide an alternative in the pytest 5.0 series and remove the ``--result-log`` +option in pytest 6.0 after the new implementation proves satisfactory to all users and is deemed +stable. + +The actual alternative is still being discussed in issue `#4488 `__. + + +Removed Features +---------------- + +As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after +an appropriate period of deprecation has passed. + + +``pytest.config`` global +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 5.0 + +The ``pytest.config`` global object is deprecated. Instead use +``request.config`` (via the ``request`` fixture) or if you are a plugin author +use the ``pytest_configure(config)`` hook. Note that many hooks can also access +the ``config`` object indirectly, through ``session.config`` or ``item.config`` for example. + + .. _`raises message deprecated`: ``"message"`` parameter of ``pytest.raises`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 4.1 +.. versionremoved:: 5.0 It is a common mistake to think this parameter will match the exception message, while in fact it only serves to provide a custom message in case the ``pytest.raises`` check fails. To prevent @@ -70,22 +105,12 @@ If you still have concerns about this deprecation and future removal, please com `issue #3974 `__. -``pytest.config`` global -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 4.1 - -The ``pytest.config`` global object is deprecated. Instead use -``request.config`` (via the ``request`` fixture) or if you are a plugin author -use the ``pytest_configure(config)`` hook. Note that many hooks can also access -the ``config`` object indirectly, through ``session.config`` or ``item.config`` for example. - .. _raises-warns-exec: ``raises`` / ``warns`` with a string as the second argument ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 4.1 +.. versionremoved:: 5.0 Use the context manager form of these instead. When necessary, invoke ``exec`` directly. @@ -116,27 +141,6 @@ Becomes: -Result log (``--result-log``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 4.0 - -The ``--result-log`` option produces a stream of test reports which can be -analysed at runtime. It uses a custom format which requires users to implement their own -parser, but the team believes using a line-based format that can be parsed using standard -tools would provide a suitable and better alternative. - -The current plan is to provide an alternative in the pytest 5.0 series and remove the ``--result-log`` -option in pytest 6.0 after the new implementation proves satisfactory to all users and is deemed -stable. - -The actual alternative is still being discussed in issue `#4488 `__. - -Removed Features ----------------- - -As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after -an appropriate period of deprecation has passed. Using ``Class`` in custom Collectors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index ce23563ee..2718e1e63 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -36,7 +36,7 @@ then you can just invoke ``pytest`` directly: test_example.txt . [100%] - ========================= 1 passed in 0.12 seconds ========================= + ============================ 1 passed in 0.02s ============================= By default, pytest will collect ``test*.txt`` files looking for doctest directives, but you can pass additional globs using the ``--doctest-glob`` option (multi-allowed). @@ -66,7 +66,7 @@ and functions, including from test modules: mymodule.py . [ 50%] test_example.txt . [100%] - ========================= 2 passed in 0.12 seconds ========================= + ============================ 2 passed in 0.03s ============================= You can make these changes permanent in your project by putting them into a pytest.ini file like this: @@ -103,7 +103,7 @@ that will be used for those doctest files using the Using 'doctest' options ----------------------- -The standard ``doctest`` module provides some `options `__ +Python's standard ``doctest`` module provides some `options `__ to configure the strictness of doctest tests. In pytest, you can enable those flags using the configuration file. @@ -115,23 +115,50 @@ lengthy exception stack traces you can just write: [pytest] doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL -pytest also introduces new options to allow doctests to run in Python 2 and -Python 3 unchanged: - -* ``ALLOW_UNICODE``: when enabled, the ``u`` prefix is stripped from unicode - strings in expected doctest output. - -* ``ALLOW_BYTES``: when enabled, the ``b`` prefix is stripped from byte strings - in expected doctest output. - Alternatively, options can be enabled by an inline comment in the doc test itself: .. code-block:: rst - # content of example.rst - >>> get_unicode_greeting() # doctest: +ALLOW_UNICODE - 'Hello' + >>> something_that_raises() # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValueError: ... + +pytest also introduces new options: + +* ``ALLOW_UNICODE``: when enabled, the ``u`` prefix is stripped from unicode + strings in expected doctest output. This allows doctests to run in Python 2 + and Python 3 unchanged. + +* ``ALLOW_BYTES``: similarly, the ``b`` prefix is stripped from byte strings + in expected doctest output. + +* ``NUMBER``: when enabled, floating-point numbers only need to match as far as + the precision you have written in the expected doctest output. For example, + the following output would only need to match to 2 decimal places:: + + >>> math.pi + 3.14 + + If you wrote ``3.1416`` then the actual output would need to match to 4 + decimal places; and so on. + + This avoids false positives caused by limited floating-point precision, like + this:: + + Expected: + 0.233 + Got: + 0.23300000000000001 + + ``NUMBER`` also supports lists of floating-point numbers -- in fact, it + matches floating-point numbers appearing anywhere in the output, even inside + a string! This means that it may not be appropriate to enable globally in + ``doctest_optionflags`` in your configuration file. + + +Continue on failure +------------------- By default, pytest would report only the first failure for a given doctest. If you want to continue the test even when you have failures, do: diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 909f23a2e..38d02ed0c 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -52,7 +52,7 @@ You can then restrict a test run to only run tests marked with ``webtest``: test_server.py::test_send_http PASSED [100%] - ================== 1 passed, 3 deselected in 0.12 seconds ================== + ===================== 1 passed, 3 deselected in 0.01s ====================== Or the inverse, running all tests except the webtest ones: @@ -69,7 +69,7 @@ Or the inverse, running all tests except the webtest ones: test_server.py::test_another PASSED [ 66%] test_server.py::TestClass::test_method PASSED [100%] - ================== 3 passed, 1 deselected in 0.12 seconds ================== + ===================== 3 passed, 1 deselected in 0.02s ====================== Selecting tests based on their node ID -------------------------------------- @@ -89,7 +89,7 @@ tests based on their module, class, method, or function name: test_server.py::TestClass::test_method PASSED [100%] - ========================= 1 passed in 0.12 seconds ========================= + ============================ 1 passed in 0.01s ============================= You can also select on the class: @@ -104,7 +104,7 @@ You can also select on the class: test_server.py::TestClass::test_method PASSED [100%] - ========================= 1 passed in 0.12 seconds ========================= + ============================ 1 passed in 0.01s ============================= Or select multiple nodes: @@ -120,7 +120,7 @@ Or select multiple nodes: test_server.py::TestClass::test_method PASSED [ 50%] test_server.py::test_send_http PASSED [100%] - ========================= 2 passed in 0.12 seconds ========================= + ============================ 2 passed in 0.02s ============================= .. _node-id: @@ -159,7 +159,7 @@ select tests based on their names: test_server.py::test_send_http PASSED [100%] - ================== 1 passed, 3 deselected in 0.12 seconds ================== + ===================== 1 passed, 3 deselected in 0.01s ====================== And you can also run all tests except the ones that match the keyword: @@ -176,7 +176,7 @@ And you can also run all tests except the ones that match the keyword: test_server.py::test_another PASSED [ 66%] test_server.py::TestClass::test_method PASSED [100%] - ================== 3 passed, 1 deselected in 0.12 seconds ================== + ===================== 3 passed, 1 deselected in 0.02s ====================== Or to select "http" and "quick" tests: @@ -192,7 +192,7 @@ Or to select "http" and "quick" tests: test_server.py::test_send_http PASSED [ 50%] test_server.py::test_something_quick PASSED [100%] - ================== 2 passed, 2 deselected in 0.12 seconds ================== + ===================== 2 passed, 2 deselected in 0.02s ====================== .. note:: @@ -413,7 +413,7 @@ the test needs: test_someenv.py s [100%] - ======================== 1 skipped in 0.12 seconds ========================= + ============================ 1 skipped in 0.01s ============================ and here is one that specifies exactly the environment needed: @@ -428,7 +428,7 @@ and here is one that specifies exactly the environment needed: test_someenv.py . [100%] - ========================= 1 passed in 0.12 seconds ========================= + ============================ 1 passed in 0.01s ============================= The ``--markers`` option always gives you a list of available markers: @@ -499,7 +499,7 @@ The output is as follows: $ pytest -q -s Mark(name='my_marker', args=(,), kwargs={}) . - 1 passed in 0.12 seconds + 1 passed in 0.01s We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``. @@ -551,7 +551,7 @@ Let's run this without capturing output and see what we get: glob args=('class',) kwargs={'x': 2} glob args=('module',) kwargs={'x': 1} . - 1 passed in 0.12 seconds + 1 passed in 0.01s marking platform specific tests with pytest -------------------------------------------------------------- @@ -623,7 +623,7 @@ then you will see two tests skipped and two executed tests as expected: ========================= short test summary info ========================== SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux - =================== 2 passed, 2 skipped in 0.12 seconds ==================== + ======================= 2 passed, 2 skipped in 0.02s ======================= Note that if you specify a platform via the marker-command line option like this: @@ -638,7 +638,7 @@ Note that if you specify a platform via the marker-command line option like this test_plat.py . [100%] - ================== 1 passed, 3 deselected in 0.12 seconds ================== + ===================== 1 passed, 3 deselected in 0.01s ====================== then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests. @@ -711,7 +711,7 @@ We can now use the ``-m option`` to select one set: test_module.py:8: in test_interface_complex assert 0 E assert 0 - ================== 2 failed, 2 deselected in 0.12 seconds ================== + ===================== 2 failed, 2 deselected in 0.07s ====================== or to select both "event" and "interface" tests: @@ -739,4 +739,4 @@ or to select both "event" and "interface" tests: test_module.py:12: in test_event_simple assert 0 E assert 0 - ================== 3 failed, 1 deselected in 0.12 seconds ================== + ===================== 3 failed, 1 deselected in 0.07s ====================== diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index 75dc764e9..9c9e462f6 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -41,7 +41,7 @@ now execute the test specification: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.12 seconds ==================== + ======================= 1 failed, 1 passed in 0.06s ======================== .. regendoc:wipe @@ -77,7 +77,7 @@ consulted when reporting in ``verbose`` mode: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.12 seconds ==================== + ======================= 1 failed, 1 passed in 0.07s ======================== .. regendoc:wipe @@ -97,4 +97,4 @@ interesting to just look at the collection tree: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.05s =========================== diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 387e3f9de..08b414880 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -54,7 +54,7 @@ This means that we only run 2 tests if we do not pass ``--all``: $ pytest -q test_compute.py .. [100%] - 2 passed in 0.12 seconds + 2 passed in 0.01s We run only two computations, so we see two dots. let's run the full monty: @@ -72,8 +72,8 @@ let's run the full monty: > assert param1 < 4 E assert 4 < 4 - test_compute.py:3: AssertionError - 1 failed, 4 passed in 0.12 seconds + test_compute.py:4: AssertionError + 1 failed, 4 passed in 0.06s As expected when running the full range of ``param1`` values we'll get an error on the last one. @@ -172,7 +172,7 @@ objects, they are still using the default pytest representation: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.02s =========================== In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs together with the actual data, instead of listing them separately. @@ -229,7 +229,7 @@ this is a fully self-contained example which you can run with: test_scenarios.py .... [100%] - ========================= 4 passed in 0.12 seconds ========================= + ============================ 4 passed in 0.02s ============================= If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function: @@ -248,7 +248,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.02s =========================== Note that we told ``metafunc.parametrize()`` that your scenario values should be considered class-scoped. With pytest-2.3 this leads to a @@ -323,7 +323,7 @@ Let's first see how it looks like at collection time: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== And then when we run the test: @@ -342,8 +342,8 @@ And then when we run the test: > pytest.fail("deliberately failing for demo purposes") E Failed: deliberately failing for demo purposes - test_backends.py:6: Failed - 1 failed, 1 passed in 0.12 seconds + test_backends.py:8: Failed + 1 failed, 1 passed in 0.05s The first invocation with ``db == "DB1"`` passed while the second with ``db == "DB2"`` failed. Our ``db`` fixture function has instantiated each of the DB values during the setup phase while the ``pytest_generate_tests`` generated two according calls to the ``test_db_initialized`` during the collection phase. @@ -394,7 +394,7 @@ The result of this test will be successful: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== .. regendoc:wipe @@ -453,8 +453,8 @@ argument sets to use for each test function. Let's run it: > assert a == b E assert 1 == 2 - test_parametrize.py:18: AssertionError - 1 failed, 2 passed in 0.12 seconds + test_parametrize.py:21: AssertionError + 1 failed, 2 passed in 0.07s Indirect parametrization with multiple fixtures -------------------------------------------------------------- @@ -479,7 +479,7 @@ Running it results in some skips if we don't have all the python interpreters in ========================= 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.12 seconds + 3 passed, 24 skipped in 0.43s Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- @@ -547,8 +547,8 @@ If you run this with reporting for skips enabled: test_module.py .s [100%] ========================= short test summary info ========================== - SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2': No module named 'opt2' - =================== 1 passed, 1 skipped in 0.12 seconds ==================== + SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:13: could not import 'opt2': No module named 'opt2' + ======================= 1 passed, 1 skipped in 0.02s ======================= You'll see that we don't have an ``opt2`` module and thus the second test run of our ``test_func1`` was skipped. A few notes: @@ -610,7 +610,7 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker: test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%] test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%] - ============ 2 passed, 15 deselected, 1 xfailed in 0.12 seconds ============ + =============== 2 passed, 15 deselected, 1 xfailed in 0.23s ================ As the result: diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index cd4e34352..95faae34b 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -158,7 +158,7 @@ The test collection would look like this: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== You can check for multiple glob patterns by adding a space between the patterns: @@ -221,7 +221,7 @@ You can always peek at the collection tree without running tests like this: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== .. _customizing-test-collection: @@ -297,7 +297,7 @@ file will be left out: rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 0 items - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.04s =========================== It's also possible to ignore files based on Unix shell-style wildcards by adding patterns to ``collect_ignore_glob``. diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 05d06ecb6..1ad7c6966 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -119,7 +119,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: a = "1" * 100 + "a" + "2" * 100 b = "1" * 100 + "b" + "2" * 100 > assert a == b - E AssertionError: assert '111111111111...2222222222222' == '1111111111111...2222222222222' + E AssertionError: assert '111111111111...2222222222222' == '111111111111...2222222222222' E Skipping 90 identical leading characters in diff, use -v to show E Skipping 91 identical trailing characters in diff, use -v to show E - 1111111111a222222222 @@ -136,7 +136,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: a = "1\n" * 100 + "a" + "2\n" * 100 b = "1\n" * 100 + "b" + "2\n" * 100 > assert a == b - E AssertionError: assert '1\n1\n1\n1\n...n2\n2\n2\n2\n' == '1\n1\n1\n1\n1...n2\n2\n2\n2\n' + E AssertionError: assert '1\n1\n1\n1\n...n2\n2\n2\n2\n' == '1\n1\n1\n1\n...n2\n2\n2\n2\n' E Skipping 190 identical leading characters in diff, use -v to show E Skipping 191 identical trailing characters in diff, use -v to show E 1 @@ -235,7 +235,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_not_in_text_multiline(self): text = "some multiline\ntext\nwhich\nincludes foo\nand a\ntail" > assert "foo" not in text - E AssertionError: assert 'foo' not in 'some multiline\ntext\nw...ncludes foo\nand a\ntail' + E AssertionError: assert 'foo' not in 'some multil...nand a\ntail' E 'foo' is contained here: E some multiline E text @@ -267,7 +267,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_not_in_text_single_long(self): text = "head " * 50 + "foo " + "tail " * 20 > assert "foo" not in text - E AssertionError: assert 'foo' not in 'head head head head hea...ail tail tail tail tail ' + E AssertionError: assert 'foo' not in 'head head h...l tail tail ' E 'foo' is contained here: E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? +++ @@ -280,7 +280,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_not_in_text_single_long_term(self): text = "head " * 50 + "f" * 70 + "tail " * 20 > assert "f" * 70 not in text - E AssertionError: assert 'fffffffffff...ffffffffffff' not in 'head head he...l tail tail ' + E AssertionError: assert 'fffffffffff...ffffffffffff' not in 'head head h...l tail tail ' E 'ffffffffffffffffff...fffffffffffffffffff' is contained here: E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -301,7 +301,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: left = Foo(1, "b") right = Foo(1, "c") > assert left == right - E AssertionError: assert TestSpecialis...oo(a=1, b='b') == TestSpecialise...oo(a=1, b='c') + E AssertionError: assert TestSpecialis...oo(a=1, b='b') == TestSpecialis...oo(a=1, b='c') E Omitting 1 identical items, use -vv to show E Differing attributes: E b: 'b' != 'c' @@ -650,4 +650,4 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a failure_demo.py:282: AssertionError - ======================== 44 failed in 0.12 seconds ========================= + ============================ 44 failed in 0.82s ============================ diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index b4baa2b9b..ce2fbff54 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -65,7 +65,7 @@ Let's run this without supplying our new option: test_sample.py:6: AssertionError --------------------------- Captured stdout call --------------------------- first - 1 failed in 0.12 seconds + 1 failed in 0.06s And now with supplying a command line option: @@ -89,7 +89,7 @@ And now with supplying a command line option: test_sample.py:6: AssertionError --------------------------- Captured stdout call --------------------------- second - 1 failed in 0.12 seconds + 1 failed in 0.06s You can see that the command line option arrived in our test. This completes the basic pattern. However, one often rather wants to process @@ -132,7 +132,7 @@ directory with the above conftest.py: rootdir: $REGENDOC_TMPDIR collected 0 items - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== .. _`excontrolskip`: @@ -201,7 +201,7 @@ and when running it will see a skipped "slow" test: ========================= short test summary info ========================== SKIPPED [1] test_module.py:8: need --runslow option to run - =================== 1 passed, 1 skipped in 0.12 seconds ==================== + ======================= 1 passed, 1 skipped in 0.01s ======================= Or run it including the ``slow`` marked test: @@ -216,7 +216,7 @@ Or run it including the ``slow`` marked test: test_module.py .. [100%] - ========================= 2 passed in 0.12 seconds ========================= + ============================ 2 passed in 0.01s ============================= Writing well integrated assertion helpers -------------------------------------------------- @@ -261,7 +261,7 @@ Let's run our little function: E Failed: not configured: 42 test_checkconfig.py:11: Failed - 1 failed in 0.12 seconds + 1 failed in 0.05s If you only want to hide certain exceptions, you can set ``__tracebackhide__`` to a callable which gets the ``ExceptionInfo`` object. You can for example use @@ -358,7 +358,7 @@ which will add the string to the test header accordingly: rootdir: $REGENDOC_TMPDIR collected 0 items - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.00s =========================== .. regendoc:wipe @@ -388,7 +388,7 @@ which will add info only when run with "--v": rootdir: $REGENDOC_TMPDIR collecting ... collected 0 items - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.00s =========================== and nothing when run plainly: @@ -401,7 +401,7 @@ and nothing when run plainly: rootdir: $REGENDOC_TMPDIR collected 0 items - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.00s =========================== profiling test duration -------------------------- @@ -445,9 +445,9 @@ 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.25s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast - ========================= 3 passed in 0.12 seconds ========================= + ============================ 3 passed in 0.68s ============================= incremental testing - test steps --------------------------------------------------- @@ -531,7 +531,7 @@ If we run this: ========================= short test summary info ========================== XFAIL test_step.py::TestUserHandling::test_deletion reason: previous test failed (test_modification) - ============== 1 failed, 2 passed, 1 xfailed in 0.12 seconds =============== + ================== 1 failed, 2 passed, 1 xfailed in 0.07s ================== We'll see that ``test_deletion`` was not executed because ``test_modification`` failed. It is reported as an "expected failure". @@ -644,7 +644,7 @@ We can run this: E assert 0 a/test_db2.py:2: AssertionError - ========== 3 failed, 2 passed, 1 xfailed, 1 error in 0.12 seconds ========== + ============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.10s ============== The two test modules in the ``a`` directory see the same ``db`` fixture instance while the one test in the sister-directory ``b`` doesn't see it. We could of course @@ -733,7 +733,7 @@ and run them: E assert 0 test_module.py:6: AssertionError - ========================= 2 failed in 0.12 seconds ========================= + ============================ 2 failed in 0.07s ============================= you will have a "failures" file which contains the failing test ids: @@ -848,7 +848,7 @@ and run it: E assert 0 test_module.py:19: AssertionError - ==================== 2 failed, 1 error in 0.12 seconds ===================== + ======================== 2 failed, 1 error in 0.07s ======================== You'll see that the fixture finalizers could use the precise reporting information. diff --git a/doc/en/example/special.rst b/doc/en/example/special.rst index 5161c43ab..5142d08b9 100644 --- a/doc/en/example/special.rst +++ b/doc/en/example/special.rst @@ -81,4 +81,4 @@ If you run this without output capturing: .test other .test_unit1 method called . - 4 passed in 0.12 seconds + 4 passed in 0.02s diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index b494ec0fe..b8469ad46 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -92,11 +92,11 @@ marked ``smtp_connection`` fixture function. Running the test looks like this: def test_ehlo(smtp_connection): response, msg = smtp_connection.ehlo() assert response == 250 - > assert 0 # for demo purposes + > assert 0 # for demo purposes E assert 0 - test_smtpsimple.py:11: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + test_smtpsimple.py:14: AssertionError + ============================ 1 failed in 0.57s ============================= In the failure traceback we see that the test function was called with a ``smtp_connection`` argument, the ``smtplib.SMTP()`` instance created by the fixture @@ -246,7 +246,7 @@ inspect what is going on and can now run the tests: > assert 0 # for demo purposes E assert 0 - test_module.py:6: AssertionError + test_module.py:7: AssertionError ________________________________ test_noop _________________________________ smtp_connection = @@ -257,8 +257,8 @@ inspect what is going on and can now run the tests: > assert 0 # for demo purposes E assert 0 - test_module.py:11: AssertionError - ========================= 2 failed in 0.12 seconds ========================= + test_module.py:13: AssertionError + ============================ 2 failed in 0.76s ============================= You see the two ``assert 0`` failing and more importantly you can also see that the same (module-scoped) ``smtp_connection`` object was passed into the @@ -361,7 +361,7 @@ Let's execute it: $ pytest -s -q --tb=no FFteardown smtp - 2 failed in 0.12 seconds + 2 failed in 0.76s We see that the ``smtp_connection`` instance is finalized after the two tests finished execution. Note that if we decorated our fixture @@ -515,7 +515,7 @@ again, nothing much has changed: $ pytest -s -q --tb=no FFfinalizing (smtp.gmail.com) - 2 failed in 0.12 seconds + 2 failed in 0.76s Let's quickly create another test module that actually sets the server URL in its module namespace: @@ -538,7 +538,7 @@ Running it: F [100%] ================================= FAILURES ================================= ______________________________ test_showhelo _______________________________ - test_anothersmtp.py:5: in test_showhelo + test_anothersmtp.py:6: in test_showhelo assert 0, smtp_connection.helo() E AssertionError: (250, b'mail.python.org') E assert 0 @@ -654,7 +654,7 @@ So let's just do another run: > assert 0 # for demo purposes E assert 0 - test_module.py:6: AssertionError + test_module.py:7: AssertionError ________________________ test_noop[smtp.gmail.com] _________________________ smtp_connection = @@ -665,7 +665,7 @@ So let's just do another run: > assert 0 # for demo purposes E assert 0 - test_module.py:11: AssertionError + test_module.py:13: AssertionError ________________________ test_ehlo[mail.python.org] ________________________ smtp_connection = @@ -676,7 +676,7 @@ So let's just do another run: > assert b"smtp.gmail.com" in msg E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8\nCHUNKING' - test_module.py:5: AssertionError + test_module.py:6: AssertionError -------------------------- Captured stdout setup --------------------------- finalizing ________________________ test_noop[mail.python.org] ________________________ @@ -689,10 +689,10 @@ So let's just do another run: > assert 0 # for demo purposes E assert 0 - test_module.py:11: AssertionError + test_module.py:13: AssertionError ------------------------- Captured stdout teardown ------------------------- finalizing - 4 failed in 0.12 seconds + 4 failed in 1.77s We see that our two test functions each ran twice, against the different ``smtp_connection`` instances. Note also, that with the ``mail.python.org`` @@ -771,7 +771,7 @@ Running the above tests results in the following test IDs being used: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.04s =========================== .. _`fixture-parametrize-marks`: @@ -812,7 +812,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``: test_fixture_marks.py::test_data[1] PASSED [ 66%] test_fixture_marks.py::test_data[2] SKIPPED [100%] - =================== 2 passed, 1 skipped in 0.12 seconds ==================== + ======================= 2 passed, 1 skipped in 0.01s ======================= .. _`interdependent fixtures`: @@ -861,7 +861,7 @@ Here we declare an ``app`` fixture which receives the previously defined test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%] test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%] - ========================= 2 passed in 0.12 seconds ========================= + ============================ 2 passed in 0.79s ============================= Due to the parametrization of ``smtp_connection``, the test will run twice with two different ``App`` instances and respective smtp servers. There is no @@ -971,7 +971,7 @@ Let's run the tests in verbose mode and with looking at the print-output: TEARDOWN modarg mod2 - ========================= 8 passed in 0.12 seconds ========================= + ============================ 8 passed in 0.02s ============================= You can see that the parametrized module-scoped ``modarg`` resource caused an ordering of test execution that lead to the fewest possible "active" resources. @@ -1043,7 +1043,7 @@ to verify our fixture is activated and the tests pass: $ pytest -q .. [100%] - 2 passed in 0.12 seconds + 2 passed in 0.02s You can specify multiple fixtures like this: @@ -1151,7 +1151,7 @@ If we run it, we get two passing tests: $ pytest -q .. [100%] - 2 passed in 0.12 seconds + 2 passed in 0.02s Here is how autouse fixtures work in other scopes: diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index f1c28769f..bf1f6ac3d 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.x/site-packages/pytest.py + This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py .. _`simpletest`: @@ -68,8 +68,8 @@ That’s it. You can now execute the test function: E assert 4 == 5 E + where 4 = func(3) - test_sample.py:5: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + test_sample.py:6: AssertionError + ============================ 1 failed in 0.05s ============================= This test returns a failure report because ``func(3)`` does not return ``5``. @@ -108,7 +108,7 @@ Execute the test function with “quiet” reporting mode: $ pytest -q test_sysexit.py . [100%] - 1 passed in 0.12 seconds + 1 passed in 0.01s Group multiple tests in a class -------------------------------------------------------------- @@ -140,12 +140,12 @@ Once you develop multiple tests, you may want to group them into a class. pytest def test_two(self): x = "hello" - > assert hasattr(x, 'check') + > assert hasattr(x, "check") E AssertionError: assert False E + where False = hasattr('hello', 'check') test_class.py:8: AssertionError - 1 failed, 1 passed in 0.12 seconds + 1 failed, 1 passed in 0.05s The first test passed and the second failed. You can easily see the intermediate values in the assertion to help you understand the reason for the failure. @@ -180,7 +180,7 @@ List the name ``tmpdir`` in the test function signature and ``pytest`` will look test_tmpdir.py:3: AssertionError --------------------------- Captured stdout call --------------------------- PYTEST_TMPDIR/test_needsfiles0 - 1 failed in 0.12 seconds + 1 failed in 0.05s More info on tmpdir handling is available at :ref:`Temporary directories and files `. diff --git a/doc/en/index.rst b/doc/en/index.rst index 6c7c84865..8b8e8b337 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -44,7 +44,7 @@ To execute it: E + where 4 = inc(3) test_sample.py:6: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + ============================ 1 failed in 0.06s ============================= Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See :ref:`Getting Started ` for more examples. diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 2e2d846ea..79716b379 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -75,7 +75,7 @@ them in turn: E + where 54 = eval('6*9') test_expectation.py:6: AssertionError - ==================== 1 failed, 2 passed in 0.12 seconds ==================== + ======================= 1 failed, 2 passed in 0.05s ======================== .. note:: @@ -128,7 +128,7 @@ Let's run this: test_expectation.py ..x [100%] - =================== 2 passed, 1 xfailed in 0.12 seconds ==================== + ======================= 2 passed, 1 xfailed in 0.06s ======================= The one parameter set which caused a failure previously now shows up as an "xfailed (expected to fail)" test. @@ -205,7 +205,7 @@ If we now pass two stringinput values, our test will run twice: $ pytest -q --stringinput="hello" --stringinput="world" test_strings.py .. [100%] - 2 passed in 0.12 seconds + 2 passed in 0.01s Let's also run with a stringinput that will lead to a failing test: @@ -225,7 +225,7 @@ Let's also run with a stringinput that will lead to a failing test: E + where = '!'.isalpha test_strings.py:4: AssertionError - 1 failed in 0.12 seconds + 1 failed in 0.05s As expected our test function fails. @@ -239,7 +239,7 @@ list: s [100%] ========================= short test summary info ========================== SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:2 - 1 skipped in 0.12 seconds + 1 skipped in 0.01s Note that when calling ``metafunc.parametrize`` multiple times with different parameter sets, all parameter names across those sets cannot be duplicated, otherwise an error will be raised. diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 57f565472..7be1eb364 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -180,7 +180,7 @@ Skipping on a missing import dependency ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can skip tests on a missing import by using :ref:`pytest.importorskip ref` -at module level or within a test or test setup function. +at module level, within a test, or test setup function. .. code-block:: python @@ -371,7 +371,7 @@ Running it with the report-on-xfail option gives this output: XFAIL xfail_demo.py::test_hello6 reason: reason XFAIL xfail_demo.py::test_hello7 - ======================== 7 xfailed in 0.12 seconds ========================= + ============================ 7 xfailed in 0.17s ============================ .. _`skip/xfail with parametrize`: diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 1b565cee8..4084fc015 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -64,7 +64,7 @@ Running this would result in a passed test except for the last E assert 0 test_tmp_path.py:13: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + ============================ 1 failed in 0.06s ============================= .. _`tmp_path_factory example`: @@ -132,8 +132,8 @@ Running this would result in a passed test except for the last > assert 0 E assert 0 - test_tmpdir.py:7: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + test_tmpdir.py:9: AssertionError + ============================ 1 failed in 0.05s ============================= .. _`tmpdir factory example`: diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index 18b6a721b..b1d58071a 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -151,22 +151,22 @@ the ``self.db`` values in the traceback: def test_method1(self): assert hasattr(self, "db") - > assert 0, self.db # fail for demo purposes + > assert 0, self.db # fail for demo purposes E AssertionError: .DummyDB object at 0xdeadbeef> E assert 0 - test_unittest_db.py:9: AssertionError + test_unittest_db.py:10: AssertionError ___________________________ MyTest.test_method2 ____________________________ self = def test_method2(self): - > assert 0, self.db # fail for demo purposes + > assert 0, self.db # fail for demo purposes E AssertionError: .DummyDB object at 0xdeadbeef> E assert 0 - test_unittest_db.py:12: AssertionError - ========================= 2 failed in 0.12 seconds ========================= + test_unittest_db.py:13: AssertionError + ============================ 2 failed in 0.07s ============================= This default pytest traceback shows that the two test methods share the same ``self.db`` instance which was our intention @@ -219,7 +219,7 @@ Running this test module ...: $ pytest -q test_unittest_cleandir.py . [100%] - 1 passed in 0.12 seconds + 1 passed in 0.02s ... gives us one passed test because the ``initdir`` fixture function was executed ahead of the ``test_method``. diff --git a/doc/en/usage.rst b/doc/en/usage.rst index d5ff8a984..0ad70ff27 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -247,7 +247,7 @@ Example: XPASS test_example.py::test_xpass always xfail ERROR test_example.py::test_error - assert 0 FAILED test_example.py::test_fail - assert 0 - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.08s === The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes". @@ -297,7 +297,7 @@ More than one character can be used, so for example to only see failed and skipp ========================= short test summary info ========================== FAILED test_example.py::test_fail - assert 0 SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.08s === Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had captured output: @@ -336,7 +336,7 @@ captured output: ok ========================= short test summary info ========================== PASSED test_example.py::test_ok - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.08s === .. _pdb-option: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 398380ea5..e54d9f027 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.12 seconds =================== + ====================== 1 passed, 1 warnings in 0.01s ======================= The ``-W`` flag can be passed to control which warnings will be displayed or even turn them into errors: @@ -64,7 +64,7 @@ them into errors: E UserWarning: api v1, should use functions from v2 test_show_warnings.py:5: UserWarning - 1 failed in 0.12 seconds + 1 failed in 0.05s The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option. For example, the configuration below will ignore all user warnings, but will transform @@ -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.12 seconds + 1 warnings in 0.01s These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings. @@ -433,5 +433,3 @@ The following warning types are used by pytest and are part of the public API: .. autoclass:: pytest.PytestUnhandledCoroutineWarning .. autoclass:: pytest.PytestUnknownMarkWarning - -.. autoclass:: pytest.RemovedInPytest4Warning diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 7975f5826..9f3d3115e 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.12 seconds =================== + ====================== 2 passed, 1 warnings in 0.28s ======================= For more information about the result object that ``runpytest()`` returns, and the methods that it provides please check out the :py:class:`RunResult diff --git a/pyproject.toml b/pyproject.toml index 2a4cd65c1..552cdfa0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,11 @@ template = "changelog/_template.rst" name = "Features" showcontent = true + [[tool.towncrier.type]] + directory = "improvement" + name = "Improvements" + showcontent = true + [[tool.towncrier.type]] directory = "bugfix" name = "Bug Fixes" diff --git a/scripts/publish_gh_release_notes.py b/scripts/publish_gh_release_notes.py new file mode 100644 index 000000000..23f7b40ad --- /dev/null +++ b/scripts/publish_gh_release_notes.py @@ -0,0 +1,95 @@ +""" +Script used to publish GitHub release notes extracted from CHANGELOG.rst. + +This script is meant to be executed after a successful deployment in Travis. + +Uses the following environment variables: + +* GIT_TAG: the name of the tag of the current commit. +* GH_RELEASE_NOTES_TOKEN: a personal access token with 'repo' permissions. It should be encrypted using: + + $travis encrypt GH_RELEASE_NOTES_TOKEN= -r pytest-dev/pytest + + And the contents pasted in the ``deploy.env.secure`` section in the ``travis.yml`` file. + +The script also requires ``pandoc`` to be previously installed in the system. + +Requires Python3.6+. +""" +import os +import re +import sys +from pathlib import Path + +import github3 +import pypandoc + + +def publish_github_release(slug, token, tag_name, body): + github = github3.login(token=token) + owner, repo = slug.split("/") + repo = github.repository(owner, repo) + return repo.create_release(tag_name=tag_name, body=body) + + +def parse_changelog(tag_name): + p = Path(__file__).parent.parent / "CHANGELOG.rst" + changelog_lines = p.read_text(encoding="UTF-8").splitlines() + + title_regex = re.compile(r"pytest (\d\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)") + consuming_version = False + version_lines = [] + for line in changelog_lines: + m = title_regex.match(line) + if m: + # found the version we want: start to consume lines until we find the next version title + if m.group(1) == tag_name: + consuming_version = True + # found a new version title while parsing the version we want: break out + elif consuming_version: + break + if consuming_version: + version_lines.append(line) + + return "\n".join(version_lines) + + +def convert_rst_to_md(text): + return pypandoc.convert_text(text, "md", format="rst") + + +def main(argv): + if len(argv) > 1: + tag_name = argv[1] + else: + tag_name = os.environ.get("TRAVIS_TAG") + if not tag_name: + print("tag_name not given and $TRAVIS_TAG not set", file=sys.stderr) + return 1 + + token = os.environ.get("GH_RELEASE_NOTES_TOKEN") + if not token: + print("GH_RELEASE_NOTES_TOKEN not set", file=sys.stderr) + return 1 + + slug = os.environ.get("TRAVIS_REPO_SLUG") + if not slug: + print("TRAVIS_REPO_SLUG not set", file=sys.stderr) + return 1 + + rst_body = parse_changelog(tag_name) + md_body = convert_rst_to_md(rst_body) + if not publish_github_release(slug, token, tag_name, md_body): + print("Could not publish release notes:", file=sys.stderr) + print(md_body, file=sys.stderr) + return 5 + + print() + print(f"Release notes for {tag_name} published successfully:") + print(f"https://github.com/{slug}/releases/tag/{tag_name}") + print() + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/setup.py b/setup.py index 4c87c6429..adbafb557 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ INSTALL_REQUIRES = [ 'pathlib2>=2.2.0;python_version<"3.6"', 'colorama;sys_platform=="win32"', "pluggy>=0.12,<1.0", - "importlib-metadata>=0.12", + 'importlib-metadata>=0.12;python_version<"3.8"', "wcwidth", ] @@ -21,7 +21,6 @@ def main(): use_scm_version={"write_to": "src/_pytest/_version.py"}, setup_requires=["setuptools-scm", "setuptools>=40.0"], package_dir={"": "src"}, - # fmt: off extras_require={ "testing": [ "argcomplete", @@ -29,9 +28,9 @@ def main(): "mock", "nose", "requests", - ], + "xmlschema", + ] }, - # fmt: on install_requires=INSTALL_REQUIRES, ) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index d63c010e4..7d72234e7 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -5,6 +5,13 @@ import traceback from inspect import CO_VARARGS from inspect import CO_VARKEYWORDS from traceback import format_exception_only +from types import TracebackType +from typing import Generic +from typing import Optional +from typing import Pattern +from typing import Tuple +from typing import TypeVar +from typing import Union from weakref import ref import attr @@ -15,6 +22,9 @@ import _pytest from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr +if False: # TYPE_CHECKING + from typing import Type + class Code: """ wrapper around Python code objects """ @@ -371,20 +381,52 @@ co_equal = compile( ) +_E = TypeVar("_E", bound=BaseException) + + @attr.s(repr=False) -class ExceptionInfo: +class ExceptionInfo(Generic[_E]): """ wraps sys.exc_info() objects and offers help for navigating the traceback. """ _assert_start_repr = "AssertionError('assert " - _excinfo = attr.ib() - _striptext = attr.ib(default="") - _traceback = attr.ib(default=None) + _excinfo = attr.ib(type=Optional[Tuple["Type[_E]", "_E", TracebackType]]) + _striptext = attr.ib(type=str, default="") + _traceback = attr.ib(type=Optional[Traceback], default=None) @classmethod - def from_current(cls, exprinfo=None): + def from_exc_info( + cls, + exc_info: Tuple["Type[_E]", "_E", TracebackType], + exprinfo: Optional[str] = None, + ) -> "ExceptionInfo[_E]": + """returns an ExceptionInfo for an existing exc_info tuple. + + .. warning:: + + Experimental API + + + :param exprinfo: a text string helping to determine if we should + strip ``AssertionError`` from the output, defaults + to the exception message/``__str__()`` + """ + _striptext = "" + if exprinfo is None and isinstance(exc_info[1], AssertionError): + exprinfo = getattr(exc_info[1], "msg", None) + if exprinfo is None: + exprinfo = saferepr(exc_info[1]) + if exprinfo and exprinfo.startswith(cls._assert_start_repr): + _striptext = "AssertionError: " + + return cls(exc_info, _striptext) + + @classmethod + def from_current( + cls, exprinfo: Optional[str] = None + ) -> "ExceptionInfo[BaseException]": """returns an ExceptionInfo matching the current traceback .. warning:: @@ -398,59 +440,71 @@ class ExceptionInfo: """ tup = sys.exc_info() assert tup[0] is not None, "no current exception" - _striptext = "" - if exprinfo is None and isinstance(tup[1], AssertionError): - exprinfo = getattr(tup[1], "msg", None) - if exprinfo is None: - exprinfo = saferepr(tup[1]) - if exprinfo and exprinfo.startswith(cls._assert_start_repr): - _striptext = "AssertionError: " - - return cls(tup, _striptext) + 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) @classmethod - def for_later(cls): + def for_later(cls) -> "ExceptionInfo[_E]": """return an unfilled ExceptionInfo """ return cls(None) + def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None: + """fill an unfilled ExceptionInfo created with for_later()""" + assert self._excinfo is None, "ExceptionInfo was already filled" + self._excinfo = exc_info + @property - def type(self): + def type(self) -> "Type[_E]": """the exception class""" + assert ( + self._excinfo is not None + ), ".type can only be used after the context manager exits" return self._excinfo[0] @property - def value(self): + def value(self) -> _E: """the exception value""" + assert ( + self._excinfo is not None + ), ".value can only be used after the context manager exits" return self._excinfo[1] @property - def tb(self): + def tb(self) -> TracebackType: """the exception raw traceback""" + assert ( + self._excinfo is not None + ), ".tb can only be used after the context manager exits" return self._excinfo[2] @property - def typename(self): + def typename(self) -> str: """the type name of the exception""" + assert ( + self._excinfo is not None + ), ".typename can only be used after the context manager exits" return self.type.__name__ @property - def traceback(self): + def traceback(self) -> Traceback: """the traceback""" if self._traceback is None: self._traceback = Traceback(self.tb, excinfo=ref(self)) return self._traceback @traceback.setter - def traceback(self, value): + def traceback(self, value: Traceback) -> None: self._traceback = value - def __repr__(self): + def __repr__(self) -> str: if self._excinfo is None: return "" return "" % (self.typename, len(self.traceback)) - def exconly(self, tryshort=False): + def exconly(self, tryshort: bool = False) -> str: """ return the exception as a string when 'tryshort' resolves to True, and the exception is a @@ -466,11 +520,13 @@ class ExceptionInfo: text = text[len(self._striptext) :] return text - def errisinstance(self, exc): + def errisinstance( + self, exc: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]] + ) -> bool: """ return True if the exception is an instance of exc """ return isinstance(self.value, exc) - def _getreprcrash(self): + def _getreprcrash(self) -> "ReprFileLocation": exconly = self.exconly(tryshort=True) entry = self.traceback.getcrashentry() path, lineno = entry.frame.code.raw.co_filename, entry.lineno @@ -478,13 +534,13 @@ class ExceptionInfo: def getrepr( self, - showlocals=False, - style="long", - abspath=False, - tbfilter=True, - funcargs=False, - truncate_locals=True, - chain=True, + showlocals: bool = False, + style: str = "long", + abspath: bool = False, + tbfilter: bool = True, + funcargs: bool = False, + truncate_locals: bool = True, + chain: bool = True, ): """ Return str()able representation of this exception info. @@ -535,7 +591,7 @@ class ExceptionInfo: ) return fmt.repr_excinfo(self) - def match(self, regexp): + def match(self, regexp: Union[str, Pattern]) -> bool: """ Check whether the regular expression 'regexp' is found in the string representation of the exception using ``re.search``. If it matches diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 74f75124f..7704421a2 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -2,19 +2,23 @@ import pprint import reprlib -def _call_and_format_exception(call, x, *args): +def _format_repr_exception(exc, obj): + exc_name = type(exc).__name__ try: - # Try the vanilla repr and make sure that the result is a string - return call(x, *args) - except Exception as exc: - exc_name = type(exc).__name__ - try: - exc_info = str(exc) - except Exception: - exc_info = "unknown" - return '<[{}("{}") raised in repr()] {} object at 0x{:x}>'.format( - exc_name, exc_info, x.__class__.__name__, id(x) - ) + exc_info = str(exc) + except Exception: + exc_info = "unknown" + return '<[{}("{}") raised in repr()] {} object at 0x{:x}>'.format( + exc_name, exc_info, obj.__class__.__name__, id(obj) + ) + + +def _ellipsize(s, maxsize): + if len(s) > maxsize: + i = max(0, (maxsize - 3) // 2) + j = max(0, maxsize - 3 - i) + return s[:i] + "..." + s[len(s) - j :] + return s class SafeRepr(reprlib.Repr): @@ -22,37 +26,24 @@ class SafeRepr(reprlib.Repr): and includes information on exceptions raised during the call. """ + def __init__(self, maxsize): + super().__init__() + self.maxstring = maxsize + self.maxsize = maxsize + def repr(self, x): - return self._callhelper(reprlib.Repr.repr, self, x) - - def repr_unicode(self, x, level): - # Strictly speaking wrong on narrow builds - def repr(u): - if "'" not in u: - return "'%s'" % u - elif '"' not in u: - return '"%s"' % u - else: - return "'%s'" % u.replace("'", r"\'") - - s = repr(x[: self.maxstring]) - if len(s) > self.maxstring: - i = max(0, (self.maxstring - 3) // 2) - j = max(0, self.maxstring - 3 - i) - s = repr(x[:i] + x[len(x) - j :]) - s = s[:i] + "..." + s[len(s) - j :] - return s + try: + s = super().repr(x) + except Exception as exc: + s = _format_repr_exception(exc, x) + return _ellipsize(s, self.maxsize) def repr_instance(self, x, level): - return self._callhelper(repr, x) - - def _callhelper(self, call, x, *args): - s = _call_and_format_exception(call, x, *args) - if len(s) > self.maxsize: - i = max(0, (self.maxsize - 3) // 2) - j = max(0, self.maxsize - 3 - i) - s = s[:i] + "..." + s[len(s) - j :] - return s + try: + s = repr(x) + except Exception as exc: + s = _format_repr_exception(exc, x) + return _ellipsize(s, self.maxsize) def safeformat(obj): @@ -60,7 +51,10 @@ def safeformat(obj): Failing __repr__ functions of user instances will be represented with a short exception info. """ - return _call_and_format_exception(pprint.pformat, obj) + try: + return pprint.pformat(obj) + except Exception as exc: + return _format_repr_exception(exc, obj) def saferepr(obj, maxsize=240): @@ -70,9 +64,4 @@ def saferepr(obj, maxsize=240): care to never raise exceptions itself. This function is a wrapper around the Repr/reprlib functionality of the standard 2.6 lib. """ - # review exception handling - srepr = SafeRepr() - srepr.maxstring = maxsize - srepr.maxsize = maxsize - srepr.maxother = 160 - return srepr.repr(obj) + return SafeRepr(maxsize).repr(obj) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 3ef92704b..0567e8fb8 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -33,6 +33,9 @@ PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version) PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT +AST_IS = ast.Is() +AST_NONE = ast.NameConstant(None) + class AssertionRewritingHook: """PEP302/PEP451 import hook which rewrites asserts.""" @@ -854,10 +857,7 @@ class AssertionRewriter(ast.NodeVisitor): internally already. See issue #3191 for more details. """ - - # Using parse because it is different between py2 and py3. - AST_NONE = ast.parse("None").body[0].value - val_is_none = ast.Compare(node, [ast.Is()], [AST_NONE]) + val_is_none = ast.Compare(node, [AST_IS], [AST_NONE]) send_warning = ast.parse( """\ from _pytest.warning_types import PytestAssertRewriteWarning diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 106c44a8a..732194ec2 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -119,9 +119,9 @@ def isiterable(obj): def assertrepr_compare(config, op, left, right): """Return specialised explanations for some operators/operands""" - width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op - left_repr = saferepr(left, maxsize=int(width // 2)) - right_repr = saferepr(right, maxsize=width - len(left_repr)) + maxsize = (80 - 15 - len(op) - 2) // 2 # 15 chars indentation, 1 space around op + left_repr = saferepr(left, maxsize=maxsize) + right_repr = saferepr(right, maxsize=maxsize) summary = "{} {} {}".format(left_repr, op, right_repr) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index e45d3b7c8..2d11231a4 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -26,6 +26,12 @@ MODULE_NOT_FOUND_ERROR = ( ) +if sys.version_info >= (3, 8): + from importlib import metadata as importlib_metadata # noqa +else: + import importlib_metadata # noqa + + def _format_args(func): return str(signature(func)) @@ -52,11 +58,11 @@ def iscoroutinefunction(func): return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False) -def getlocation(function, curdir): +def getlocation(function, curdir=None): function = get_real_func(function) fn = py.path.local(inspect.getfile(function)) lineno = function.__code__.co_firstlineno - if fn.relto(curdir): + if curdir is not None and fn.relto(curdir): fn = fn.relto(curdir) return "%s:%d" % (fn, lineno + 1) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 1ddf78829..b861563e9 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -8,8 +8,9 @@ import sys import types import warnings from functools import lru_cache +from pathlib import Path -import importlib_metadata +import attr import py from packaging.version import Version from pluggy import HookimplMarker @@ -18,6 +19,7 @@ from pluggy import PluginManager import _pytest._code import _pytest.assertion +import _pytest.deprecated import _pytest.hookspec # the extension point definitions from .exceptions import PrintHelp from .exceptions import UsageError @@ -25,6 +27,7 @@ from .findpaths import determine_setup from .findpaths import exists from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback +from _pytest.compat import importlib_metadata from _pytest.outcomes import fail from _pytest.outcomes import Skipped from _pytest.warning_types import PytestConfigWarning @@ -147,10 +150,15 @@ builtin_plugins = set(default_plugins) builtin_plugins.add("pytester") -def get_config(args=None): +def get_config(args=None, plugins=None): # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() - config = Config(pluginmanager) + config = Config( + pluginmanager, + invocation_params=Config.InvocationParams( + args=args, plugins=plugins, dir=Path().resolve() + ), + ) if args is not None: # Handle any "-p no:plugin" args. @@ -183,7 +191,7 @@ def _prepareconfig(args=None, plugins=None): msg = "`args` parameter expected to be a list or tuple of strings, got: {!r} (type: {})" raise TypeError(msg.format(args, type(args))) - config = get_config(args) + config = get_config(args, plugins) pluginmanager = config.pluginmanager try: if plugins: @@ -204,6 +212,19 @@ def _prepareconfig(args=None, plugins=None): raise +def _fail_on_non_top_pytest_plugins(conftestpath, confcutdir): + msg = ( + "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n" + "It affects the entire test suite instead of just below the conftest as expected.\n" + " {}\n" + "Please move it to a top level conftest file at the rootdir:\n" + " {}\n" + "For more information, visit:\n" + " https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" + ) + fail(msg.format(conftestpath, confcutdir), pytrace=False) + + class PytestPluginManager(PluginManager): """ Overwrites :py:class:`pluggy.PluginManager ` to add pytest-specific @@ -424,16 +445,7 @@ class PytestPluginManager(PluginManager): and self._configured and not self._using_pyargs ): - from _pytest.deprecated import ( - PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST, - ) - - fail( - PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST.format( - conftestpath, self._confcutdir - ), - pytrace=False, - ) + _fail_on_non_top_pytest_plugins(conftestpath, self._confcutdir) except Exception: raise ConftestImportFailure(conftestpath, sys.exc_info()) @@ -608,20 +620,57 @@ def _iter_rewritable_modules(package_files): class Config: - """ access to configuration values, pluginmanager and plugin hooks. """ + """ + Access to configuration values, pluginmanager and plugin hooks. - def __init__(self, pluginmanager): - #: access to command line option as attributes. - #: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead - self.option = argparse.Namespace() + :ivar PytestPluginManager pluginmanager: the plugin manager handles plugin registration and hook invocation. + + :ivar argparse.Namespace option: access to command line option as attributes. + + :ivar InvocationParams invocation_params: + + Object containing the parameters regarding the ``pytest.main`` + invocation. + + Contains the following read-only attributes: + + * ``args``: list of command-line arguments as passed to ``pytest.main()``. + * ``plugins``: list of extra plugins, might be None. + * ``dir``: directory where ``pytest.main()`` was invoked from. + """ + + @attr.s(frozen=True) + class InvocationParams: + """Holds parameters passed during ``pytest.main()`` + + .. note:: + + Currently the environment variable PYTEST_ADDOPTS is also handled by + pytest implicitly, not being part of the invocation. + + Plugins accessing ``InvocationParams`` must be aware of that. + """ + + args = attr.ib() + plugins = attr.ib() + dir = attr.ib() + + def __init__(self, pluginmanager, *, invocation_params=None): from .argparsing import Parser, FILE_OR_DIR + if invocation_params is None: + invocation_params = self.InvocationParams( + args=(), plugins=None, dir=Path().resolve() + ) + + self.option = argparse.Namespace() + self.invocation_params = invocation_params + _a = FILE_OR_DIR self._parser = Parser( usage="%(prog)s [options] [{}] [{}] [...]".format(_a, _a), processopt=self._processopt, ) - #: a pluginmanager instance self.pluginmanager = pluginmanager self.trace = self.pluginmanager.trace.root.get("config") self.hook = self.pluginmanager.hook @@ -631,9 +680,13 @@ class Config: self._cleanup = [] self.pluginmanager.register(self, "pytestconfig") self._configured = False - self.invocation_dir = py.path.local() self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) + @property + def invocation_dir(self): + """Backward compatibility""" + return py.path.local(str(self.invocation_params.dir)) + def add_cleanup(self, func): """ Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" @@ -864,7 +917,7 @@ class Config: assert not hasattr( self, "args" ), "can only parse cmdline args at most once per Config object" - self._origargs = args + assert self.invocation_params.args == args self.hook.pytest_addhooks.call_historic( kwargs=dict(pluginmanager=self.pluginmanager) ) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 3f91bbd07..ec991316a 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -20,8 +20,6 @@ def getcfg(args, config=None): note: config is optional and used only to issue warnings explicitly (#2891). """ - from _pytest.deprecated import CFG_PYTEST_SECTION - inibasenames = ["pytest.ini", "tox.ini", "setup.cfg"] args = [x for x in args if not str(x).startswith("-")] if not args: @@ -101,6 +99,9 @@ def get_dirs_from_args(args): return [get_dir_from_path(path) for path in possible_paths if path.exists()] +CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." + + def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): dirs = get_dirs_from_args(args) if inifile: @@ -111,8 +112,6 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): try: inicfg = iniconfig[section] if is_cfg_file and section == "pytest" and config is not None: - from _pytest.deprecated import CFG_PYTEST_SECTION - fail( CFG_PYTEST_SECTION.format(filename=str(inifile)), pytrace=False ) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index e31b9eb0e..c06908932 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -9,10 +9,6 @@ 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 RemovedInPytest4Warning -from _pytest.warning_types import UnformattedWarning - -YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored" # set of plugins which have been integrated into the core; we use this list to ignore # them during registration to avoid conflicts @@ -23,82 +19,13 @@ DEPRECATED_EXTERNAL_PLUGINS = { } -FIXTURE_FUNCTION_CALL = ( - 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' - "but are created automatically when test functions request them as parameters.\n" - "See https://docs.pytest.org/en/latest/fixture.html for more information about fixtures, and\n" - "https://docs.pytest.org/en/latest/deprecations.html#calling-fixtures-directly about how to update your code." -) - -FIXTURE_NAMED_REQUEST = PytestDeprecationWarning( - "'request' is a reserved name for fixtures and will raise an error in future versions" -) - -CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." - -GETFUNCARGVALUE = RemovedInPytest4Warning( - "getfuncargvalue is deprecated, use getfixturevalue" -) - FUNCARGNAMES = PytestDeprecationWarning( "The `funcargnames` attribute was an alias for `fixturenames`, " "since pytest 2.3 - use the newer attribute instead." ) -RAISES_MESSAGE_PARAMETER = PytestDeprecationWarning( - "The 'message' parameter is deprecated.\n" - "(did you mean to use `match='some regex'` to check the exception message?)\n" - "Please see:\n" - " https://docs.pytest.org/en/4.6-maintenance/deprecations.html#message-parameter-of-pytest-raises" -) RESULT_LOG = PytestDeprecationWarning( "--result-log is deprecated and scheduled for removal in pytest 6.0.\n" "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." ) - -RAISES_EXEC = PytestDeprecationWarning( - "raises(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly\n\n" - "See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec" -) -WARNS_EXEC = PytestDeprecationWarning( - "warns(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly.\n\n" - "See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec" -) - -PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = ( - "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported " - "because it affects the entire directory tree in a non-explicit way.\n" - " {}\n" - "Please move it to a top level conftest file at the rootdir:\n" - " {}\n" - "For more information, visit:\n" - " https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" -) - -PYTEST_CONFIG_GLOBAL = PytestDeprecationWarning( - "the `pytest.config` global is deprecated. Please use `request.config` " - "or `pytest_configure` (if you're a pytest plugin) instead." -) - -PYTEST_ENSURETEMP = RemovedInPytest4Warning( - "pytest/tmpdir_factory.ensuretemp is deprecated, \n" - "please use the tmp_path fixture or tmp_path_factory.mktemp" -) - -PYTEST_LOGWARNING = PytestDeprecationWarning( - "pytest_logwarning is deprecated, no longer being called, and will be removed soon\n" - "please use pytest_warning_captured instead" -) - -PYTEST_WARNS_UNKNOWN_KWARGS = UnformattedWarning( - PytestDeprecationWarning, - "pytest.warns() got unexpected keyword arguments: {args!r}.\n" - "This will be an error in future versions.", -) - -PYTEST_PARAM_UNKNOWN_KWARGS = UnformattedWarning( - PytestDeprecationWarning, - "pytest.param() got unexpected keyword arguments: {args!r}.\n" - "This will be an error in future versions.", -) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index ca6e4675f..cf886f906 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -13,6 +13,7 @@ from _pytest._code.code import TerminalRepr from _pytest.compat import safe_getattr from _pytest.fixtures import FixtureRequest from _pytest.outcomes import Skipped +from _pytest.python_api import approx from _pytest.warning_types import PytestWarning DOCTEST_REPORT_CHOICE_NONE = "none" @@ -286,6 +287,7 @@ def _get_flag_lookup(): COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, ALLOW_UNICODE=_get_allow_unicode_flag(), ALLOW_BYTES=_get_allow_bytes_flag(), + NUMBER=_get_number_flag(), ) @@ -453,10 +455,15 @@ def _setup_fixtures(doctest_item): def _get_checker(): """ - Returns a doctest.OutputChecker subclass that takes in account the - ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES - to strip b'' prefixes. - Useful when the same doctest should run in Python 2 and Python 3. + Returns a doctest.OutputChecker subclass that supports some + additional options: + + * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b'' + prefixes (respectively) in string literals. Useful when the same + doctest should run in Python 2 and Python 3. + + * NUMBER to ignore floating-point differences smaller than the + precision of the literal number in the doctest. An inner class is used to avoid importing "doctest" at the module level. @@ -469,38 +476,89 @@ def _get_checker(): class LiteralsOutputChecker(doctest.OutputChecker): """ - Copied from doctest_nose_plugin.py from the nltk project: - https://github.com/nltk/nltk - - Further extended to also support byte literals. + Based on doctest_nose_plugin.py from the nltk project + (https://github.com/nltk/nltk) and on the "numtest" doctest extension + by Sebastien Boisgerault (https://github.com/boisgera/numtest). """ _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE) + _number_re = re.compile( + r""" + (?P + (?P + (?P [+-]?\d*)\.(?P\d+) + | + (?P [+-]?\d+)\. + ) + (?: + [Ee] + (?P [+-]?\d+) + )? + | + (?P [+-]?\d+) + (?: + [Ee] + (?P [+-]?\d+) + ) + ) + """, + re.VERBOSE, + ) def check_output(self, want, got, optionflags): - res = doctest.OutputChecker.check_output(self, want, got, optionflags) - if res: + if doctest.OutputChecker.check_output(self, want, got, optionflags): return True allow_unicode = optionflags & _get_allow_unicode_flag() allow_bytes = optionflags & _get_allow_bytes_flag() - if not allow_unicode and not allow_bytes: + allow_number = optionflags & _get_number_flag() + + if not allow_unicode and not allow_bytes and not allow_number: return False - else: # pragma: no cover + def remove_prefixes(regex, txt): + return re.sub(regex, r"\1\2", txt) - def remove_prefixes(regex, txt): - return re.sub(regex, r"\1\2", txt) + if allow_unicode: + want = remove_prefixes(self._unicode_literal_re, want) + got = remove_prefixes(self._unicode_literal_re, got) - if allow_unicode: - want = remove_prefixes(self._unicode_literal_re, want) - got = remove_prefixes(self._unicode_literal_re, got) - if allow_bytes: - want = remove_prefixes(self._bytes_literal_re, want) - got = remove_prefixes(self._bytes_literal_re, got) - res = doctest.OutputChecker.check_output(self, want, got, optionflags) - return res + if allow_bytes: + want = remove_prefixes(self._bytes_literal_re, want) + got = remove_prefixes(self._bytes_literal_re, got) + + if allow_number: + got = self._remove_unwanted_precision(want, got) + + return doctest.OutputChecker.check_output(self, want, got, optionflags) + + def _remove_unwanted_precision(self, want, got): + wants = list(self._number_re.finditer(want)) + gots = list(self._number_re.finditer(got)) + if len(wants) != len(gots): + return got + offset = 0 + for w, g in zip(wants, gots): + fraction = w.group("fraction") + exponent = w.group("exponent1") + if exponent is None: + exponent = w.group("exponent2") + if fraction is None: + precision = 0 + else: + precision = len(fraction) + if exponent is not None: + precision -= int(exponent) + if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): + # They're close enough. Replace the text we actually + # got with the text we want, so that it will match when we + # check the string literally. + got = ( + got[: g.start() + offset] + w.group() + got[g.end() + offset :] + ) + offset += w.end() - w.start() - (g.end() - g.start()) + return got _get_checker.LiteralsOutputChecker = LiteralsOutputChecker return _get_checker.LiteralsOutputChecker() @@ -524,6 +582,15 @@ def _get_allow_bytes_flag(): return doctest.register_optionflag("ALLOW_BYTES") +def _get_number_flag(): + """ + Registers and returns the NUMBER flag. + """ + import doctest + + return doctest.register_optionflag("NUMBER") + + def _get_report_choice(key): """ This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 6a3e82907..d5f9ad2d3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2,7 +2,6 @@ import functools import inspect import itertools import sys -import warnings from collections import defaultdict from collections import deque from collections import OrderedDict @@ -28,8 +27,6 @@ from _pytest.compat import getlocation from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import safe_getattr -from _pytest.deprecated import FIXTURE_FUNCTION_CALL -from _pytest.deprecated import FIXTURE_NAMED_REQUEST from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -475,13 +472,6 @@ class FixtureRequest(FuncargnamesCompatAttr): """ return self._get_active_fixturedef(argname).cached_result[0] - def getfuncargvalue(self, argname): - """ Deprecated, use getfixturevalue. """ - from _pytest import deprecated - - warnings.warn(deprecated.GETFUNCARGVALUE, stacklevel=2) - return self.getfixturevalue(argname) - def _get_active_fixturedef(self, argname): try: return self._fixture_defs[argname] @@ -945,9 +935,12 @@ def wrap_function_to_error_out_if_called_directly(function, fixture_marker): """Wrap the given fixture function so we can raise an error about it being called directly, instead of used as an argument in a test function. """ - message = FIXTURE_FUNCTION_CALL.format( - name=fixture_marker.name or function.__name__ - ) + message = ( + 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' + "but are created automatically when test functions request them as parameters.\n" + "See https://docs.pytest.org/en/latest/fixture.html for more information about fixtures, and\n" + "https://docs.pytest.org/en/latest/deprecations.html#calling-fixtures-directly about how to update your code." + ).format(name=fixture_marker.name or function.__name__) @functools.wraps(function) def result(*args, **kwargs): @@ -982,7 +975,13 @@ class FixtureFunctionMarker: name = self.name or function.__name__ if name == "request": - warnings.warn(FIXTURE_NAMED_REQUEST) + location = getlocation(function) + fail( + "'request' is a reserved word for fixtures, use another name:\n {}".format( + location + ), + pytrace=False, + ) function._pytestfixturefunction = self return function diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index b379fae01..50acc2d7d 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -98,7 +98,7 @@ def pytest_cmdline_parse(): py.__version__, ".".join(map(str, sys.version_info)), os.getcwd(), - config._origargs, + config.invocation_params.args, ) ) config.trace.root.setwriter(debugfile.write) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 9e6d13fab..59fc569f4 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,7 +1,6 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ from pluggy import HookspecMarker -from _pytest.deprecated import PYTEST_LOGWARNING hookspec = HookspecMarker("pytest") @@ -575,27 +574,6 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): """ -@hookspec(historic=True, warn_on_impl=PYTEST_LOGWARNING) -def pytest_logwarning(message, code, nodeid, fslocation): - """ - .. deprecated:: 3.8 - - This hook is will stop working in a future release. - - pytest no longer triggers this hook, but the - terminal writer still implements it to display warnings issued by - :meth:`_pytest.config.Config.warn` and :meth:`_pytest.nodes.Node.warn`. Calling those functions will be - an error in future releases. - - process a warning specified by a message, a code string, - a nodeid and fslocation (both of which may be None - if the warning is not tied to a particular node/location). - - .. note:: - This hook is incompatible with ``hookwrapper=True``. - """ - - @hookspec(historic=True) def pytest_warning_captured(warning_message, when, item): """ diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index ea33e606c..ba0f48ed3 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -10,9 +10,11 @@ src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd """ import functools import os +import platform import re import sys import time +from datetime import datetime import py @@ -657,18 +659,19 @@ class LogXML: ) logfile.write('') - logfile.write( - Junit.testsuite( - self._get_global_properties_node(), - [x.to_xml() for x in self.node_reporters_ordered], - name=self.suite_name, - errors=self.stats["error"], - failures=self.stats["failure"], - skipped=self.stats["skipped"], - tests=numtests, - time="%.3f" % suite_time_delta, - ).unicode(indent=0) + suite_node = Junit.testsuite( + self._get_global_properties_node(), + [x.to_xml() for x in self.node_reporters_ordered], + name=self.suite_name, + errors=self.stats["error"], + failures=self.stats["failure"], + skipped=self.stats["skipped"], + tests=numtests, + time="%.3f" % suite_time_delta, + timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(), + hostname=platform.node(), ) + logfile.write(Junit.testsuites([suite_node]).unicode(indent=0)) logfile.close() def pytest_terminal_summary(self, terminalreporter): diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 8d3f9f234..23eff52dd 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -5,7 +5,6 @@ import functools import importlib import os import sys -import warnings import attr import py @@ -15,7 +14,6 @@ from _pytest import nodes from _pytest.config import directory_arg from _pytest.config import hookimpl from _pytest.config import UsageError -from _pytest.deprecated import PYTEST_CONFIG_GLOBAL from _pytest.outcomes import exit from _pytest.runner import collect_one_node @@ -179,26 +177,6 @@ def pytest_addoption(parser): ) -class _ConfigDeprecated: - def __init__(self, config): - self.__dict__["_config"] = config - - def __getattr__(self, attr): - warnings.warn(PYTEST_CONFIG_GLOBAL, stacklevel=2) - return getattr(self._config, attr) - - def __setattr__(self, attr, val): - warnings.warn(PYTEST_CONFIG_GLOBAL, stacklevel=2) - return setattr(self._config, attr, val) - - def __repr__(self): - return "{}({!r})".format(type(self).__name__, self._config) - - -def pytest_configure(config): - __import__("pytest").config = _ConfigDeprecated(config) # compatibility - - def wrap_session(config, doit): """Skeleton command line program""" session = Session(config) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 2fb6ad851..332c86bde 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -10,7 +10,6 @@ import attr from ..compat import ascii_escaped from ..compat import getfslineno from ..compat import NOTSET -from _pytest.deprecated import PYTEST_PARAM_UNKNOWN_KWARGS from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning @@ -62,26 +61,19 @@ def get_empty_parameterset_mark(config, argnames, func): class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): @classmethod - def param(cls, *values, **kwargs): - marks = kwargs.pop("marks", ()) + def param(cls, *values, marks=(), id=None): if isinstance(marks, MarkDecorator): marks = (marks,) else: assert isinstance(marks, (tuple, list, set)) - id_ = kwargs.pop("id", None) - if id_ is not None: - if not isinstance(id_, str): + if id is not None: + if not isinstance(id, str): raise TypeError( - "Expected id to be a string, got {}: {!r}".format(type(id_), id_) + "Expected id to be a string, got {}: {!r}".format(type(id), id) ) - id_ = ascii_escaped(id_) - - if kwargs: - warnings.warn( - PYTEST_PARAM_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=3 - ) - return cls(values, marks, id_) + id = ascii_escaped(id) + return cls(values, marks, id) @classmethod def extract_from(cls, parameterset, force_tuple=False): diff --git a/src/_pytest/nose.py b/src/_pytest/nose.py index bb5ca198c..d6f3c2b22 100644 --- a/src/_pytest/nose.py +++ b/src/_pytest/nose.py @@ -1,31 +1,9 @@ """ run test suites written for nose. """ -import sys - -import pytest from _pytest import python -from _pytest import runner from _pytest import unittest from _pytest.config import hookimpl -def get_skip_exceptions(): - skip_classes = set() - for module_name in ("unittest", "unittest2", "nose"): - mod = sys.modules.get(module_name) - if hasattr(mod, "SkipTest"): - skip_classes.add(mod.SkipTest) - return tuple(skip_classes) - - -def pytest_runtest_makereport(item, call): - if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()): - # let's substitute the excinfo with a pytest.skip one - call2 = runner.CallInfo.from_call( - lambda: pytest.skip(str(call.excinfo.value)), call.when - ) - call.excinfo = call2.excinfo - - @hookimpl(trylast=True) def pytest_runtest_setup(item): if is_potential_nosetest(item): @@ -40,9 +18,6 @@ def teardown_nose(item): if is_potential_nosetest(item): if not call_optional(item.obj, "teardown"): call_optional(item.parent.obj, "teardown") - # if hasattr(item.parent, '_nosegensetup'): - # #call_optional(item._nosegensetup, 'teardown') - # del item.parent._nosegensetup def is_potential_nosetest(item): diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index a3b6d4054..947136625 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -3,16 +3,21 @@ exception classes and constants handling test outcomes as well as functions creating them """ import sys +from typing import Any +from typing import Optional from packaging.version import Version +if False: # TYPE_CHECKING + from typing import NoReturn + class OutcomeException(BaseException): """ OutcomeException and its subclass instances indicate and contain info about test and collection outcomes. """ - def __init__(self, msg=None, pytrace=True): + def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None: if msg is not None and not isinstance(msg, str): error_msg = ( "{} expected string as 'msg' parameter, got '{}' instead.\n" @@ -23,7 +28,7 @@ class OutcomeException(BaseException): self.msg = msg self.pytrace = pytrace - def __repr__(self): + def __repr__(self) -> str: if self.msg: return self.msg return "<{} instance>".format(self.__class__.__name__) @@ -39,7 +44,12 @@ class Skipped(OutcomeException): # in order to have Skipped exception printing shorter/nicer __module__ = "builtins" - def __init__(self, msg=None, pytrace=True, allow_module_level=False): + def __init__( + self, + msg: Optional[str] = None, + pytrace: bool = True, + allow_module_level: bool = False, + ) -> None: OutcomeException.__init__(self, msg=msg, pytrace=pytrace) self.allow_module_level = allow_module_level @@ -53,7 +63,9 @@ class Failed(OutcomeException): class Exit(Exception): """ raised for immediate program exits (no tracebacks/summaries)""" - def __init__(self, msg="unknown reason", returncode=None): + def __init__( + self, msg: str = "unknown reason", returncode: Optional[int] = None + ) -> None: self.msg = msg self.returncode = returncode super().__init__(msg) @@ -62,7 +74,7 @@ class Exit(Exception): # exposed helper methods -def exit(msg, returncode=None): +def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn": """ Exit testing process. @@ -77,7 +89,7 @@ def exit(msg, returncode=None): exit.Exception = Exit # type: ignore -def skip(msg="", *, allow_module_level=False): +def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn": """ Skip an executing test with the given message. @@ -104,7 +116,7 @@ def skip(msg="", *, allow_module_level=False): skip.Exception = Skipped # type: ignore -def fail(msg="", pytrace=True): +def fail(msg: str = "", pytrace: bool = True) -> "NoReturn": """ Explicitly fail an executing test with the given message. @@ -124,7 +136,7 @@ class XFailed(Failed): """ raised from an explicit call to pytest.xfail() """ -def xfail(reason=""): +def xfail(reason: str = "") -> "NoReturn": """ Imperatively xfail an executing test or setup functions with the given reason. @@ -142,12 +154,14 @@ def xfail(reason=""): xfail.Exception = XFailed # type: ignore -def importorskip(modname, minversion=None, reason=None): +def importorskip( + modname: str, minversion: Optional[str] = None, reason: Optional[str] = None +) -> Any: """Imports and returns the requested module ``modname``, or skip the current test if the module cannot be imported. :param str modname: the name of the module to import - :param str minversion: if given, the imported module ``__version__`` + :param str minversion: if given, the imported module's ``__version__`` attribute must be at least this minimal version, otherwise the test is still skipped. :param str reason: if given, this reason is shown as the message when the diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 29c200ce6..94058f70c 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -340,7 +340,10 @@ def _config_for_test(): config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles. -rex_outcome = re.compile(r"(\d+) ([\w-]+)") +# regex to match the session duration string in the summary: "74.34s" +rex_session_duration = re.compile(r"\d+\.\d\ds") +# regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped" +rex_outcome = re.compile(r"(\d+) (\w+)") class RunResult: @@ -379,14 +382,11 @@ class RunResult: """ for line in reversed(self.outlines): - if "seconds" in line: + if rex_session_duration.search(line): outcomes = rex_outcome.findall(line) - if outcomes: - d = {} - for num, cat in outcomes: - d[cat] = int(num) - return d - raise ValueError("Pytest terminal report not found") + return {noun: int(count) for (count, noun) in outcomes} + + raise ValueError("Pytest terminal summary report not found") def assert_outcomes( self, passed=0, skipped=0, failed=0, error=0, xpassed=0, xfailed=0 @@ -632,7 +632,7 @@ class Testdir: def copy_example(self, name=None): """Copy file from project's directory into the testdir. - :param str name: The name of the file for copy. + :param str name: The name of the file to copy. :return: path to the copied directory (inside ``self.tmpdir``). """ @@ -1194,6 +1194,8 @@ class Testdir: pytest.skip("pypy-64 bit not supported") if sys.platform.startswith("freebsd"): pytest.xfail("pexpect does not work reliably on freebsd") + if not hasattr(pexpect, "spawn"): + pytest.skip("pexpect.spawn not available") logfile = self.tmpdir.join("spawn.out").open("wb") # Do not load user config. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 951da792b..913a93bc0 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1,18 +1,18 @@ """ Python test discovery, setup and run of test functions. """ -import collections import enum import fnmatch import inspect import os import sys import warnings +from collections import Counter +from collections.abc import Sequence from functools import partial from textwrap import dedent import py import _pytest -from _pytest import deprecated from _pytest import fixtures from _pytest import nodes from _pytest._code import filter_traceback @@ -225,7 +225,9 @@ def pytest_pycollect_makeitem(collector, name, obj): elif getattr(obj, "__test__", True): if is_generator(obj): res = Function(name, parent=collector) - reason = deprecated.YIELD_TESTS.format(name=name) + reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format( + name=name + ) res.add_marker(MARK_GEN.xfail(run=False, reason=reason)) res.warn(PytestCollectionWarning(reason)) else: @@ -246,9 +248,6 @@ class PyobjContext: class PyobjMixin(PyobjContext): _ALLOW_MARKERS = True - def __init__(self, *k, **kw): - super().__init__(*k, **kw) - @property def obj(self): """Underlying Python object.""" @@ -400,12 +399,8 @@ class PyCollector(PyobjMixin, nodes.Collector): methods.append(module.pytest_generate_tests) if hasattr(cls, "pytest_generate_tests"): methods.append(cls().pytest_generate_tests) - if methods: - self.ihook.pytest_generate_tests.call_extra( - methods, dict(metafunc=metafunc) - ) - else: - self.ihook.pytest_generate_tests(metafunc=metafunc) + + self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) if not metafunc._calls: yield Function(name, parent=self, fixtureinfo=fixtureinfo) @@ -450,13 +445,12 @@ class Module(nodes.File, PyCollector): Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - setup_module = _get_non_fixture_func(self.obj, "setUpModule") - if setup_module is None: - setup_module = _get_non_fixture_func(self.obj, "setup_module") - - teardown_module = _get_non_fixture_func(self.obj, "tearDownModule") - if teardown_module is None: - teardown_module = _get_non_fixture_func(self.obj, "teardown_module") + setup_module = _get_first_non_fixture_func( + self.obj, ("setUpModule", "setup_module") + ) + teardown_module = _get_first_non_fixture_func( + self.obj, ("tearDownModule", "teardown_module") + ) if setup_module is None and teardown_module is None: return @@ -478,8 +472,10 @@ class Module(nodes.File, PyCollector): Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - setup_function = _get_non_fixture_func(self.obj, "setup_function") - teardown_function = _get_non_fixture_func(self.obj, "teardown_function") + setup_function = _get_first_non_fixture_func(self.obj, ("setup_function",)) + teardown_function = _get_first_non_fixture_func( + self.obj, ("teardown_function",) + ) if setup_function is None and teardown_function is None: return @@ -563,15 +559,15 @@ class Package(Module): def setup(self): # not using fixtures to call setup_module here because autouse fixtures # from packages are not called automatically (#4085) - setup_module = _get_non_fixture_func(self.obj, "setUpModule") - if setup_module is None: - setup_module = _get_non_fixture_func(self.obj, "setup_module") + setup_module = _get_first_non_fixture_func( + self.obj, ("setUpModule", "setup_module") + ) if setup_module is not None: _call_with_optional_argument(setup_module, self.obj) - teardown_module = _get_non_fixture_func(self.obj, "tearDownModule") - if teardown_module is None: - teardown_module = _get_non_fixture_func(self.obj, "teardown_module") + teardown_module = _get_first_non_fixture_func( + self.obj, ("tearDownModule", "teardown_module") + ) if teardown_module is not None: func = partial(_call_with_optional_argument, teardown_module, self.obj) self.addfinalizer(func) @@ -662,27 +658,6 @@ class Package(Module): pkg_prefixes.add(path) -def _get_xunit_setup_teardown(holder, attr_name, param_obj=None): - """ - Return a callable to perform xunit-style setup or teardown if - the function exists in the ``holder`` object. - The ``param_obj`` parameter is the parameter which will be passed to the function - when the callable is called without arguments, defaults to the ``holder`` object. - Return ``None`` if a suitable callable is not found. - """ - # TODO: only needed because of Package! - param_obj = param_obj if param_obj is not None else holder - result = _get_non_fixture_func(holder, attr_name) - if result is not None: - arg_count = result.__code__.co_argcount - if inspect.ismethod(result): - arg_count -= 1 - if arg_count: - return lambda: result(param_obj) - else: - return result - - def _call_with_optional_argument(func, arg): """Call the given function with the given argument if func accepts one argument, otherwise calls func without arguments""" @@ -695,14 +670,15 @@ def _call_with_optional_argument(func, arg): func() -def _get_non_fixture_func(obj, name): +def _get_first_non_fixture_func(obj, names): """Return the attribute from the given object to be used as a setup/teardown xunit-style function, but only if not marked as a fixture to avoid calling it twice. """ - meth = getattr(obj, name, None) - if fixtures.getfixturemarker(meth) is None: - return meth + for name in names: + meth = getattr(obj, name, None) + if meth is not None and fixtures.getfixturemarker(meth) is None: + return meth class Class(PyCollector): @@ -742,7 +718,7 @@ class Class(PyCollector): Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - setup_class = _get_non_fixture_func(self.obj, "setup_class") + setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",)) teardown_class = getattr(self.obj, "teardown_class", None) if setup_class is None and teardown_class is None: return @@ -766,7 +742,7 @@ class Class(PyCollector): Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - setup_method = _get_non_fixture_func(self.obj, "setup_method") + setup_method = _get_first_non_fixture_func(self.obj, ("setup_method",)) teardown_method = getattr(self.obj, "teardown_method", None) if setup_method is None and teardown_method is None: return @@ -904,18 +880,6 @@ class CallSpec2: self._idlist.append(id) self.marks.extend(normalize_mark_list(marks)) - def setall(self, funcargs, id, param): - for x in funcargs: - self._checkargnotcontained(x) - self.funcargs.update(funcargs) - if id is not NOTSET: - self._idlist.append(id) - if param is not NOTSET: - assert self._globalparam is NOTSET - self._globalparam = param - for arg in funcargs: - self._arg2scopenum[arg] = fixtures.scopenum_function - class Metafunc(fixtures.FuncargnamesCompatAttr): """ @@ -1076,12 +1040,9 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): * "params" if the argname should be the parameter of a fixture of the same name. * "funcargs" if the argname should be a parameter to the parametrized test function. """ - valtypes = {} - if indirect is True: - valtypes = dict.fromkeys(argnames, "params") - elif indirect is False: - valtypes = dict.fromkeys(argnames, "funcargs") - elif isinstance(indirect, (tuple, list)): + if isinstance(indirect, bool): + valtypes = dict.fromkeys(argnames, "params" if indirect else "funcargs") + elif isinstance(indirect, Sequence): valtypes = dict.fromkeys(argnames, "funcargs") for arg in indirect: if arg not in argnames: @@ -1092,6 +1053,13 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): pytrace=False, ) valtypes[arg] = "params" + else: + fail( + "In {func}: expected Sequence or boolean for indirect, got {type}".format( + type=type(indirect).__name__, func=self.function.__name__ + ), + pytrace=False, + ) return valtypes def _validate_if_using_arg_names(self, argnames, indirect): @@ -1191,7 +1159,7 @@ def _idval(val, argname, idx, idfn, item, config): return str(val) elif isinstance(val, REGEX_TYPE): return ascii_escaped(val.pattern) - elif enum is not None and isinstance(val, enum.Enum): + elif isinstance(val, enum.Enum): return str(val) elif (inspect.isclass(val) or inspect.isfunction(val)) and hasattr(val, "__name__"): return val.__name__ @@ -1219,7 +1187,7 @@ def idmaker(argnames, parametersets, idfn=None, ids=None, config=None, item=None if len(set(ids)) != len(ids): # The ids are not unique duplicates = [testid for testid in ids if ids.count(testid) > 1] - counters = collections.defaultdict(lambda: 0) + counters = Counter() for index, testid in enumerate(ids): if testid in duplicates: ids[index] = testid + str(counters[testid]) @@ -1408,14 +1376,11 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): # https://github.com/pytest-dev/pytest/issues/4569 self.keywords.update( - dict.fromkeys( - [ - mark.name - for mark in self.iter_markers() - if mark.name not in self.keywords - ], - True, - ) + { + mark.name: True + for mark in self.iter_markers() + if mark.name not in self.keywords + } ) if fixtureinfo is None: diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index bd0f4d59f..fbc3d914e 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,23 +1,33 @@ import inspect import math import pprint -import sys -import warnings from collections.abc import Iterable from collections.abc import Mapping from collections.abc import Sized from decimal import Decimal from itertools import filterfalse from numbers import Number +from types import TracebackType +from typing import Any +from typing import Callable +from typing import cast +from typing import Generic +from typing import Optional +from typing import overload +from typing import Pattern +from typing import Tuple +from typing import TypeVar from typing import Union from more_itertools.more import always_iterable import _pytest._code -from _pytest import deprecated from _pytest.compat import STRING_TYPES from _pytest.outcomes import fail +if False: # TYPE_CHECKING + from typing import Type # noqa: F401 (used in type string) + BASE_TYPE = (type, STRING_TYPES) @@ -530,8 +540,35 @@ def _is_numpy_array(obj): # builtin pytest.raises helper +_E = TypeVar("_E", bound=BaseException) -def raises(expected_exception, *args, **kwargs): + +@overload +def raises( + expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], + *, + match: Optional[Union[str, Pattern]] = ... +) -> "RaisesContext[_E]": + ... # pragma: no cover + + +@overload +def raises( + expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], + func: Callable, + *args: Any, + match: Optional[str] = ..., + **kwargs: Any +) -> Optional[_pytest._code.ExceptionInfo[_E]]: + ... # pragma: no cover + + +def raises( + expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], + *args: Any, + match: Optional[Union[str, Pattern]] = None, + **kwargs: Any +) -> Union["RaisesContext[_E]", Optional[_pytest._code.ExceptionInfo[_E]]]: r""" Assert that a code block/function call raises ``expected_exception`` or raise a failure exception otherwise. @@ -544,8 +581,6 @@ def raises(expected_exception, *args, **kwargs): __ https://docs.python.org/3/library/re.html#regular-expression-syntax - :kwparam message: **(deprecated since 4.1)** if specified, provides a custom failure message - if the exception is not raised. See :ref:`the deprecation docs ` for a workaround. .. currentmodule:: _pytest._code @@ -652,70 +687,71 @@ def raises(expected_exception, *args, **kwargs): for exc in filterfalse( inspect.isclass, always_iterable(expected_exception, BASE_TYPE) ): - msg = ( - "exceptions must be old-style classes or" - " derived from BaseException, not %s" - ) + msg = "exceptions must be derived from BaseException, not %s" raise TypeError(msg % type(exc)) message = "DID NOT RAISE {}".format(expected_exception) - match_expr = None if not args: - if "message" in kwargs: - message = kwargs.pop("message") - warnings.warn(deprecated.RAISES_MESSAGE_PARAMETER, stacklevel=2) - if "match" in kwargs: - match_expr = kwargs.pop("match") if kwargs: msg = "Unexpected keyword arguments passed to pytest.raises: " msg += ", ".join(sorted(kwargs)) + msg += "\nUse context-manager form instead?" raise TypeError(msg) - return RaisesContext(expected_exception, message, match_expr) - elif isinstance(args[0], str): - warnings.warn(deprecated.RAISES_EXEC, stacklevel=2) - code, = args - assert isinstance(code, str) - frame = sys._getframe(1) - loc = frame.f_locals.copy() - loc.update(kwargs) - # print "raises frame scope: %r" % frame.f_locals - try: - code = _pytest._code.Source(code).compile(_genframe=frame) - exec(code, frame.f_globals, loc) - # XXX didn't mean f_globals == f_locals something special? - # this is destroyed here ... - except expected_exception: - return _pytest._code.ExceptionInfo.from_current() + return RaisesContext(expected_exception, message, match) else: func = args[0] + if not callable(func): + raise TypeError( + "{!r} object (type: {}) must be callable".format(func, type(func)) + ) try: func(*args[1:], **kwargs) - except expected_exception: - return _pytest._code.ExceptionInfo.from_current() + except expected_exception as e: + # We just caught the exception - there is a traceback. + assert e.__traceback__ is not None + return _pytest._code.ExceptionInfo.from_exc_info( + (type(e), e, e.__traceback__) + ) fail(message) raises.Exception = fail.Exception # type: ignore -class RaisesContext: - def __init__(self, expected_exception, message, match_expr): +class RaisesContext(Generic[_E]): + def __init__( + self, + expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], + message: str, + match_expr: Optional[Union[str, Pattern]] = None, + ) -> None: self.expected_exception = expected_exception self.message = message self.match_expr = match_expr - self.excinfo = None + self.excinfo = None # type: Optional[_pytest._code.ExceptionInfo[_E]] - def __enter__(self): + def __enter__(self) -> _pytest._code.ExceptionInfo[_E]: self.excinfo = _pytest._code.ExceptionInfo.for_later() return self.excinfo - def __exit__(self, *tp): + def __exit__( + self, + exc_type: Optional["Type[BaseException]"], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: __tracebackhide__ = True - if tp[0] is None: + if exc_type is None: fail(self.message) - self.excinfo.__init__(tp) - suppress_exception = issubclass(self.excinfo.type, self.expected_exception) - if self.match_expr is not None and suppress_exception: + assert self.excinfo is not None + if not issubclass(exc_type, self.expected_exception): + return False + # Cast to narrow the exception type now that it's verified. + exc_info = cast( + Tuple["Type[_E]", _E, TracebackType], (exc_type, exc_val, exc_tb) + ) + self.excinfo.fill_unfilled(exc_info) + if self.match_expr is not None: self.excinfo.match(self.match_expr) - return suppress_exception + return True diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 006d97e7f..19e3938c3 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -1,15 +1,23 @@ """ recording warnings during test function execution. """ -import inspect import re -import sys import warnings +from types import TracebackType +from typing import Any +from typing import Callable +from typing import Iterator +from typing import List +from typing import Optional +from typing import overload +from typing import Pattern +from typing import Tuple +from typing import Union -import _pytest._code -from _pytest.deprecated import PYTEST_WARNS_UNKNOWN_KWARGS -from _pytest.deprecated import WARNS_EXEC from _pytest.fixtures import yield_fixture from _pytest.outcomes import fail +if False: # TYPE_CHECKING + from typing import Type + @yield_fixture def recwarn(): @@ -46,7 +54,32 @@ def deprecated_call(func=None, *args, **kwargs): return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) -def warns(expected_warning, *args, **kwargs): +@overload +def warns( + expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], + *, + match: Optional[Union[str, Pattern]] = ... +) -> "WarningsChecker": + ... # pragma: no cover + + +@overload +def warns( + expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], + func: Callable, + *args: Any, + match: Optional[Union[str, Pattern]] = ..., + **kwargs: Any +) -> Union[Any]: + ... # pragma: no cover + + +def warns( + expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], + *args: Any, + match: Optional[Union[str, Pattern]] = None, + **kwargs: Any +) -> Union["WarningsChecker", Any]: r"""Assert that code raises a particular class of warning. Specifically, the parameter ``expected_warning`` can be a warning class or @@ -80,25 +113,18 @@ def warns(expected_warning, *args, **kwargs): """ __tracebackhide__ = True if not args: - match_expr = kwargs.pop("match", None) if kwargs: - warnings.warn( - PYTEST_WARNS_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=2 - ) - return WarningsChecker(expected_warning, match_expr=match_expr) - elif isinstance(args[0], str): - warnings.warn(WARNS_EXEC, stacklevel=2) - code, = args - assert isinstance(code, str) - frame = sys._getframe(1) - loc = frame.f_locals.copy() - loc.update(kwargs) - - with WarningsChecker(expected_warning): - code = _pytest._code.Source(code).compile() - exec(code, frame.f_globals, loc) + msg = "Unexpected keyword arguments passed to pytest.warns: " + msg += ", ".join(sorted(kwargs)) + msg += "\nUse context-manager form instead?" + raise TypeError(msg) + return WarningsChecker(expected_warning, match_expr=match) else: func = args[0] + if not callable(func): + raise TypeError( + "{!r} object (type: {}) must be callable".format(func, type(func)) + ) with WarningsChecker(expected_warning): return func(*args[1:], **kwargs) @@ -112,26 +138,26 @@ class WarningsRecorder(warnings.catch_warnings): def __init__(self): super().__init__(record=True) self._entered = False - self._list = [] + self._list = [] # type: List[warnings._Record] @property - def list(self): + def list(self) -> List["warnings._Record"]: """The list of recorded warnings.""" return self._list - def __getitem__(self, i): + def __getitem__(self, i: int) -> "warnings._Record": """Get a recorded warning by index.""" return self._list[i] - def __iter__(self): + def __iter__(self) -> Iterator["warnings._Record"]: """Iterate through the recorded warnings.""" return iter(self._list) - def __len__(self): + def __len__(self) -> int: """The number of recorded warnings.""" return len(self._list) - def pop(self, cls=Warning): + def pop(self, cls: "Type[Warning]" = Warning) -> "warnings._Record": """Pop the first recorded warning, raise exception if not exists.""" for i, w in enumerate(self._list): if issubclass(w.category, cls): @@ -139,54 +165,80 @@ class WarningsRecorder(warnings.catch_warnings): __tracebackhide__ = True raise AssertionError("%r not found in warning list" % cls) - def clear(self): + def clear(self) -> None: """Clear the list of recorded warnings.""" self._list[:] = [] - def __enter__(self): + # Type ignored because it doesn't exactly warnings.catch_warnings.__enter__ + # -- it returns a List but we only emulate one. + def __enter__(self) -> "WarningsRecorder": # type: ignore if self._entered: __tracebackhide__ = True raise RuntimeError("Cannot enter %r twice" % self) - self._list = super().__enter__() + _list = super().__enter__() + # record=True means it's None. + assert _list is not None + self._list = _list warnings.simplefilter("always") return self - def __exit__(self, *exc_info): + def __exit__( + self, + exc_type: Optional["Type[BaseException]"], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: if not self._entered: __tracebackhide__ = True raise RuntimeError("Cannot exit %r without entering first" % self) - super().__exit__(*exc_info) + super().__exit__(exc_type, exc_val, exc_tb) # Built-in catch_warnings does not reset entered state so we do it # manually here for this context manager to become reusable. self._entered = False + return False + class WarningsChecker(WarningsRecorder): - def __init__(self, expected_warning=None, match_expr=None): + def __init__( + self, + expected_warning: Optional[ + Union["Type[Warning]", Tuple["Type[Warning]", ...]] + ] = None, + match_expr: Optional[Union[str, Pattern]] = None, + ) -> None: super().__init__() - msg = "exceptions must be old-style classes or derived from Warning, not %s" - if isinstance(expected_warning, tuple): + msg = "exceptions must be derived from Warning, not %s" + if expected_warning is None: + expected_warning_tup = None + elif isinstance(expected_warning, tuple): for exc in expected_warning: - if not inspect.isclass(exc): + if not issubclass(exc, Warning): raise TypeError(msg % type(exc)) - elif inspect.isclass(expected_warning): - expected_warning = (expected_warning,) - elif expected_warning is not None: + expected_warning_tup = expected_warning + elif issubclass(expected_warning, Warning): + expected_warning_tup = (expected_warning,) + else: raise TypeError(msg % type(expected_warning)) - self.expected_warning = expected_warning + self.expected_warning = expected_warning_tup self.match_expr = match_expr - def __exit__(self, *exc_info): - super().__exit__(*exc_info) + def __exit__( + self, + exc_type: Optional["Type[BaseException]"], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: + super().__exit__(exc_type, exc_val, exc_tb) __tracebackhide__ = True # only check if we're not currently handling an exception - if all(a is None for a in exc_info): + if exc_type is None and exc_val is None and exc_tb is None: if self.expected_warning is not None: if not any(issubclass(r.category, self.expected_warning) for r in self): __tracebackhide__ = True @@ -211,3 +263,4 @@ class WarningsChecker(WarningsRecorder): [each.message for each in self], ) ) + return False diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 9c91a49a5..8aae163c3 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -249,10 +249,11 @@ def pytest_make_collect_report(collector): if not call.excinfo: outcome = "passed" else: - from _pytest import nose - - skip_exceptions = (Skipped,) + nose.get_skip_exceptions() - if call.excinfo.errisinstance(skip_exceptions): + skip_exceptions = [Skipped] + unittest = sys.modules.get("unittest") + if unittest is not None: + skip_exceptions.append(unittest.SkipTest) + if call.excinfo.errisinstance(tuple(skip_exceptions)): outcome = "skipped" r = collector._repr_failure_py(call.excinfo, "line").reprcrash longrepr = (str(r.path), r.lineno, r.message) @@ -277,10 +278,7 @@ class SetupState: self._finalizers = {} def addfinalizer(self, finalizer, colitem): - """ attach a finalizer to the given colitem. - if colitem is None, this will add a finalizer that - is called at the end of teardown_all(). - """ + """ attach a finalizer to the given colitem. """ assert colitem and not isinstance(colitem, tuple) assert callable(finalizer) # assert colitem in self.stack # some unit tests don't setup stack :/ @@ -308,12 +306,9 @@ class SetupState: def _teardown_with_finalization(self, colitem): self._callfinalizers(colitem) - if hasattr(colitem, "teardown"): - colitem.teardown() + colitem.teardown() for colitem in self._finalizers: - assert ( - colitem is None or colitem in self.stack or isinstance(colitem, tuple) - ) + assert colitem in self.stack def teardown_all(self): while self.stack: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 05d5427c3..0d9794159 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -4,6 +4,7 @@ This is a good source for looking at the various reporting hooks. """ import argparse import collections +import datetime import platform import sys import time @@ -861,7 +862,7 @@ class TerminalReporter: def summary_stats(self): session_duration = time.time() - self._sessionstarttime (line, color) = build_summary_stats_line(self.stats) - msg = "{} in {:.2f} seconds".format(line, session_duration) + msg = "{} in {}".format(line, format_session_duration(session_duration)) markup = {color: True, "bold": True} if self.verbosity >= 0: @@ -1055,3 +1056,12 @@ def _plugin_nameversions(plugininfo): if name not in values: values.append(name) return values + + +def format_session_duration(seconds): + """Format the given seconds in a human readable manner to show in the final summary""" + if seconds < 60: + return "{:.2f}s".format(seconds) + else: + dt = datetime.timedelta(seconds=int(seconds)) + return "{:.2f}s ({})".format(seconds, dt) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 48680c07e..123a583ad 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -2,7 +2,6 @@ import os import re import tempfile -import warnings import attr import py @@ -88,19 +87,6 @@ class TempdirFactory: _tmppath_factory = attr.ib() - def ensuretemp(self, string, dir=1): - """ (deprecated) return temporary directory path with - the given string as the trailing part. It is usually - better to use the 'tmpdir' function argument which - provides an empty unique-per-test-invocation directory - and is guaranteed to be empty. - """ - # py.log._apiwarn(">1.1", "use tmpdir function argument") - from .deprecated import PYTEST_ENSURETEMP - - warnings.warn(PYTEST_ENSURETEMP, stacklevel=2) - return self.getbasetemp().ensure(string, dir=dir) - def mktemp(self, basename, numbered=True): """Create a subdirectory of the base temporary directory and return it. If ``numbered``, ensure the directory is unique by adding a number @@ -138,7 +124,6 @@ def pytest_configure(config): config._cleanup.append(mp.undo) mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False) mp.setattr(config, "_tmpdirhandler", t, raising=False) - mp.setattr(pytest, "ensuretemp", t.ensuretemp, raising=False) @pytest.fixture(scope="session") diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 337490d13..11dc77cc4 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -12,6 +12,7 @@ from _pytest.outcomes import skip from _pytest.outcomes import xfail from _pytest.python import Class from _pytest.python import Function +from _pytest.runner import CallInfo def pytest_pycollect_makeitem(collector, name, obj): @@ -229,6 +230,14 @@ def pytest_runtest_makereport(item, call): except AttributeError: pass + unittest = sys.modules.get("unittest") + if unittest and call.excinfo and call.excinfo.errisinstance(unittest.SkipTest): + # let's substitute the excinfo with a pytest.skip one + call2 = CallInfo.from_call( + lambda: pytest.skip(str(call.excinfo.value)), call.when + ) + call.excinfo = call2.excinfo + # twisted trial support diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index ac7e5ca48..80353ccbc 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -103,16 +103,6 @@ class PytestUnknownMarkWarning(PytestWarning): __module__ = "pytest" -class RemovedInPytest4Warning(PytestDeprecationWarning): - """ - Bases: :class:`pytest.PytestDeprecationWarning`. - - Warning class for features scheduled to be removed in pytest 4.0. - """ - - __module__ = "pytest" - - @attr.s class UnformattedWarning: """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index f47eee0d4..63d22477c 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -4,8 +4,6 @@ from contextlib import contextmanager import pytest -SHOW_PYTEST_WARNINGS_ARG = "-Walways::pytest.RemovedInPytest4Warning" - def _setoption(wmod, arg): """ @@ -74,9 +72,6 @@ def catch_warnings_for_item(config, ihook, when, item): warnings.filterwarnings("always", category=DeprecationWarning) warnings.filterwarnings("always", category=PendingDeprecationWarning) - warnings.filterwarnings("error", category=pytest.RemovedInPytest4Warning) - warnings.filterwarnings("error", category=pytest.PytestDeprecationWarning) - # filters should have this precedence: mark, cmdline options, ini # filters should be applied in the inverse order of precedence for arg in inifilters: diff --git a/src/pytest.py b/src/pytest.py index b4faf4978..b934e65cb 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -44,7 +44,7 @@ from _pytest.warning_types import PytestExperimentalApiWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestWarning -from _pytest.warning_types import RemovedInPytest4Warning + set_trace = __pytestPDB.set_trace @@ -84,7 +84,6 @@ __all__ = [ "PytestWarning", "raises", "register_assert_rewrite", - "RemovedInPytest4Warning", "Session", "set_trace", "skip", diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 7abbe734e..ad9c37737 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -4,12 +4,11 @@ import textwrap import types import attr -import importlib_metadata import py import pytest +from _pytest.compat import importlib_metadata from _pytest.main import ExitCode -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG def prepend_pythonpath(*dirs): @@ -343,7 +342,7 @@ class TestGeneralUsage: """ ) p = testdir.makepyfile("""def test_func(x): pass""") - res = testdir.runpytest(p, SHOW_PYTEST_WARNINGS_ARG) + res = testdir.runpytest(p) assert res.ret == 0 res.stdout.fnmatch_lines(["*1 skipped*"]) @@ -356,9 +355,7 @@ class TestGeneralUsage: pass """ ) - res = testdir.runpytest( - p.basename + "::" + "test_func[1]", SHOW_PYTEST_WARNINGS_ARG - ) + res = testdir.runpytest(p.basename + "::" + "test_func[1]") assert res.ret == 0 res.stdout.fnmatch_lines(["*1 passed*"]) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 7428a459c..7742b4da9 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -58,7 +58,7 @@ class TWMock: fullwidth = 80 -def test_excinfo_simple(): +def test_excinfo_simple() -> None: try: raise ValueError except ValueError: @@ -66,6 +66,14 @@ def test_excinfo_simple(): assert info.type == ValueError +def test_excinfo_from_exc_info_simple(): + try: + raise ValueError + except ValueError as e: + info = _pytest._code.ExceptionInfo.from_exc_info((type(e), e, e.__traceback__)) + assert info.type == ValueError + + def test_excinfo_getstatement(): def g(): raise ValueError diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 39439ab44..b8a22428f 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,39 +1,5 @@ -import os - import pytest from _pytest import deprecated -from _pytest.warning_types import PytestDeprecationWarning -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG - -pytestmark = pytest.mark.pytester_example_path("deprecated") - - -def test_pytest_setup_cfg_unsupported(testdir): - testdir.makefile( - ".cfg", - setup=""" - [pytest] - addopts = --verbose - """, - ) - with pytest.raises(pytest.fail.Exception): - testdir.runpytest() - - -def test_pytest_custom_cfg_unsupported(testdir): - testdir.makefile( - ".cfg", - custom=""" - [pytest] - addopts = --verbose - """, - ) - with pytest.raises(pytest.fail.Exception): - testdir.runpytest("-c", "custom.cfg") - - -def test_getfuncargvalue_is_deprecated(request): - pytest.deprecated_call(request.getfuncargvalue, "tmpdir") @pytest.mark.filterwarnings("default") @@ -78,142 +44,3 @@ def test_external_plugins_integrated(testdir, plugin): with pytest.warns(pytest.PytestConfigWarning): testdir.parseconfig("-p", plugin) - - -def test_raises_message_argument_deprecated(): - with pytest.warns(pytest.PytestDeprecationWarning): - with pytest.raises(RuntimeError, message="foobar"): - raise RuntimeError - - -def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir): - from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST - - testdir.makepyfile( - **{ - "subdirectory/conftest.py": """ - pytest_plugins=['capture'] - """ - } - ) - testdir.makepyfile( - """ - def test_func(): - pass - """ - ) - res = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) - assert res.ret == 2 - msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] - res.stdout.fnmatch_lines( - ["*{msg}*".format(msg=msg), "*subdirectory{sep}conftest.py*".format(sep=os.sep)] - ) - - -@pytest.mark.parametrize("use_pyargs", [True, False]) -def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( - testdir, use_pyargs -): - """When using --pyargs, do not emit the warning about non-top-level conftest warnings (#4039, #4044)""" - from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST - - files = { - "src/pkg/__init__.py": "", - "src/pkg/conftest.py": "", - "src/pkg/test_root.py": "def test(): pass", - "src/pkg/sub/__init__.py": "", - "src/pkg/sub/conftest.py": "pytest_plugins=['capture']", - "src/pkg/sub/test_bar.py": "def test(): pass", - } - testdir.makepyfile(**files) - testdir.syspathinsert(testdir.tmpdir.join("src")) - - args = ("--pyargs", "pkg") if use_pyargs else () - args += (SHOW_PYTEST_WARNINGS_ARG,) - res = testdir.runpytest(*args) - assert res.ret == (0 if use_pyargs else 2) - msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] - if use_pyargs: - assert msg not in res.stdout.str() - else: - res.stdout.fnmatch_lines(["*{msg}*".format(msg=msg)]) - - -def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest( - testdir -): - from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST - - subdirectory = testdir.tmpdir.join("subdirectory") - subdirectory.mkdir() - testdir.makeconftest( - """ - pytest_plugins=['capture'] - """ - ) - testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py")) - - testdir.makepyfile( - """ - def test_func(): - pass - """ - ) - - res = testdir.runpytest_subprocess() - assert res.ret == 2 - msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] - res.stdout.fnmatch_lines( - ["*{msg}*".format(msg=msg), "*subdirectory{sep}conftest.py*".format(sep=os.sep)] - ) - - -def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives( - testdir -): - from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST - - subdirectory = testdir.tmpdir.join("subdirectory") - subdirectory.mkdir() - testdir.makeconftest( - """ - pass - """ - ) - testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py")) - - testdir.makeconftest( - """ - import warnings - warnings.filterwarnings('always', category=DeprecationWarning) - pytest_plugins=['capture'] - """ - ) - testdir.makepyfile( - """ - def test_func(): - pass - """ - ) - res = testdir.runpytest_subprocess() - assert res.ret == 0 - msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] - assert msg not in res.stdout.str() - - -def test_fixture_named_request(testdir): - testdir.copy_example() - result = testdir.runpytest() - result.stdout.fnmatch_lines( - [ - "*'request' is a reserved name for fixtures and will raise an error in future versions" - ] - ) - - -def test_pytest_warns_unknown_kwargs(): - with pytest.warns( - PytestDeprecationWarning, - match=r"pytest.warns\(\) got unexpected keyword arguments: \['foo'\]", - ): - pytest.warns(UserWarning, foo="hello") diff --git a/testing/example_scripts/deprecated/test_fixture_named_request.py b/testing/example_scripts/fixtures/test_fixture_named_request.py similarity index 100% rename from testing/example_scripts/deprecated/test_fixture_named_request.py rename to testing/example_scripts/fixtures/test_fixture_named_request.py diff --git a/testing/example_scripts/junit-10.xsd b/testing/example_scripts/junit-10.xsd new file mode 100644 index 000000000..286fbf7c8 --- /dev/null +++ b/testing/example_scripts/junit-10.xsd @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index f6abfe322..86897b57c 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -45,10 +45,21 @@ def test_exceptions(): assert "unknown" in s2 +def test_buggy_builtin_repr(): + # Simulate a case where a repr for a builtin raises. + # reprlib dispatches by type name, so use "int". + + class int: + def __repr__(self): + raise ValueError("Buggy repr!") + + assert "Buggy" in saferepr(int()) + + def test_big_repr(): from _pytest._io.saferepr import SafeRepr - assert len(saferepr(range(1000))) <= len("[" + SafeRepr().maxlist * "1000" + "]") + assert len(saferepr(range(1000))) <= len("[" + SafeRepr(0).maxlist * "1000" + "]") def test_repr_on_newstyle(): diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index bb1aebc09..1ae0bd783 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -946,7 +946,7 @@ def test_collection_collect_only_live_logging(testdir, verbose): expected_lines.extend( [ "*test_collection_collect_only_live_logging.py::test_simple*", - "no tests ran in * seconds", + "no tests ran in 0.[0-9][0-9]s", ] ) elif verbose == "-qq": diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index d408dd73e..1f383e752 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -7,7 +7,6 @@ from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureRequest from _pytest.pathlib import Path from _pytest.pytester import get_public_names -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG def test_getfuncargnames_functions(): @@ -639,8 +638,7 @@ class TestRequestBasic: result = testdir.runpytest() result.stdout.fnmatch_lines(["* 2 passed in *"]) - @pytest.mark.parametrize("getfixmethod", ("getfixturevalue", "getfuncargvalue")) - def test_getfixturevalue(self, testdir, getfixmethod): + def test_getfixturevalue(self, testdir): item = testdir.getitem( """ import pytest @@ -653,35 +651,22 @@ class TestRequestBasic: def test_func(something): pass """ ) - import contextlib - - if getfixmethod == "getfuncargvalue": - warning_expectation = pytest.warns(DeprecationWarning) - else: - # see #1830 for a cleaner way to accomplish this - @contextlib.contextmanager - def expecting_no_warning(): - yield - - warning_expectation = expecting_no_warning() - req = item._request - with warning_expectation: - fixture_fetcher = getattr(req, getfixmethod) - with pytest.raises(FixtureLookupError): - fixture_fetcher("notexists") - val = fixture_fetcher("something") - assert val == 1 - val = fixture_fetcher("something") - assert val == 1 - val2 = fixture_fetcher("other") - assert val2 == 2 - val2 = fixture_fetcher("other") # see about caching - assert val2 == 2 - pytest._fillfuncargs(item) - assert item.funcargs["something"] == 1 - assert len(get_public_names(item.funcargs)) == 2 - assert "request" in item.funcargs + + with pytest.raises(FixtureLookupError): + req.getfixturevalue("notexists") + val = req.getfixturevalue("something") + assert val == 1 + val = req.getfixturevalue("something") + assert val == 1 + val2 = req.getfixturevalue("other") + assert val2 == 2 + val2 = req.getfixturevalue("other") # see about caching + assert val2 == 2 + pytest._fillfuncargs(item) + assert item.funcargs["something"] == 1 + assert len(get_public_names(item.funcargs)) == 2 + assert "request" in item.funcargs def test_request_addfinalizer(self, testdir): item = testdir.getitem( @@ -1181,21 +1166,6 @@ class TestFixtureUsages: values = reprec.getfailedcollections() assert len(values) == 1 - def test_request_can_be_overridden(self, testdir): - testdir.makepyfile( - """ - import pytest - @pytest.fixture() - def request(request): - request.a = 1 - return request - def test_request(request): - assert request.a == 1 - """ - ) - reprec = testdir.inline_run("-Wignore::pytest.PytestDeprecationWarning") - reprec.assertoutcome(passed=1) - def test_usefixtures_marker(self, testdir): testdir.makepyfile( """ @@ -2240,7 +2210,7 @@ class TestFixtureMarker: pass """ ) - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines( ["*ScopeMismatch*You tried*function*session*request*"] @@ -4028,3 +3998,14 @@ def test_fixture_param_shadowing(testdir): result.stdout.fnmatch_lines(["*::test_normal_fixture[[]a[]]*"]) result.stdout.fnmatch_lines(["*::test_normal_fixture[[]b[]]*"]) result.stdout.fnmatch_lines(["*::test_indirect[[]1[]]*"]) + + +def test_fixture_named_request(testdir): + testdir.copy_example("fixtures/test_fixture_named_request.py") + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "*'request' is a reserved word for fixtures, use another name:", + " *test_fixture_named_request.py:5", + ] + ) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 542557252..4273ab796 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -9,7 +9,6 @@ from hypothesis import strategies import pytest from _pytest import fixtures from _pytest import python -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG class TestMetafunc: @@ -600,6 +599,17 @@ class TestMetafunc: assert metafunc._calls[0].funcargs == dict(x="a", y="b") assert metafunc._calls[0].params == {} + def test_parametrize_indirect_wrong_type(self): + def func(x, y): + pass + + metafunc = self.Metafunc(func) + with pytest.raises( + pytest.fail.Exception, + match="In func: expected Sequence or boolean for indirect, got dict", + ): + metafunc.parametrize("x, y", [("a", "b")], indirect={}) + def test_parametrize_indirect_list_functional(self, testdir): """ #714 @@ -915,7 +925,7 @@ class TestMetafuncFunctional: assert metafunc.cls == TestClass """ ) - result = testdir.runpytest(p, "-v", SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest(p, "-v") result.assert_outcomes(passed=2) def test_two_functions(self, testdir): @@ -931,7 +941,7 @@ class TestMetafuncFunctional: assert arg1 in (10, 20) """ ) - result = testdir.runpytest("-v", p, SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest("-v", p) result.stdout.fnmatch_lines( [ "*test_func1*0*PASS*", @@ -967,7 +977,7 @@ class TestMetafuncFunctional: assert hello == "world" """ ) - result = testdir.runpytest("-v", p, SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest("-v", p) result.stdout.fnmatch_lines(["*test_myfunc*hello*PASS*", "*1 passed*"]) def test_two_functions_not_same_instance(self, testdir): @@ -982,7 +992,7 @@ class TestMetafuncFunctional: self.x = 1 """ ) - result = testdir.runpytest("-v", p, SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest("-v", p) result.stdout.fnmatch_lines( ["*test_func*0*PASS*", "*test_func*1*PASS*", "*2 pass*"] ) @@ -1000,7 +1010,7 @@ class TestMetafuncFunctional: self.val = 1 """ ) - result = testdir.runpytest(p, SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest(p) result.assert_outcomes(passed=1) def test_parametrize_functional2(self, testdir): @@ -1522,7 +1532,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - rec = testdir.inline_run("-m", "foo", SHOW_PYTEST_WARNINGS_ARG) + rec = testdir.inline_run("-m", "foo") passed, skipped, fail = rec.listoutcomes() assert len(passed) == 1 assert len(skipped) == 0 @@ -1562,7 +1572,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() # xfail is skip?? reprec.assertoutcome(passed=2, skipped=1) @@ -1579,7 +1589,7 @@ class TestMarkersWithParametrization: assert n % 2 == 0 """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=1) def test_xfail_with_arg(self, testdir): @@ -1595,7 +1605,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=1) def test_xfail_with_kwarg(self, testdir): @@ -1611,7 +1621,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=1) def test_xfail_with_arg_and_kwarg(self, testdir): @@ -1627,7 +1637,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=1) @pytest.mark.parametrize("strict", [True, False]) @@ -1648,7 +1658,7 @@ class TestMarkersWithParametrization: strict=strict ) testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() passed, failed = (2, 1) if strict else (3, 0) reprec.assertoutcome(passed=passed, failed=failed) @@ -1672,7 +1682,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=2) def test_parametrize_ID_generation_string_int_works(self, testdir): diff --git a/testing/python/raises.py b/testing/python/raises.py index a67f25534..668be57fc 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -2,35 +2,20 @@ import sys import pytest from _pytest.outcomes import Failed -from _pytest.warning_types import PytestDeprecationWarning class TestRaises: + def test_check_callable(self): + with pytest.raises(TypeError, match=r".* must be callable"): + pytest.raises(RuntimeError, "int('qwe')") + def test_raises(self): - source = "int('qwe')" - with pytest.warns(PytestDeprecationWarning): - excinfo = pytest.raises(ValueError, source) - code = excinfo.traceback[-1].frame.code - s = str(code.fullsource) - assert s == source - - def test_raises_exec(self): - with pytest.warns(PytestDeprecationWarning) as warninfo: - pytest.raises(ValueError, "a,x = []") - assert warninfo[0].filename == __file__ - - def test_raises_exec_correct_filename(self): - with pytest.warns(PytestDeprecationWarning): - excinfo = pytest.raises(ValueError, 'int("s")') - assert __file__ in excinfo.traceback[-1].path - - def test_raises_syntax_error(self): - with pytest.warns(PytestDeprecationWarning) as warninfo: - pytest.raises(SyntaxError, "qwe qwe qwe") - assert warninfo[0].filename == __file__ + excinfo = pytest.raises(ValueError, int, "qwe") + assert "invalid literal" in str(excinfo.value) def test_raises_function(self): - pytest.raises(ValueError, int, "hello") + excinfo = pytest.raises(ValueError, int, "hello") + assert "invalid literal" in str(excinfo.value) def test_raises_callable_no_exception(self): class A: @@ -169,17 +154,6 @@ class TestRaises: else: assert False, "Expected pytest.raises.Exception" - def test_custom_raise_message(self): - message = "TEST_MESSAGE" - try: - with pytest.warns(PytestDeprecationWarning): - with pytest.raises(ValueError, message=message): - pass - except pytest.raises.Exception as e: - assert e.msg == message - else: - assert False, "Expected pytest.raises.Exception" - @pytest.mark.parametrize("method", ["function", "with"]) def test_raises_cyclic_reference(self, method): """ @@ -274,3 +248,9 @@ class TestRaises: with pytest.raises(CrappyClass()): pass assert "via __class__" in excinfo.value.args[0] + + def test_raises_context_manager_with_kwargs(self): + with pytest.raises(TypeError) as excinfo: + with pytest.raises(Exception, foo="bar"): + pass + assert "Unexpected keyword arguments" in str(excinfo.value) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index f58d240a5..8079c45a0 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -172,7 +172,8 @@ class TestImportHookInstallation: return check """, "mainwrapper.py": """\ - import pytest, importlib_metadata + import pytest + from _pytest.compat import importlib_metadata class DummyEntryPoint(object): name = 'spam' diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 9f0979f77..89b23a72c 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -200,6 +200,16 @@ class TestAssertionRewrite: else: assert msg == ["assert cls == 42"] + def test_assertrepr_compare_same_width(self, request): + """Should use same width/truncation with same initial width.""" + + def f(): + assert "1234567890" * 5 + "A" == "1234567890" * 5 + "B" + + assert getmsg(f).splitlines()[0] == ( + "assert '123456789012...901234567890A' == '123456789012...901234567890B'" + ) + def test_dont_rewrite_if_hasattr_fails(self, request): class Y: """ A class whos getattr fails, but not with `AttributeError` """ diff --git a/testing/test_config.py b/testing/test_config.py index c1a58848a..fc3659d2a 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,10 +1,11 @@ +import os import sys import textwrap - -import importlib_metadata +from pathlib import Path import _pytest._code import pytest +from _pytest.compat import importlib_metadata from _pytest.config import _iter_rewritable_modules from _pytest.config.exceptions import UsageError from _pytest.config.findpaths import determine_setup @@ -446,7 +447,7 @@ class TestConfigFromdictargs: assert config.option.capture == "no" assert config.args == args - def test_origargs(self, _sys_snapshot): + def test_invocation_params_args(self, _sys_snapshot): """Show that fromdictargs can handle args in their "orig" format""" from _pytest.config import Config @@ -455,7 +456,7 @@ class TestConfigFromdictargs: config = Config.fromdictargs(option_dict, args) assert config.args == ["a", "b"] - assert config._origargs == args + assert config.invocation_params.args == args assert config.option.verbose == 4 assert config.option.capture == "no" @@ -1205,6 +1206,29 @@ def test_config_does_not_load_blocked_plugin_from_args(testdir): assert result.ret == ExitCode.USAGE_ERROR +def test_invocation_args(testdir): + """Ensure that Config.invocation_* arguments are correctly defined""" + + class DummyPlugin: + pass + + p = testdir.makepyfile("def test(): pass") + plugin = DummyPlugin() + rec = testdir.inline_run(p, "-v", plugins=[plugin]) + calls = rec.getcalls("pytest_runtest_protocol") + assert len(calls) == 1 + call = calls[0] + config = call.item.config + + assert config.invocation_params.args == [p, "-v"] + assert config.invocation_params.dir == Path(str(testdir.tmpdir)) + + plugins = config.invocation_params.plugins + assert len(plugins) == 2 + assert plugins[0] is plugin + assert type(plugins[1]).__name__ == "Collect" # installed by testdir.inline_run() + + @pytest.mark.parametrize( "plugin", [ @@ -1248,3 +1272,140 @@ def test_config_blocked_default_plugins(testdir, plugin): result.stdout.fnmatch_lines(["* 1 failed in *"]) else: assert result.stdout.lines == [""] + + +class TestSetupCfg: + def test_pytest_setup_cfg_unsupported(self, testdir): + testdir.makefile( + ".cfg", + setup=""" + [pytest] + addopts = --verbose + """, + ) + with pytest.raises(pytest.fail.Exception): + testdir.runpytest() + + def test_pytest_custom_cfg_unsupported(self, testdir): + testdir.makefile( + ".cfg", + custom=""" + [pytest] + addopts = --verbose + """, + ) + with pytest.raises(pytest.fail.Exception): + testdir.runpytest("-c", "custom.cfg") + + +class TestPytestPluginsVariable: + def test_pytest_plugins_in_non_top_level_conftest_unsupported(self, testdir): + testdir.makepyfile( + **{ + "subdirectory/conftest.py": """ + pytest_plugins=['capture'] + """ + } + ) + testdir.makepyfile( + """ + def test_func(): + pass + """ + ) + res = testdir.runpytest() + assert res.ret == 2 + msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" + res.stdout.fnmatch_lines( + [ + "*{msg}*".format(msg=msg), + "*subdirectory{sep}conftest.py*".format(sep=os.sep), + ] + ) + + @pytest.mark.parametrize("use_pyargs", [True, False]) + def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( + self, testdir, use_pyargs + ): + """When using --pyargs, do not emit the warning about non-top-level conftest warnings (#4039, #4044)""" + + files = { + "src/pkg/__init__.py": "", + "src/pkg/conftest.py": "", + "src/pkg/test_root.py": "def test(): pass", + "src/pkg/sub/__init__.py": "", + "src/pkg/sub/conftest.py": "pytest_plugins=['capture']", + "src/pkg/sub/test_bar.py": "def test(): pass", + } + testdir.makepyfile(**files) + testdir.syspathinsert(testdir.tmpdir.join("src")) + + args = ("--pyargs", "pkg") if use_pyargs else () + res = testdir.runpytest(*args) + assert res.ret == (0 if use_pyargs else 2) + msg = ( + msg + ) = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" + if use_pyargs: + assert msg not in res.stdout.str() + else: + res.stdout.fnmatch_lines(["*{msg}*".format(msg=msg)]) + + def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest( + self, testdir + ): + subdirectory = testdir.tmpdir.join("subdirectory") + subdirectory.mkdir() + testdir.makeconftest( + """ + pytest_plugins=['capture'] + """ + ) + testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py")) + + testdir.makepyfile( + """ + def test_func(): + pass + """ + ) + + res = testdir.runpytest_subprocess() + assert res.ret == 2 + msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" + res.stdout.fnmatch_lines( + [ + "*{msg}*".format(msg=msg), + "*subdirectory{sep}conftest.py*".format(sep=os.sep), + ] + ) + + def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives( + self, testdir + ): + subdirectory = testdir.tmpdir.join("subdirectory") + subdirectory.mkdir() + testdir.makeconftest( + """ + pass + """ + ) + testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py")) + + testdir.makeconftest( + """ + import warnings + warnings.filterwarnings('always', category=DeprecationWarning) + pytest_plugins=['capture'] + """ + ) + testdir.makepyfile( + """ + def test_func(): + pass + """ + ) + res = testdir.runpytest_subprocess() + assert res.ret == 0 + msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" + assert msg not in res.stdout.str() diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 236066673..4aac5432d 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -3,6 +3,7 @@ import textwrap import pytest from _pytest.compat import MODULE_NOT_FOUND_ERROR +from _pytest.doctest import _get_checker from _pytest.doctest import _is_mocked from _pytest.doctest import _patch_unwrap_mock_aware from _pytest.doctest import DoctestItem @@ -838,6 +839,154 @@ class TestLiterals: reprec = testdir.inline_run() reprec.assertoutcome(failed=1) + def test_number_re(self): + for s in [ + "1.", + "+1.", + "-1.", + ".1", + "+.1", + "-.1", + "0.1", + "+0.1", + "-0.1", + "1e5", + "+1e5", + "1e+5", + "+1e+5", + "1e-5", + "+1e-5", + "-1e-5", + "1.2e3", + "-1.2e-3", + ]: + print(s) + m = _get_checker()._number_re.match(s) + assert m is not None + assert float(m.group()) == pytest.approx(float(s)) + for s in ["1", "abc"]: + print(s) + assert _get_checker()._number_re.match(s) is None + + @pytest.mark.parametrize("config_mode", ["ini", "comment"]) + def test_number_precision(self, testdir, config_mode): + """Test the NUMBER option.""" + if config_mode == "ini": + testdir.makeini( + """ + [pytest] + doctest_optionflags = NUMBER + """ + ) + comment = "" + else: + comment = "#doctest: +NUMBER" + + testdir.maketxtfile( + test_doc=""" + + Scalars: + + >>> import math + >>> math.pi {comment} + 3.141592653589793 + >>> math.pi {comment} + 3.1416 + >>> math.pi {comment} + 3.14 + >>> -math.pi {comment} + -3.14 + >>> math.pi {comment} + 3. + >>> 3. {comment} + 3.0 + >>> 3. {comment} + 3. + >>> 3. {comment} + 3.01 + >>> 3. {comment} + 2.99 + >>> .299 {comment} + .3 + >>> .301 {comment} + .3 + >>> 951. {comment} + 1e3 + >>> 1049. {comment} + 1e3 + >>> -1049. {comment} + -1e3 + >>> 1e3 {comment} + 1e3 + >>> 1e3 {comment} + 1000. + + Lists: + + >>> [3.1415, 0.097, 13.1, 7, 8.22222e5, 0.598e-2] {comment} + [3.14, 0.1, 13., 7, 8.22e5, 6.0e-3] + >>> [[0.333, 0.667], [0.999, 1.333]] {comment} + [[0.33, 0.667], [0.999, 1.333]] + >>> [[[0.101]]] {comment} + [[[0.1]]] + + Doesn't barf on non-numbers: + + >>> 'abc' {comment} + 'abc' + >>> None {comment} + """.format( + comment=comment + ) + ) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + + @pytest.mark.parametrize( + "expression,output", + [ + # ints shouldn't match floats: + ("3.0", "3"), + ("3e0", "3"), + ("1e3", "1000"), + ("3", "3.0"), + # Rounding: + ("3.1", "3.0"), + ("3.1", "3.2"), + ("3.1", "4.0"), + ("8.22e5", "810000.0"), + # Only the actual output is rounded up, not the expected output: + ("3.0", "2.98"), + ("1e3", "999"), + # The current implementation doesn't understand that numbers inside + # strings shouldn't be treated as numbers: + pytest.param("'3.1416'", "'3.14'", marks=pytest.mark.xfail), + ], + ) + def test_number_non_matches(self, testdir, expression, output): + testdir.maketxtfile( + test_doc=""" + >>> {expression} #doctest: +NUMBER + {output} + """.format( + expression=expression, output=output + ) + ) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=0, failed=1) + + def test_number_and_allow_unicode(self, testdir): + testdir.maketxtfile( + test_doc=""" + >>> from collections import namedtuple + >>> T = namedtuple('T', 'a b c') + >>> T(a=0.2330000001, b=u'str', c=b'bytes') # doctest: +ALLOW_UNICODE, +ALLOW_BYTES, +NUMBER + T(a=0.233, b=u'str', c='bytes') + """ + ) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + class TestDoctestSkips: """ diff --git a/testing/test_entry_points.py b/testing/test_entry_points.py index 9812ce998..5d0031273 100644 --- a/testing/test_entry_points.py +++ b/testing/test_entry_points.py @@ -1,4 +1,4 @@ -import importlib_metadata +from _pytest.compat import importlib_metadata def test_pytest_entry_points_are_identical(): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index de5be7084..d4a1f6cc3 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,18 +1,47 @@ import os +import platform +from datetime import datetime +from pathlib import Path from xml.dom import minidom import py +import xmlschema import pytest from _pytest.junitxml import LogXML from _pytest.reports import BaseReport -def runandparse(testdir, *args): - resultpath = testdir.tmpdir.join("junit.xml") - result = testdir.runpytest("--junitxml=%s" % resultpath, *args) - xmldoc = minidom.parse(str(resultpath)) - return result, DomNode(xmldoc) +@pytest.fixture(scope="session") +def schema(): + """Returns a xmlschema.XMLSchema object for the junit-10.xsd file""" + fn = Path(__file__).parent / "example_scripts/junit-10.xsd" + with fn.open() as f: + return xmlschema.XMLSchema(f) + + +@pytest.fixture +def run_and_parse(testdir, schema): + """ + Fixture that returns a function that can be used to execute pytest and return + the parsed ``DomNode`` of the root xml node. + + The ``family`` parameter is used to configure the ``junit_family`` of the written report. + "xunit2" is also automatically validated against the schema. + """ + + def run(*args, family="xunit1"): + if family: + args = ("-o", "junit_family=" + family) + args + xml_path = testdir.tmpdir.join("junit.xml") + result = testdir.runpytest("--junitxml=%s" % xml_path, *args) + if family == "xunit2": + with xml_path.open() as f: + schema.validate(f) + xmldoc = minidom.parse(str(xml_path)) + return result, DomNode(xmldoc) + + return run def assert_attr(node, **kwargs): @@ -41,6 +70,16 @@ class DomNode: def _by_tag(self, tag): return self.__node.getElementsByTagName(tag) + @property + def children(self): + return [type(self)(x) for x in self.__node.childNodes] + + @property + def get_unique_child(self): + children = self.children + assert len(children) == 1 + return children[0] + def find_nth_by_tag(self, tag, n): items = self._by_tag(tag) try: @@ -75,12 +114,16 @@ class DomNode: return self.__node.tagName @property - def next_siebling(self): + def next_sibling(self): return type(self)(self.__node.nextSibling) +parametrize_families = pytest.mark.parametrize("xunit_family", ["xunit1", "xunit2"]) + + class TestPython: - def test_summing_simple(self, testdir): + @parametrize_families + def test_summing_simple(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -98,12 +141,13 @@ class TestPython: assert 1 """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(name="pytest", errors=0, failures=1, skipped=2, tests=5) - def test_summing_simple_with_errors(self, testdir): + @parametrize_families + def test_summing_simple_with_errors(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -124,12 +168,38 @@ class TestPython: assert True """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5) - def test_timing_function(self, testdir): + @parametrize_families + def test_hostname_in_xml(self, testdir, run_and_parse, xunit_family): + testdir.makepyfile( + """ + def test_pass(): + pass + """ + ) + result, dom = run_and_parse(family=xunit_family) + node = dom.find_first_by_tag("testsuite") + node.assert_attr(hostname=platform.node()) + + @parametrize_families + def test_timestamp_in_xml(self, testdir, run_and_parse, xunit_family): + testdir.makepyfile( + """ + def test_pass(): + pass + """ + ) + start_time = datetime.now() + result, dom = run_and_parse(family=xunit_family) + node = dom.find_first_by_tag("testsuite") + timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f") + assert start_time <= timestamp < datetime.now() + + def test_timing_function(self, testdir, run_and_parse): testdir.makepyfile( """ import time, pytest @@ -141,14 +211,16 @@ class TestPython: time.sleep(0.01) """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") val = tnode["time"] assert round(float(val), 2) >= 0.03 @pytest.mark.parametrize("duration_report", ["call", "total"]) - def test_junit_duration_report(self, testdir, monkeypatch, duration_report): + def test_junit_duration_report( + self, testdir, monkeypatch, duration_report, run_and_parse + ): # mock LogXML.node_reporter so it always sets a known duration to each test report object original_node_reporter = LogXML.node_reporter @@ -166,8 +238,8 @@ class TestPython: pass """ ) - result, dom = runandparse( - testdir, "-o", "junit_duration_report={}".format(duration_report) + result, dom = run_and_parse( + "-o", "junit_duration_report={}".format(duration_report) ) node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") @@ -178,7 +250,8 @@ class TestPython: assert duration_report == "call" assert val == 1.0 - def test_setup_error(self, testdir): + @parametrize_families + def test_setup_error(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -190,7 +263,7 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) @@ -200,7 +273,8 @@ class TestPython: fnode.assert_attr(message="test setup failure") assert "ValueError" in fnode.toxml() - def test_teardown_error(self, testdir): + @parametrize_families + def test_teardown_error(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -213,7 +287,7 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") @@ -222,7 +296,8 @@ class TestPython: fnode.assert_attr(message="test teardown failure") assert "ValueError" in fnode.toxml() - def test_call_failure_teardown_error(self, testdir): + @parametrize_families + def test_call_failure_teardown_error(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -235,7 +310,7 @@ class TestPython: raise Exception("Call Exception") """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, failures=1, tests=1) @@ -247,7 +322,8 @@ class TestPython: snode = second.find_first_by_tag("error") snode.assert_attr(message="test teardown failure") - def test_skip_contains_name_reason(self, testdir): + @parametrize_families + def test_skip_contains_name_reason(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -255,7 +331,7 @@ class TestPython: pytest.skip("hello23") """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=1) @@ -264,7 +340,8 @@ class TestPython: snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello23") - def test_mark_skip_contains_name_reason(self, testdir): + @parametrize_families + def test_mark_skip_contains_name_reason(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -273,7 +350,7 @@ class TestPython: assert True """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=1) @@ -284,7 +361,10 @@ class TestPython: snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello24") - def test_mark_skipif_contains_name_reason(self, testdir): + @parametrize_families + def test_mark_skipif_contains_name_reason( + self, testdir, run_and_parse, xunit_family + ): testdir.makepyfile( """ import pytest @@ -294,7 +374,7 @@ class TestPython: assert True """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=1) @@ -305,7 +385,10 @@ class TestPython: snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello25") - def test_mark_skip_doesnt_capture_output(self, testdir): + @parametrize_families + def test_mark_skip_doesnt_capture_output( + self, testdir, run_and_parse, xunit_family + ): testdir.makepyfile( """ import pytest @@ -314,12 +397,13 @@ class TestPython: print("bar!") """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node_xml = dom.find_first_by_tag("testsuite").toxml() assert "bar!" not in node_xml - def test_classname_instance(self, testdir): + @parametrize_families + def test_classname_instance(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ class TestClass(object): @@ -327,7 +411,7 @@ class TestPython: assert 0 """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1) @@ -336,20 +420,22 @@ class TestPython: classname="test_classname_instance.TestClass", name="test_method" ) - def test_classname_nested_dir(self, testdir): + @parametrize_families + def test_classname_nested_dir(self, testdir, run_and_parse, xunit_family): p = testdir.tmpdir.ensure("sub", "test_hello.py") p.write("def test_func(): 0/0") - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr(classname="sub.test_hello", name="test_func") - def test_internal_error(self, testdir): + @parametrize_families + def test_internal_error(self, testdir, run_and_parse, xunit_family): testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0") testdir.makepyfile("def test_function(): pass") - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) @@ -360,7 +446,10 @@ class TestPython: assert "Division" in fnode.toxml() @pytest.mark.parametrize("junit_logging", ["no", "system-out", "system-err"]) - def test_failure_function(self, testdir, junit_logging): + @parametrize_families + def test_failure_function( + self, testdir, junit_logging, run_and_parse, xunit_family + ): testdir.makepyfile( """ import logging @@ -375,7 +464,9 @@ class TestPython: """ ) - result, dom = runandparse(testdir, "-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse( + "-o", "junit_logging=%s" % junit_logging, family=xunit_family + ) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1, tests=1) @@ -384,11 +475,11 @@ class TestPython: fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="ValueError: 42") assert "ValueError" in fnode.toxml() - systemout = fnode.next_siebling + systemout = fnode.next_sibling assert systemout.tag == "system-out" assert "hello-stdout" in systemout.toxml() assert "info msg" not in systemout.toxml() - systemerr = systemout.next_siebling + systemerr = systemout.next_sibling assert systemerr.tag == "system-err" assert "hello-stderr" in systemerr.toxml() assert "info msg" not in systemerr.toxml() @@ -403,7 +494,8 @@ class TestPython: assert "warning msg" not in systemout.toxml() assert "warning msg" not in systemerr.toxml() - def test_failure_verbose_message(self, testdir): + @parametrize_families + def test_failure_verbose_message(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import sys @@ -411,14 +503,14 @@ class TestPython: assert 0, "An error" """ ) - - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="AssertionError: An error assert 0") - def test_failure_escape(self, testdir): + @parametrize_families + def test_failure_escape(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -428,7 +520,7 @@ class TestPython: assert 0 """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=3, tests=3) @@ -443,7 +535,8 @@ class TestPython: text = sysout.text assert text == "%s\n" % char - def test_junit_prefixing(self, testdir): + @parametrize_families + def test_junit_prefixing(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ def test_func(): @@ -453,7 +546,7 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir, "--junitprefix=xyz") + result, dom = run_and_parse("--junitprefix=xyz", family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1, tests=2) @@ -464,7 +557,8 @@ class TestPython: classname="xyz.test_junit_prefixing.TestHello", name="test_hello" ) - def test_xfailure_function(self, testdir): + @parametrize_families + def test_xfailure_function(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -472,7 +566,7 @@ class TestPython: pytest.xfail("42") """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert not result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=1, tests=1) @@ -480,9 +574,9 @@ class TestPython: tnode.assert_attr(classname="test_xfailure_function", name="test_xfail") fnode = tnode.find_first_by_tag("skipped") fnode.assert_attr(type="pytest.xfail", message="42") - # assert "ValueError" in fnode.toxml() - def test_xfailure_marker(self, testdir): + @parametrize_families + def test_xfailure_marker(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -491,7 +585,7 @@ class TestPython: assert False """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert not result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=1, tests=1) @@ -500,7 +594,7 @@ class TestPython: fnode = tnode.find_first_by_tag("skipped") fnode.assert_attr(type="pytest.xfail", message="42") - def test_xfail_captures_output_once(self, testdir): + def test_xfail_captures_output_once(self, testdir, run_and_parse): testdir.makepyfile( """ import sys @@ -513,13 +607,14 @@ class TestPython: assert 0 """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") assert len(tnode.find_by_tag("system-err")) == 1 assert len(tnode.find_by_tag("system-out")) == 1 - def test_xfailure_xpass(self, testdir): + @parametrize_families + def test_xfailure_xpass(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -528,14 +623,15 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) # assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=0, tests=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr(classname="test_xfailure_xpass", name="test_xpass") - def test_xfailure_xpass_strict(self, testdir): + @parametrize_families + def test_xfailure_xpass_strict(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -544,7 +640,7 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) # assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=0, tests=1) @@ -553,9 +649,10 @@ class TestPython: fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="[XPASS(strict)] This needs to fail!") - def test_collect_error(self, testdir): + @parametrize_families + def test_collect_error(self, testdir, run_and_parse, xunit_family): testdir.makepyfile("syntax error") - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) @@ -564,7 +661,7 @@ class TestPython: fnode.assert_attr(message="collection failure") assert "SyntaxError" in fnode.toxml() - def test_unicode(self, testdir): + def test_unicode(self, testdir, run_and_parse): value = "hx\xc4\x85\xc4\x87\n" testdir.makepyfile( """\ @@ -575,14 +672,14 @@ class TestPython: """ % value ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() assert result.ret == 1 tnode = dom.find_first_by_tag("testcase") fnode = tnode.find_first_by_tag("failure") assert "hx" in fnode.toxml() - def test_assertion_binchars(self, testdir): - """this test did fail when the escaping wasn't strict""" + def test_assertion_binchars(self, testdir, run_and_parse): + """this test did fail when the escaping wasnt strict""" testdir.makepyfile( """ @@ -593,23 +690,23 @@ class TestPython: assert M1 == M2 """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() print(dom.toxml()) - def test_pass_captures_stdout(self, testdir): + def test_pass_captures_stdout(self, testdir, run_and_parse): testdir.makepyfile( """ def test_pass(): print('hello-stdout') """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-out") assert "hello-stdout" in systemout.toxml() - def test_pass_captures_stderr(self, testdir): + def test_pass_captures_stderr(self, testdir, run_and_parse): testdir.makepyfile( """ import sys @@ -617,13 +714,13 @@ class TestPython: sys.stderr.write('hello-stderr') """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-err") assert "hello-stderr" in systemout.toxml() - def test_setup_error_captures_stdout(self, testdir): + def test_setup_error_captures_stdout(self, testdir, run_and_parse): testdir.makepyfile( """ import pytest @@ -636,13 +733,13 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-out") assert "hello-stdout" in systemout.toxml() - def test_setup_error_captures_stderr(self, testdir): + def test_setup_error_captures_stderr(self, testdir, run_and_parse): testdir.makepyfile( """ import sys @@ -656,13 +753,13 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-err") assert "hello-stderr" in systemout.toxml() - def test_avoid_double_stdout(self, testdir): + def test_avoid_double_stdout(self, testdir, run_and_parse): testdir.makepyfile( """ import sys @@ -677,7 +774,7 @@ class TestPython: sys.stdout.write('hello-stdout call') """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-out") @@ -720,7 +817,8 @@ def test_dont_configure_on_slaves(tmpdir): class TestNonPython: - def test_summing_simple(self, testdir): + @parametrize_families + def test_summing_simple(self, testdir, run_and_parse, xunit_family): testdir.makeconftest( """ import pytest @@ -738,7 +836,7 @@ class TestNonPython: """ ) testdir.tmpdir.join("myfile.xyz").write("hello") - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=0, failures=1, skipped=0, tests=1) @@ -786,8 +884,8 @@ def test_nullbyte_replace(testdir): def test_invalid_xml_escape(): # Test some more invalid xml chars, the full range should be - # tested really but let's just thest the edges of the ranges - # intead. + # tested really but let's just test the edges of the ranges + # instead. # XXX This only tests low unicode character points for now as # there are some issues with the testing infrastructure for # the higher ones. @@ -871,7 +969,7 @@ def test_logxml_check_isdir(testdir): result.stderr.fnmatch_lines(["*--junitxml must be a filename*"]) -def test_escaped_parametrized_names_xml(testdir): +def test_escaped_parametrized_names_xml(testdir, run_and_parse): testdir.makepyfile( """\ import pytest @@ -880,13 +978,13 @@ def test_escaped_parametrized_names_xml(testdir): assert char """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() assert result.ret == 0 node = dom.find_first_by_tag("testcase") node.assert_attr(name="test_func[\\x00]") -def test_double_colon_split_function_issue469(testdir): +def test_double_colon_split_function_issue469(testdir, run_and_parse): testdir.makepyfile( """ import pytest @@ -895,14 +993,14 @@ def test_double_colon_split_function_issue469(testdir): pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() assert result.ret == 0 node = dom.find_first_by_tag("testcase") node.assert_attr(classname="test_double_colon_split_function_issue469") node.assert_attr(name="test_func[double::colon]") -def test_double_colon_split_method_issue469(testdir): +def test_double_colon_split_method_issue469(testdir, run_and_parse): testdir.makepyfile( """ import pytest @@ -912,7 +1010,7 @@ def test_double_colon_split_method_issue469(testdir): pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() assert result.ret == 0 node = dom.find_first_by_tag("testcase") node.assert_attr(classname="test_double_colon_split_method_issue469.TestClass") @@ -948,7 +1046,7 @@ def test_unicode_issue368(testdir): log.pytest_sessionfinish() -def test_record_property(testdir): +def test_record_property(testdir, run_and_parse): testdir.makepyfile( """ import pytest @@ -960,7 +1058,7 @@ def test_record_property(testdir): record_property("foo", "<1"); """ ) - result, dom = runandparse(testdir, "-rwv") + result, dom = run_and_parse("-rwv") node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") psnode = tnode.find_first_by_tag("properties") @@ -969,7 +1067,7 @@ def test_record_property(testdir): pnodes[1].assert_attr(name="foo", value="<1") -def test_record_property_same_name(testdir): +def test_record_property_same_name(testdir, run_and_parse): testdir.makepyfile( """ def test_record_with_same_name(record_property): @@ -977,7 +1075,7 @@ def test_record_property_same_name(testdir): record_property("foo", "baz") """ ) - result, dom = runandparse(testdir, "-rw") + result, dom = run_and_parse("-rw") node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") psnode = tnode.find_first_by_tag("properties") @@ -1001,7 +1099,7 @@ def test_record_fixtures_without_junitxml(testdir, fixture_name): @pytest.mark.filterwarnings("default") -def test_record_attribute(testdir): +def test_record_attribute(testdir, run_and_parse): testdir.makeini( """ [pytest] @@ -1019,7 +1117,7 @@ def test_record_attribute(testdir): record_xml_attribute("foo", "<1"); """ ) - result, dom = runandparse(testdir, "-rw") + result, dom = run_and_parse("-rw") node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") tnode.assert_attr(bar="1") @@ -1031,7 +1129,7 @@ def test_record_attribute(testdir): @pytest.mark.filterwarnings("default") @pytest.mark.parametrize("fixture_name", ["record_xml_attribute", "record_property"]) -def test_record_fixtures_xunit2(testdir, fixture_name): +def test_record_fixtures_xunit2(testdir, fixture_name, run_and_parse): """Ensure record_xml_attribute and record_property drop values when outside of legacy family """ testdir.makeini( @@ -1054,7 +1152,7 @@ def test_record_fixtures_xunit2(testdir, fixture_name): ) ) - result, dom = runandparse(testdir, "-rw") + result, dom = run_and_parse("-rw", family=None) expected_lines = [] if fixture_name == "record_xml_attribute": expected_lines.append( @@ -1069,7 +1167,7 @@ def test_record_fixtures_xunit2(testdir, fixture_name): result.stdout.fnmatch_lines(expected_lines) -def test_random_report_log_xdist(testdir, monkeypatch): +def test_random_report_log_xdist(testdir, monkeypatch, run_and_parse): """xdist calls pytest_runtest_logreport as they are executed by the slaves, with nodes from several nodes overlapping, so junitxml must cope with that to produce correct reports. #1064 @@ -1084,7 +1182,7 @@ def test_random_report_log_xdist(testdir, monkeypatch): assert i != 22 """ ) - _, dom = runandparse(testdir, "-n2") + _, dom = run_and_parse("-n2") suite_node = dom.find_first_by_tag("testsuite") failed = [] for case_node in suite_node.find_by_tag("testcase"): @@ -1094,7 +1192,22 @@ def test_random_report_log_xdist(testdir, monkeypatch): assert failed == ["test_x[22]"] -def test_runs_twice(testdir): +@parametrize_families +def test_root_testsuites_tag(testdir, run_and_parse, xunit_family): + testdir.makepyfile( + """ + def test_x(): + pass + """ + ) + _, dom = run_and_parse(family=xunit_family) + root = dom.get_unique_child + assert root.tag == "testsuites" + suite_node = root.get_unique_child + assert suite_node.tag == "testsuite" + + +def test_runs_twice(testdir, run_and_parse): f = testdir.makepyfile( """ def test_pass(): @@ -1102,14 +1215,13 @@ def test_runs_twice(testdir): """ ) - result, dom = runandparse(testdir, f, f) + result, dom = run_and_parse(f, f) assert "INTERNALERROR" not in result.stdout.str() first, second = [x["classname"] for x in dom.find_by_tag("testcase")] assert first == second -@pytest.mark.xfail(reason="hangs", run=False) -def test_runs_twice_xdist(testdir): +def test_runs_twice_xdist(testdir, run_and_parse): pytest.importorskip("xdist") f = testdir.makepyfile( """ @@ -1118,13 +1230,13 @@ def test_runs_twice_xdist(testdir): """ ) - result, dom = runandparse(testdir, f, "--dist", "each", "--tx", "2*popen") + result, dom = run_and_parse(f, "--dist", "each", "--tx", "2*popen") assert "INTERNALERROR" not in result.stdout.str() first, second = [x["classname"] for x in dom.find_by_tag("testcase")] assert first == second -def test_fancy_items_regression(testdir): +def test_fancy_items_regression(testdir, run_and_parse): # issue 1259 testdir.makeconftest( """ @@ -1157,7 +1269,7 @@ def test_fancy_items_regression(testdir): """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() assert "INTERNALERROR" not in result.stdout.str() @@ -1176,9 +1288,10 @@ def test_fancy_items_regression(testdir): ] -def test_global_properties(testdir): +@parametrize_families +def test_global_properties(testdir, xunit_family): path = testdir.tmpdir.join("test_global_properties.xml") - log = LogXML(str(path), None) + log = LogXML(str(path), None, family=xunit_family) class Report(BaseReport): sections = [] @@ -1236,7 +1349,8 @@ def test_url_property(testdir): ), "The URL did not get written to the xml" -def test_record_testsuite_property(testdir): +@parametrize_families +def test_record_testsuite_property(testdir, run_and_parse, xunit_family): testdir.makepyfile( """ def test_func1(record_testsuite_property): @@ -1246,7 +1360,7 @@ def test_record_testsuite_property(testdir): record_testsuite_property("stats", 10) """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") properties_node = node.find_first_by_tag("properties") @@ -1284,14 +1398,16 @@ def test_record_testsuite_property_type_checking(testdir, junit): @pytest.mark.parametrize("suite_name", ["my_suite", ""]) -def test_set_suite_name(testdir, suite_name): +@parametrize_families +def test_set_suite_name(testdir, suite_name, run_and_parse, xunit_family): if suite_name: testdir.makeini( """ [pytest] - junit_suite_name={} + junit_suite_name={suite_name} + junit_family={family} """.format( - suite_name + suite_name=suite_name, family=xunit_family ) ) expected = suite_name @@ -1305,13 +1421,13 @@ def test_set_suite_name(testdir, suite_name): pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") node.assert_attr(name=expected) -def test_escaped_skipreason_issue3533(testdir): +def test_escaped_skipreason_issue3533(testdir, run_and_parse): testdir.makepyfile( """ import pytest @@ -1320,20 +1436,26 @@ def test_escaped_skipreason_issue3533(testdir): pass """ ) - _, dom = runandparse(testdir) + _, dom = run_and_parse() node = dom.find_first_by_tag("testcase") snode = node.find_first_by_tag("skipped") assert "1 <> 2" in snode.text snode.assert_attr(message="1 <> 2") -def test_logging_passing_tests_disabled_does_not_log_test_output(testdir): +@parametrize_families +def test_logging_passing_tests_disabled_does_not_log_test_output( + testdir, run_and_parse, xunit_family +): testdir.makeini( """ [pytest] junit_log_passing_tests=False junit_logging=system-out - """ + junit_family={family} + """.format( + family=xunit_family + ) ) testdir.makepyfile( """ @@ -1347,7 +1469,7 @@ def test_logging_passing_tests_disabled_does_not_log_test_output(testdir): logging.warning('hello') """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testcase") assert len(node.find_by_tag("system-err")) == 0 diff --git a/testing/test_mark.py b/testing/test_mark.py index 273ba2298..c8d5851ac 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -8,12 +8,6 @@ from _pytest.mark import EMPTY_PARAMETERSET_OPTION from _pytest.mark import MarkGenerator as Mark from _pytest.nodes import Collector from _pytest.nodes import Node -from _pytest.warning_types import PytestDeprecationWarning -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG - -ignore_markinfo = pytest.mark.filterwarnings( - "ignore:MarkInfo objects:pytest.RemovedInPytest4Warning" -) class TestMark: @@ -25,7 +19,8 @@ class TestMark: def test_pytest_mark_notcallable(self): mark = Mark() - pytest.raises((AttributeError, TypeError), mark) + with pytest.raises(TypeError): + mark() def test_mark_with_param(self): def some_function(abc): @@ -625,7 +620,6 @@ class TestFunctional: reprec = testdir.inline_run() reprec.assertoutcome(passed=1) - @ignore_markinfo def test_keyword_added_for_session(self, testdir): testdir.makeconftest( """ @@ -651,7 +645,7 @@ class TestFunctional: assert marker.kwargs == {} """ ) - reprec = testdir.inline_run("-m", "mark1", SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run("-m", "mark1") reprec.assertoutcome(passed=1) def assert_markers(self, items, **expected): @@ -689,7 +683,7 @@ class TestFunctional: assert True """ ) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(skipped=1) @@ -989,7 +983,7 @@ def test_markers_from_parametrize(testdir): """ ) - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest() result.assert_outcomes(passed=4) @@ -1003,15 +997,3 @@ def test_pytest_param_id_requires_string(): @pytest.mark.parametrize("s", (None, "hello world")) def test_pytest_param_id_allows_none_or_string(s): assert pytest.param(id=s) - - -def test_pytest_param_warning_on_unknown_kwargs(): - with pytest.warns(PytestDeprecationWarning) as warninfo: - # typo, should be marks= - pytest.param(1, 2, mark=pytest.mark.xfail()) - assert warninfo[0].filename == __file__ - msg, = warninfo[0].message.args - assert msg == ( - "pytest.param() got unexpected keyword arguments: ['mark'].\n" - "This will be an error in future versions." - ) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index ba3135f65..d330ff253 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -72,8 +72,7 @@ def test_make_hook_recorder(testdir): def test_parseconfig(testdir): config1 = testdir.parseconfig() config2 = testdir.parseconfig() - assert config2 != config1 - assert config1 != pytest.config + assert config2 is not config1 def test_testdir_runs_with_plugin(testdir): @@ -279,7 +278,7 @@ def test_assert_outcomes_after_pytest_error(testdir): testdir.makepyfile("def test_foo(): assert True") result = testdir.runpytest("--unexpected-argument") - with pytest.raises(ValueError, match="Pytest terminal report not found"): + with pytest.raises(ValueError, match="Pytest terminal summary report not found"): result.assert_outcomes(passed=0) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 1c68b3787..208dc5b44 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -3,7 +3,6 @@ import warnings import pytest from _pytest.recwarn import WarningsRecorder -from _pytest.warning_types import PytestDeprecationWarning def test_recwarn_stacklevel(recwarn): @@ -206,22 +205,17 @@ class TestDeprecatedCall: class TestWarns: - def test_strings(self): + def test_check_callable(self): + source = "warnings.warn('w1', RuntimeWarning)" + with pytest.raises(TypeError, match=r".* must be callable"): + pytest.warns(RuntimeWarning, source) + + def test_several_messages(self): # different messages, b/c Python suppresses multiple identical warnings - source1 = "warnings.warn('w1', RuntimeWarning)" - source2 = "warnings.warn('w2', RuntimeWarning)" - source3 = "warnings.warn('w3', RuntimeWarning)" - with pytest.warns(PytestDeprecationWarning) as warninfo: # yo dawg - pytest.warns(RuntimeWarning, source1) - pytest.raises( - pytest.fail.Exception, lambda: pytest.warns(UserWarning, source2) - ) - pytest.warns(RuntimeWarning, source3) - assert len(warninfo) == 3 - for w in warninfo: - assert w.filename == __file__ - msg, = w.message.args - assert msg.startswith("warns(..., 'code(as_a_string)') is deprecated") + pytest.warns(RuntimeWarning, lambda: warnings.warn("w1", RuntimeWarning)) + with pytest.raises(pytest.fail.Exception): + pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning)) + pytest.warns(RuntimeWarning, lambda: warnings.warn("w3", RuntimeWarning)) def test_function(self): pytest.warns( @@ -380,3 +374,9 @@ class TestWarns: assert f() == 10 assert pytest.warns(UserWarning, f) == 10 assert pytest.warns(UserWarning, f) == 10 + + def test_warns_context_manager_with_kwargs(self): + with pytest.raises(TypeError) as excinfo: + with pytest.warns(UserWarning, foo="bar"): + pass + assert "Unexpected keyword arguments" in str(excinfo.value) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 381a5b2e1..88f96f894 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -617,7 +617,7 @@ class TestTerminalFunctional: pluggy.__version__, ), "*test_header_trailer_info.py .*", - "=* 1 passed*in *.[0-9][0-9] seconds *=", + "=* 1 passed*in *.[0-9][0-9]s *=", ] ) if request.config.pluginmanager.list_plugin_distinfo(): @@ -1678,3 +1678,20 @@ def test_line_with_reprcrash(monkeypatch): check("😄😄😄😄😄\n2nd line", 41, "FAILED nodeid::😄::withunicode - 😄😄...") check("😄😄😄😄😄\n2nd line", 42, "FAILED nodeid::😄::withunicode - 😄😄😄...") check("😄😄😄😄😄\n2nd line", 80, "FAILED nodeid::😄::withunicode - 😄😄😄😄😄") + + +@pytest.mark.parametrize( + "seconds, expected", + [ + (10.0, "10.00s"), + (10.34, "10.34s"), + (59.99, "59.99s"), + (60.55, "60.55s (0:01:00)"), + (123.55, "123.55s (0:02:03)"), + (60 * 60 + 0.5, "3600.50s (1:00:00)"), + ], +) +def test_format_session_duration(seconds, expected): + from _pytest.terminal import format_session_duration + + assert format_session_duration(seconds) == expected diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 2c6454b0c..ebde9044c 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -7,7 +7,6 @@ import attr import pytest from _pytest import pathlib from _pytest.pathlib import Path -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG def test_tmpdir_fixture(testdir): @@ -16,13 +15,6 @@ def test_tmpdir_fixture(testdir): results.stdout.fnmatch_lines(["*1 passed*"]) -def test_ensuretemp(recwarn): - d1 = pytest.ensuretemp("hello") - d2 = pytest.ensuretemp("hello") - assert d1 == d2 - assert d1.check(dir=1) - - @attr.s class FakeConfig: basetemp = attr.ib() @@ -87,12 +79,13 @@ def test_basetemp(testdir): p = testdir.makepyfile( """ import pytest - def test_1(): - pytest.ensuretemp("hello") + def test_1(tmpdir_factory): + tmpdir_factory.mktemp('hello', numbered=False) """ ) - result = testdir.runpytest(p, "--basetemp=%s" % mytemp, SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest(p, "--basetemp=%s" % mytemp) assert result.ret == 0 + print(mytemp) assert mytemp.join("hello").check() diff --git a/testing/test_unittest.py b/testing/test_unittest.py index ec5f92e18..9b1b688ff 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -939,9 +939,7 @@ def test_class_method_containing_test_issue1558(testdir): reprec.assertoutcome(passed=1) -@pytest.mark.parametrize( - "base", ["builtins.object", "unittest.TestCase", "unittest2.TestCase"] -) +@pytest.mark.parametrize("base", ["builtins.object", "unittest.TestCase"]) def test_usefixtures_marker_on_unittest(base, testdir): """#3498""" module = base.rsplit(".", 1)[0] diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 4580ab53f..077636c52 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -498,38 +498,15 @@ class TestDeprecationWarningsByDefault: @pytest.mark.parametrize("change_default", [None, "ini", "cmdline"]) -def test_removed_in_pytest4_warning_as_error(testdir, change_default): - testdir.makepyfile( - """ - import warnings, pytest - def test(): - warnings.warn(pytest.RemovedInPytest4Warning("some warning")) - """ - ) - if change_default == "ini": - testdir.makeini( - """ - [pytest] - filterwarnings = - ignore::pytest.RemovedInPytest4Warning - """ - ) - - args = ( - ("-Wignore::pytest.RemovedInPytest4Warning",) - if change_default == "cmdline" - else () - ) - result = testdir.runpytest(*args) - if change_default is None: - result.stdout.fnmatch_lines(["* 1 failed in *"]) - else: - assert change_default in ("ini", "cmdline") - result.stdout.fnmatch_lines(["* 1 passed in *"]) - - -@pytest.mark.parametrize("change_default", [None, "ini", "cmdline"]) +@pytest.mark.skip( + reason="This test should be enabled again before pytest 6.0 is released" +) def test_deprecation_warning_as_error(testdir, change_default): + """This ensures that PytestDeprecationWarnings raised by pytest are turned into errors. + + This test should be enabled as part of each major release, and skipped again afterwards + to ensure our deprecations are turning into warnings as expected. + """ testdir.makepyfile( """ import warnings, pytest diff --git a/tox.ini b/tox.ini index 52d400524..6467ddacd 100644 --- a/tox.ini +++ b/tox.ini @@ -45,7 +45,6 @@ deps = pexpect: pexpect pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master twisted: twisted - twisted: unittest2 xdist: pytest-xdist>=1.13 {env:_PYTEST_TOX_EXTRA_DEP:} platform = {env:_PYTEST_TOX_PLATFORM:.*} @@ -115,6 +114,17 @@ deps = wheel commands = python scripts/release.py {posargs} +[testenv:publish_gh_release_notes] +description = create GitHub release after deployment +basepython = python3.6 +usedevelop = True +passenv = GH_RELEASE_NOTES_TOKEN TRAVIS_TAG +deps = + github3.py + pypandoc +commands = python scripts/publish_gh_release_notes.py + + [pytest] minversion = 2.0 addopts = -ra -p pytester --strict-markers @@ -128,9 +138,6 @@ norecursedirs = testing/example_scripts xfail_strict=true filterwarnings = error - ignore:yield tests are deprecated, and scheduled to be removed in pytest 4.0:pytest.RemovedInPytest4Warning - ignore:Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0:pytest.RemovedInPytest4Warning - ignore::pytest.RemovedInPytest4Warning default:Using or importing the ABCs:DeprecationWarning:unittest2.* 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). @@ -160,7 +167,7 @@ markers = [flake8] max-line-length = 120 -ignore = E203,W503 +extend-ignore = E203 [isort] ; This config mimics what reorder-python-imports does.