diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f516959bc..7f9aa9556 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,15 +2,22 @@ Thanks for submitting a PR, your contribution is really appreciated! Here is a quick checklist that should be present in PRs. -(please delete this text from the final description, this is just a guideline) ---> - [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes. - [ ] 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. -Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please: +Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please: - [ ] Create a new changelog file in the `changelog` folder, with a name like `..rst`. See [changelog/README.rst](https://github.com/pytest-dev/pytest/blob/master/changelog/README.rst) for details. -- [ ] Add yourself to `AUTHORS` in alphabetical order; + + Write sentences in the **past or present tense**, examples: + + * *Improved verbose diff output with sequences.* + * *Terminal summary statistics now use multiple colors.* + + Also make sure to end the sentence with a `.`. + +- [ ] Add yourself to `AUTHORS` in alphabetical order. +--> diff --git a/.gitignore b/.gitignore index 27bd93c7b..fc61c6ee6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ dist/ issue/ env/ .env/ +.venv/ 3rdparty/ .tox .cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8481848f7..9548cd079 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ exclude: doc/en/example/py2py3/test_py2.py repos: - repo: https://github.com/psf/black - rev: 19.3b0 + rev: 19.10b0 hooks: - id: black args: [--safe, --quiet] @@ -37,12 +37,8 @@ repos: hooks: - id: pyupgrade args: [--py3-plus] -- repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.4.0 - hooks: - - id: rst-backticks - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.740 + rev: v0.750 hooks: - id: mypy files: ^(src/|testing/) diff --git a/.travis.yml b/.travis.yml index 310d7093b..e3edbfe9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -108,7 +108,7 @@ before_script: export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess fi -script: tox -vv +script: tox after_success: - | diff --git a/AUTHORS b/AUTHORS index 763d904a4..a3e526c5a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -70,6 +70,7 @@ Daniel Hahler Daniel Nuri Daniel Wandschneider Danielle Jenkins +Daniil Galiev Dave Hunt David Díaz-Barquero David Mohr @@ -134,6 +135,7 @@ Jordan Guymon Jordan Moldow Jordan Speicher Joseph Hunkeler +Josh Karpel Joshua Bronson Jurko Gospodnetić Justyna Janczyszyn @@ -163,6 +165,7 @@ Marcelo Duarte Trevisani Marcin Bachry Marco Gorelli Mark Abramowitz +Mark Dickinson Markus Unterwaditzer Martijn Faassen Martin Altmayer @@ -204,6 +207,7 @@ Oscar Benjamin Patrick Hayes Paweł Adamczak Pedro Algarvio +Philipp Loose Pieter Mulder Piotr Banaszkiewicz Pulkit Goyal @@ -263,6 +267,7 @@ Virgil Dupras Vitaly Lashmanov Vlad Dragos Volodymyr Piskun +Wei Lin Wil Cooley William Lee Wim Glenn diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 375b5dabf..afa4b4f7c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,238 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.3.0 (2019-11-19) +========================= + +Deprecations +------------ + +- `#6179 `_: The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given + that this is the version supported by default in modern tools that manipulate this type of file. + + In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option + is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``. + + For more information, `see the docs `__. + + + +Features +-------- + +- `#4488 `_: The pytest team has created the `pytest-reportlog `__ + plugin, which provides a new ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes. + + Each line of the report log contains a self contained JSON object corresponding to a testing event, + such as a collection or a test result report. The file is guaranteed to be flushed after writing + each line, so systems can read and process events in real-time. + + The plugin is meant to replace the ``--resultlog`` option, which is deprecated and meant to be removed + in a future release. If you use ``--resultlog``, please try out ``pytest-reportlog`` and + provide feedback. + + +- `#4730 `_: When ``sys.pycache_prefix`` (Python 3.8+) is set, it will be used by pytest to cache test files changed by the assertion rewriting mechanism. + + This makes it easier to benefit of cached ``.pyc`` files even on file systems without permissions. + + +- `#5515 `_: Allow selective auto-indentation of multiline log messages. + + Adds command line option ``--log-auto-indent``, config option + ``log_auto_indent`` and support for per-entry configuration of + indentation behavior on calls to ``logging.log()``. + + Alters the default for auto-indention from ``on`` to ``off``. This + restores the older behavior that existed prior to v4.6.0. This + reversion to earlier behavior was done because it is better to + activate new features that may lead to broken tests explicitly + rather than implicitly. + + +- `#5914 `_: ``pytester`` learned two new functions, `no_fnmatch_line `_ and + `no_re_match_line `_. + + The functions are used to ensure the captured text *does not* match the given + pattern. + + The previous idiom was to use ``re.match``: + + .. code-block:: python + + assert re.match(pat, result.stdout.str()) is None + + Or the ``in`` operator: + + .. code-block:: python + + assert text in result.stdout.str() + + But the new functions produce best output on failure. + + +- `#6057 `_: Added tolerances to complex values when printing ``pytest.approx``. + + For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle. + + +- `#6061 `_: Added the pluginmanager as an argument to ``pytest_addoption`` + so that hooks can be invoked when setting up command line options. This is + useful for having one plugin communicate things to another plugin, + such as default values or which set of command line options to add. + + + +Improvements +------------ + +- `#5061 `_: Use multiple colors with terminal summary statistics. + + +- `#5630 `_: Quitting from debuggers is now properly handled in ``doctest`` items. + + +- `#5924 `_: Improved verbose diff output with sequences. + + Before: + + :: + + E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] + E Right contains 3 more items, first extra item: ' ' + E Full diff: + E - ['version', 'version_info', 'sys.version', 'sys.version_info'] + E + ['version', + E + 'version_info', + E + 'sys.version', + E + 'sys.version_info', + E + ' ', + E + 'sys.version', + E + 'sys.version_info'] + + After: + + :: + + E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] + E Right contains 3 more items, first extra item: ' ' + E Full diff: + E [ + E 'version', + E 'version_info', + E 'sys.version', + E 'sys.version_info', + E + ' ', + E + 'sys.version', + E + 'sys.version_info', + E ] + + +- `#5934 `_: ``repr`` of ``ExceptionInfo`` objects has been improved to honor the ``__repr__`` method of the underlying exception. + +- `#5936 `_: Display untruncated assertion message with ``-vv``. + + +- `#5990 `_: Fixed plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors"). + + +- `#6008 `_: ``Config.InvocationParams.args`` is now always a ``tuple`` to better convey that it should be + immutable and avoid accidental modifications. + + +- `#6023 `_: ``pytest.main`` returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). + + +- `#6026 `_: Align prefixes in output of pytester's ``LineMatcher``. + + +- `#6059 `_: Collection errors are reported as errors (and not failures like before) in the terminal's short test summary. + + +- `#6069 `_: ``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally. + + +- `#6097 `_: The "[...%]" indicator in the test summary is now colored according to the final (new) multi-colored line's main color. + + +- `#6116 `_: Added ``--co`` as a synonym to ``--collect-only``. + + +- `#6148 `_: ``atomicwrites`` is now only used on Windows, fixing a performance regression with assertion rewriting on Unix. + + +- `#6152 `_: Now parametrization will use the ``__name__`` attribute of any object for the id, if present. Previously it would only use ``__name__`` for functions and classes. + + +- `#6176 `_: Improved failure reporting with pytester's ``Hookrecorder.assertoutcome``. + + +- `#6181 `_: The reason for a stopped session, e.g. with ``--maxfail`` / ``-x``, now gets reported in the test summary. + + +- `#6206 `_: Improved ``cache.set`` robustness and performance. + + + +Bug Fixes +--------- + +- `#2049 `_: Fixed ``--setup-plan`` showing inaccurate information about fixture lifetimes. + + +- `#2548 `_: Fixed line offset mismatch of skipped tests in terminal summary. + + +- `#6039 `_: The ``PytestDoctestRunner`` is now properly invalidated when unconfiguring the doctest plugin. + + This is important when used with ``pytester``'s ``runpytest_inprocess``. + + +- `#6047 `_: BaseExceptions are now handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. + + +- `#6074 `_: pytester: fixed order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``. + + +- `#6189 `_: Fixed result of ``getmodpath`` method. + + + +Trivial/Internal Changes +------------------------ + +- `#4901 `_: ``RunResult`` from ``pytester`` now displays the mnemonic of the ``ret`` attribute when it is a + valid ``pytest.ExitCode`` value. + + +pytest 5.2.4 (2019-11-15) +========================= + +Bug Fixes +--------- + +- `#6194 `_: Fix incorrect discovery of non-test ``__init__.py`` files. + + +- `#6197 `_: Revert "The first test in a package (``__init__.py``) marked with ``@pytest.mark.skip`` is now correctly skipped.". + + +pytest 5.2.3 (2019-11-14) +========================= + +Bug Fixes +--------- + +- `#5830 `_: The first test in a package (``__init__.py``) marked with ``@pytest.mark.skip`` is now correctly skipped. + + +- `#6099 `_: Fix ``--trace`` when used with parametrized functions. + + +- `#6183 `_: Using ``request`` as a parameter name in ``@pytest.mark.parametrize`` now produces a more + user-friendly error. + + pytest 5.2.2 (2019-10-24) ========================= @@ -1873,7 +2105,8 @@ Features live-logging is enabled and/or when they are logged to a file. -- `#3985 `_: Introduce ``tmp_path`` as a fixture providing a Path object. +- `#3985 `_: Introduce ``tmp_path`` as a fixture providing a Path object. Also introduce ``tmp_path_factory`` as + a session-scoped fixture for creating arbitrary temporary directories from any other fixture or test. - `#4013 `_: Deprecation warnings are now shown even if you customize the warnings filters yourself. In the previous version @@ -3462,7 +3695,7 @@ Deprecations and Removals - ``pytest.approx`` no longer supports ``>``, ``>=``, ``<`` and ``<=`` operators to avoid surprising/inconsistent behavior. See `the approx docs - `_ for more + `_ for more information. (`#2003 `_) - All old-style specific behavior in current classes in the pytest's API is @@ -4819,7 +5052,7 @@ time or change existing behaviors in order to make them less surprising/more use * Fix (`#1422`_): junit record_xml_property doesn't allow multiple records with same name. -.. _`traceback style docs`: https://pytest.org/latest/usage.html#modifying-python-traceback-printing +.. _`traceback style docs`: https://pytest.org/en/latest/usage.html#modifying-python-traceback-printing .. _#1609: https://github.com/pytest-dev/pytest/issues/1609 .. _#1422: https://github.com/pytest-dev/pytest/issues/1422 @@ -5337,7 +5570,7 @@ time or change existing behaviors in order to make them less surprising/more use - add ability to set command line options by environment variable PYTEST_ADDOPTS. - added documentation on the new pytest-dev teams on bitbucket and - github. See https://pytest.org/latest/contributing.html . + github. See https://pytest.org/en/latest/contributing.html . Thanks to Anatoly for pushing and initial work on this. - fix issue650: new option ``--docttest-ignore-import-errors`` which @@ -6078,7 +6311,7 @@ Bug fixes: - yielded test functions will now have autouse-fixtures active but cannot accept fixtures as funcargs - it's anyway recommended to rather use the post-2.0 parametrize features instead of yield, see: - http://pytest.org/latest/example/parametrize.html + http://pytest.org/en/latest/example/parametrize.html - fix autouse-issue where autouse-fixtures would not be discovered if defined in an a/conftest.py file and tests in a/tests/test_some.py - fix issue226 - LIFO ordering for fixture teardowns @@ -6211,7 +6444,7 @@ Bug fixes: - pluginmanager.register(...) now raises ValueError if the plugin has been already registered or the name is taken -- fix issue159: improve http://pytest.org/latest/faq.html +- fix issue159: improve http://pytest.org/en/latest/faq.html especially with respect to the "magic" history, also mention pytest-django, trial and unittest integration. @@ -6324,7 +6557,7 @@ Bug fixes: or through plugin hooks. Also introduce a "--strict" option which will treat unregistered markers as errors allowing to avoid typos and maintain a well described set of markers - for your test suite. See exaples at http://pytest.org/latest/mark.html + for your test suite. See exaples at http://pytest.org/en/latest/mark.html and its links. - issue50: introduce "-m marker" option to select tests based on markers (this is a stricter and more predictable version of '-k' in that "-m" @@ -6507,7 +6740,7 @@ Bug fixes: - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output - introduce a mechanism to prevent/unregister plugins from the - command line, see http://pytest.org/plugins.html#cmdunregister + command line, see http://pytest.org/en/latest/plugins.html#cmdunregister - activate resultlog plugin by default - fix regression wrt yielded tests which due to the collection-before-running semantics were not diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8e59191ab..a3ae731e4 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -262,6 +262,19 @@ Here is a simple overview, with pytest-specific bits: When committing, ``pre-commit`` will re-format the files if necessary. +#. If instead of using ``tox`` you prefer to run the tests directly, then we suggest to create a virtual environment and use + an editable install with the ``testing`` extra:: + + $ python3 -m venv .venv + $ source .venv/bin/activate # Linux + $ .venv/Scripts/activate.bat # Windows + $ pip install -e ".[testing]" + + Afterwards, you can edit the files and run pytest normally:: + + $ pytest testing/test_config.py + + #. Commit and push once your tests pass and you are happy with your change(s):: $ git commit -a -m "" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2ee1604a7..f18ce0887 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -57,7 +57,7 @@ jobs: export COVERAGE_FILE="$PWD/.coverage" export COVERAGE_PROCESS_START="$PWD/.coveragerc" fi - python -m tox -e $(tox.env) -vv + python -m tox -e $(tox.env) displayName: 'Run tests' - task: PublishTestResults@2 diff --git a/changelog/1857.improvement.rst b/changelog/1857.improvement.rst new file mode 100644 index 000000000..9a8ce90f5 --- /dev/null +++ b/changelog/1857.improvement.rst @@ -0,0 +1 @@ +``pytest.mark.parametrize`` accepts integers for ``ids`` again, converting it to strings. diff --git a/changelog/2548.bugfix.rst b/changelog/2548.bugfix.rst deleted file mode 100644 index 8ee3b6462..000000000 --- a/changelog/2548.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix line offset mismatch with skipped tests in terminal summary. diff --git a/changelog/4445.bugfix.rst b/changelog/4445.bugfix.rst new file mode 100644 index 000000000..f7583b2bf --- /dev/null +++ b/changelog/4445.bugfix.rst @@ -0,0 +1 @@ +Fixed some warning reports produced by pytest to point to the correct location of the warning in the user's code. diff --git a/changelog/4488.feature.rst b/changelog/4488.feature.rst deleted file mode 100644 index ddbca65d6..000000000 --- a/changelog/4488.feature.rst +++ /dev/null @@ -1,9 +0,0 @@ -New ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes. - -Each line of the report log contains a self contained JSON object corresponding to a testing event, -such as a collection or a test result report. The file is guaranteed to be flushed after writing -each line, so systems can read and process events in real-time. - -This option is meant to replace ``--resultlog``, which is deprecated and meant to be removed -in a future release. If you use ``--resultlog``, please try out ``--report-log`` and -provide feedback. diff --git a/changelog/4730.feature.rst b/changelog/4730.feature.rst deleted file mode 100644 index 80d1c4a38..000000000 --- a/changelog/4730.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -When ``sys.pycache_prefix`` (Python 3.8+) is set, it will be used by pytest to cache test files changed by the assertion rewriting mechanism. - -This makes it easier to benefit of cached ``.pyc`` files even on file systems without permissions. diff --git a/changelog/4901.trivial.rst b/changelog/4901.trivial.rst deleted file mode 100644 index f6609ddf1..000000000 --- a/changelog/4901.trivial.rst +++ /dev/null @@ -1,2 +0,0 @@ -``RunResult`` from ``pytester`` now displays the mnemonic of the ``ret`` attribute when it is a -valid ``pytest.ExitCode`` value. diff --git a/changelog/5061.improvement.rst b/changelog/5061.improvement.rst deleted file mode 100644 index 9eb0c1cd3..000000000 --- a/changelog/5061.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Use multiple colors with terminal summary statistics. diff --git a/changelog/5515.feature.rst b/changelog/5515.feature.rst deleted file mode 100644 index b53097c43..000000000 --- a/changelog/5515.feature.rst +++ /dev/null @@ -1,11 +0,0 @@ -Allow selective auto-indentation of multiline log messages. - -Adds command line option ``--log-auto-indent``, config option -``log_auto_indent`` and support for per-entry configuration of -indentation behavior on calls to ``logging.log()``. - -Alters the default for auto-indention from ``on`` to ``off``. This -restores the older behavior that existed prior to v4.6.0. This -reversion to earlier behavior was done because it is better to -activate new features that may lead to broken tests explicitly -rather than implicitly. diff --git a/changelog/5630.improvement.rst b/changelog/5630.improvement.rst deleted file mode 100644 index 45d49bdae..000000000 --- a/changelog/5630.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Quitting from debuggers is now properly handled in ``doctest`` items. diff --git a/changelog/5914.bugfix.rst b/changelog/5914.bugfix.rst new file mode 100644 index 000000000..b62b0b3c0 --- /dev/null +++ b/changelog/5914.bugfix.rst @@ -0,0 +1 @@ +pytester: fix ``no_fnmatch_line`` when used after positive matching. diff --git a/changelog/5914.feature.rst b/changelog/5914.feature.rst deleted file mode 100644 index 68cd66f99..000000000 --- a/changelog/5914.feature.rst +++ /dev/null @@ -1,19 +0,0 @@ -``pytester`` learned two new functions, `no_fnmatch_line `_ and -`no_re_match_line `_. - -The functions are used to ensure the captured text *does not* match the given -pattern. - -The previous idiom was to use ``re.match``: - -.. code-block:: python - - assert re.match(pat, result.stdout.str()) is None - -Or the ``in`` operator: - -.. code-block:: python - - assert text in result.stdout.str() - -But the new functions produce best output on failure. diff --git a/changelog/5924.improvement.rst b/changelog/5924.improvement.rst deleted file mode 100644 index a03eb4704..000000000 --- a/changelog/5924.improvement.rst +++ /dev/null @@ -1,34 +0,0 @@ -Improve verbose diff output with sequences. - -Before: - -.. code-block:: - - E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] - E Right contains 3 more items, first extra item: ' ' - E Full diff: - E - ['version', 'version_info', 'sys.version', 'sys.version_info'] - E + ['version', - E + 'version_info', - E + 'sys.version', - E + 'sys.version_info', - E + ' ', - E + 'sys.version', - E + 'sys.version_info'] - -After: - -.. code-block:: - - E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] - E Right contains 3 more items, first extra item: ' ' - E Full diff: - E [ - E 'version', - E 'version_info', - E 'sys.version', - E 'sys.version_info', - E + ' ', - E + 'sys.version', - E + 'sys.version_info', - E ] diff --git a/changelog/5928.bugfix.rst b/changelog/5928.bugfix.rst new file mode 100644 index 000000000..fbc53757d --- /dev/null +++ b/changelog/5928.bugfix.rst @@ -0,0 +1 @@ +Report ``PytestUnknownMarkWarning`` at the level of the user's code, not ``pytest``'s. diff --git a/changelog/5936.improvement.rst b/changelog/5936.improvement.rst deleted file mode 100644 index c5cd924bb..000000000 --- a/changelog/5936.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Display untruncated assertion message with ``-vv``. diff --git a/changelog/5975.deprecation.rst b/changelog/5975.deprecation.rst new file mode 100644 index 000000000..6e5dbc2ac --- /dev/null +++ b/changelog/5975.deprecation.rst @@ -0,0 +1,6 @@ +Deprecate using direct constructors for ``Nodes``. + +Instead they are new constructed via ``Node.from_parent``. + +This transitional mechanism enables us to detangle the very intensely +entangled ``Node`` relationships by enforcing more controlled creation/configruation patterns. diff --git a/changelog/5984.improvement.rst b/changelog/5984.improvement.rst new file mode 100644 index 000000000..1a0ad66f7 --- /dev/null +++ b/changelog/5984.improvement.rst @@ -0,0 +1 @@ +The ``pytest_warning_captured`` hook now receives a ``location`` parameter with the code location that generated the warning. diff --git a/changelog/5990.improvement.rst b/changelog/5990.improvement.rst deleted file mode 100644 index 6f5ad648e..000000000 --- a/changelog/5990.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Fix plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors"). diff --git a/changelog/6008.improvement.rst b/changelog/6008.improvement.rst deleted file mode 100644 index 22ef35cc8..000000000 --- a/changelog/6008.improvement.rst +++ /dev/null @@ -1,2 +0,0 @@ -``Config.InvocationParams.args`` is now always a ``tuple`` to better convey that it should be -immutable and avoid accidental modifications. diff --git a/changelog/6023.improvement.rst b/changelog/6023.improvement.rst deleted file mode 100644 index 6cf81002e..000000000 --- a/changelog/6023.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``pytest.main`` returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). diff --git a/changelog/6026.improvement.rst b/changelog/6026.improvement.rst deleted file mode 100644 index 34dfb278d..000000000 --- a/changelog/6026.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Align prefixes in output of pytester's ``LineMatcher``. diff --git a/changelog/6039.bugfix.rst b/changelog/6039.bugfix.rst deleted file mode 100644 index b13a677c8..000000000 --- a/changelog/6039.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -The ``PytestDoctestRunner`` is properly invalidated when unconfiguring the doctest plugin. - -This is important when used with ``pytester``'s ``runpytest_inprocess``. diff --git a/changelog/6047.bugfix.rst b/changelog/6047.bugfix.rst deleted file mode 100644 index 11a997f71..000000000 --- a/changelog/6047.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -BaseExceptions are handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. diff --git a/changelog/6057.feature.rst b/changelog/6057.feature.rst deleted file mode 100644 index b7334e7fe..000000000 --- a/changelog/6057.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Add tolerances to complex values when printing ``pytest.approx``. - -For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle. diff --git a/changelog/6059.improvement.rst b/changelog/6059.improvement.rst deleted file mode 100644 index 39ffff99b..000000000 --- a/changelog/6059.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Collection errors are reported as errors (and not failures like before) in the terminal's short test summary. diff --git a/changelog/6061.feature.rst b/changelog/6061.feature.rst deleted file mode 100644 index 11f548625..000000000 --- a/changelog/6061.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -Adding the pluginmanager as an option ``pytest_addoption`` -so that hooks can be invoked when setting up command line options. This is -useful for having one plugin communicate things to another plugin, -such as default values or which set of command line options to add. diff --git a/changelog/6069.improvement.rst b/changelog/6069.improvement.rst deleted file mode 100644 index e60d154bb..000000000 --- a/changelog/6069.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally. diff --git a/changelog/6074.bugfix.rst b/changelog/6074.bugfix.rst deleted file mode 100644 index 624cf5d1c..000000000 --- a/changelog/6074.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -pytester: fix order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``. diff --git a/changelog/6097.improvement.rst b/changelog/6097.improvement.rst deleted file mode 100644 index 32eb84906..000000000 --- a/changelog/6097.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -The "[XXX%]" indicator in the test summary is colored according to the final (new) multi-colored line's main color. diff --git a/changelog/6099.bugfix.rst b/changelog/6099.bugfix.rst deleted file mode 100644 index 77f33cde1..000000000 --- a/changelog/6099.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``--trace`` when used with parametrized functions. diff --git a/changelog/6116.improvement.rst b/changelog/6116.improvement.rst deleted file mode 100644 index 4fc96ec77..000000000 --- a/changelog/6116.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``--co`` as a synonym to ``--collect-only``. diff --git a/changelog/6148.improvement.rst b/changelog/6148.improvement.rst deleted file mode 100644 index 3d77ab528..000000000 --- a/changelog/6148.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``atomicwrites`` is now only used on Windows, fixing a performance regression with assertion rewriting on Unix. diff --git a/changelog/6152.improvement.rst b/changelog/6152.improvement.rst deleted file mode 100644 index 8e5f4d52a..000000000 --- a/changelog/6152.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Now parametrization will use the ``__name__`` attribute of any object for the id, if present. Previously it would only use ``__name__`` for functions and classes. diff --git a/changelog/6213.improvement.rst b/changelog/6213.improvement.rst new file mode 100644 index 000000000..735d4455f --- /dev/null +++ b/changelog/6213.improvement.rst @@ -0,0 +1 @@ +pytester: the ``testdir`` fixture respects environment settings from the ``monkeypatch`` fixture for inner runs. diff --git a/changelog/6231.improvement.rst b/changelog/6231.improvement.rst new file mode 100644 index 000000000..1554a229b --- /dev/null +++ b/changelog/6231.improvement.rst @@ -0,0 +1 @@ +Improve check for misspelling of ``pytest.mark.parametrize``. diff --git a/changelog/6247.improvement.rst b/changelog/6247.improvement.rst new file mode 100644 index 000000000..6634d6b80 --- /dev/null +++ b/changelog/6247.improvement.rst @@ -0,0 +1 @@ +``--fulltrace`` is honored with collection errors. diff --git a/changelog/6255.bugfix.rst b/changelog/6255.bugfix.rst new file mode 100644 index 000000000..831187feb --- /dev/null +++ b/changelog/6255.bugfix.rst @@ -0,0 +1,3 @@ +Clear the ``sys.last_traceback``, ``sys.last_type`` and ``sys.last_value`` +attributes by deleting them instead of setting them to ``None``. This better +matches the behaviour of the Python standard library. diff --git a/changelog/759.improvement.rst b/changelog/759.improvement.rst new file mode 100644 index 000000000..83ace7485 --- /dev/null +++ b/changelog/759.improvement.rst @@ -0,0 +1 @@ +``pytest.mark.parametrize`` supports iterators and generators for ``ids``. diff --git a/changelog/README.rst b/changelog/README.rst index 5c182758b..3e464508a 100644 --- a/changelog/README.rst +++ b/changelog/README.rst @@ -1,12 +1,14 @@ This directory contains "newsfragments" which are short files that contain a small **ReST**-formatted text that will be added to the next ``CHANGELOG``. -The ``CHANGELOG`` will be read by users, so this description should be aimed to pytest users +The ``CHANGELOG`` will be read by **users**, so this description should be aimed to pytest users instead of describing internal changes which are only relevant to the developers. -Make sure to use full sentences with correct case and punctuation, for example:: +Make sure to use full sentences in the **past or present tense** and use punctuation, examples:: - Fix issue with non-ascii messages from the ``warnings`` module. + Improved verbose diff output with sequences. + + Terminal summary statistics now use multiple colors. Each file should be named like ``..rst``, where ```` is an issue number, and ```` is one of: diff --git a/doc/5934.feature.rst b/doc/5934.feature.rst deleted file mode 100644 index 17c0b1737..000000000 --- a/doc/5934.feature.rst +++ /dev/null @@ -1 +0,0 @@ -``repr`` of ``ExceptionInfo`` objects has been improved to honor the ``__repr__`` method of the underlying exception. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index f7a634b31..6e6914f2d 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,9 @@ Release announcements :maxdepth: 2 + release-5.3.0 + release-5.2.4 + release-5.2.3 release-5.2.2 release-5.2.1 release-5.2.0 diff --git a/doc/en/announce/release-2.0.0.rst b/doc/en/announce/release-2.0.0.rst index af745fc59..d9d90c09a 100644 --- a/doc/en/announce/release-2.0.0.rst +++ b/doc/en/announce/release-2.0.0.rst @@ -7,7 +7,7 @@ see below for summary and detailed lists. A lot of long-deprecated code has been removed, resulting in a much smaller and cleaner implementation. See the new docs with examples here: - http://pytest.org/2.0.0/index.html + http://pytest.org/en/latest/index.html A note on packaging: pytest used to part of the "py" distribution up until version py-1.3.4 but this has changed now: pytest-2.0.0 only @@ -36,12 +36,12 @@ New Features import pytest ; pytest.main(arglist, pluginlist) - see http://pytest.org/2.0.0/usage.html for details. + see http://pytest.org/en/latest/usage.html for details. - new and better reporting information in assert expressions if comparing lists, sequences or strings. - see http://pytest.org/2.0.0/assert.html#newreport + see http://pytest.org/en/latest/assert.html#newreport - new configuration through ini-files (setup.cfg or tox.ini recognized), for example:: @@ -50,7 +50,7 @@ New Features norecursedirs = .hg data* # don't ever recurse in such dirs addopts = -x --pyargs # add these command line options by default - see http://pytest.org/2.0.0/customize.html + see http://pytest.org/en/latest/customize.html - improved standard unittest support. In general py.test should now better be able to run custom unittest.TestCases like twisted trial diff --git a/doc/en/announce/release-2.0.1.rst b/doc/en/announce/release-2.0.1.rst index 2f41ef943..f86537e1d 100644 --- a/doc/en/announce/release-2.0.1.rst +++ b/doc/en/announce/release-2.0.1.rst @@ -57,7 +57,7 @@ Changes between 2.0.0 and 2.0.1 - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output - introduce a mechanism to prevent/unregister plugins from the - command line, see http://pytest.org/latest/plugins.html#cmdunregister + command line, see http://pytest.org/en/latest/plugins.html#cmdunregister - activate resultlog plugin by default - fix regression wrt yielded tests which due to the collection-before-running semantics were not diff --git a/doc/en/announce/release-2.2.0.rst b/doc/en/announce/release-2.2.0.rst index 20bfe0a19..79e4dfd15 100644 --- a/doc/en/announce/release-2.2.0.rst +++ b/doc/en/announce/release-2.2.0.rst @@ -9,7 +9,7 @@ with these improvements: - new @pytest.mark.parametrize decorator to run tests with different arguments - new metafunc.parametrize() API for parametrizing arguments independently - - see examples at http://pytest.org/latest/example/parametrize.html + - see examples at http://pytest.org/en/latest/example/parametrize.html - NOTE that parametrize() related APIs are still a bit experimental and might change in future releases. @@ -18,7 +18,7 @@ with these improvements: - "-m markexpr" option for selecting tests according to their mark - a new "markers" ini-variable for registering test markers for your project - the new "--strict" bails out with an error if using unregistered markers. - - see examples at http://pytest.org/latest/example/markers.html + - see examples at http://pytest.org/en/latest/example/markers.html * duration profiling: new "--duration=N" option showing the N slowest test execution or setup/teardown calls. This is most useful if you want to @@ -78,7 +78,7 @@ Changes between 2.1.3 and 2.2.0 or through plugin hooks. Also introduce a "--strict" option which will treat unregistered markers as errors allowing to avoid typos and maintain a well described set of markers - for your test suite. See examples at http://pytest.org/latest/mark.html + for your test suite. See examples at http://pytest.org/en/latest/mark.html and its links. - issue50: introduce "-m marker" option to select tests based on markers (this is a stricter and more predictable version of "-k" in that "-m" diff --git a/doc/en/announce/release-2.3.0.rst b/doc/en/announce/release-2.3.0.rst index 061aa025c..5fb253670 100644 --- a/doc/en/announce/release-2.3.0.rst +++ b/doc/en/announce/release-2.3.0.rst @@ -13,12 +13,12 @@ re-useable fixture design. For detailed info and tutorial-style examples, see: - http://pytest.org/latest/fixture.html + http://pytest.org/en/latest/fixture.html Moreover, there is now support for using pytest fixtures/funcargs with unittest-style suites, see here for examples: - http://pytest.org/latest/unittest.html + http://pytest.org/en/latest/unittest.html Besides, more unittest-test suites are now expected to "simply work" with pytest. @@ -29,11 +29,11 @@ pytest-2.2.4. If you are interested in the precise reasoning (including examples) of the pytest-2.3 fixture evolution, please consult -http://pytest.org/latest/funcarg_compare.html +http://pytest.org/en/latest/funcarg_compare.html For general info on installation and getting started: - http://pytest.org/latest/getting-started.html + http://pytest.org/en/latest/getting-started.html Docs and PDF access as usual at: @@ -94,7 +94,7 @@ Changes between 2.2.4 and 2.3.0 - pluginmanager.register(...) now raises ValueError if the plugin has been already registered or the name is taken -- fix issue159: improve http://pytest.org/latest/faq.html +- fix issue159: improve http://pytest.org/en/latest/faq.html especially with respect to the "magic" history, also mention pytest-django, trial and unittest integration. diff --git a/doc/en/announce/release-2.3.4.rst b/doc/en/announce/release-2.3.4.rst index e2e8cb143..b00430f94 100644 --- a/doc/en/announce/release-2.3.4.rst +++ b/doc/en/announce/release-2.3.4.rst @@ -16,7 +16,7 @@ comes with the following fixes and features: - yielded test functions will now have autouse-fixtures active but cannot accept fixtures as funcargs - it's anyway recommended to rather use the post-2.0 parametrize features instead of yield, see: - http://pytest.org/latest/example/parametrize.html + http://pytest.org/en/latest/example/parametrize.html - fix autouse-issue where autouse-fixtures would not be discovered if defined in an a/conftest.py file and tests in a/tests/test_some.py - fix issue226 - LIFO ordering for fixture teardowns diff --git a/doc/en/announce/release-2.4.0.rst b/doc/en/announce/release-2.4.0.rst index 1b0168841..6cd14bc2d 100644 --- a/doc/en/announce/release-2.4.0.rst +++ b/doc/en/announce/release-2.4.0.rst @@ -7,7 +7,7 @@ from a few supposedly very minor incompatibilities. See below for a full list of details. A few feature highlights: - new yield-style fixtures `pytest.yield_fixture - `_, allowing to use + `_, allowing to use existing with-style context managers in fixture functions. - improved pdb support: ``import pdb ; pdb.set_trace()`` now works diff --git a/doc/en/announce/release-2.7.0.rst b/doc/en/announce/release-2.7.0.rst index d63081edb..8952ff50f 100644 --- a/doc/en/announce/release-2.7.0.rst +++ b/doc/en/announce/release-2.7.0.rst @@ -52,7 +52,7 @@ holger krekel - add ability to set command line options by environment variable PYTEST_ADDOPTS. - added documentation on the new pytest-dev teams on bitbucket and - github. See https://pytest.org/latest/contributing.html . + github. See https://pytest.org/en/latest/contributing.html . Thanks to Anatoly for pushing and initial work on this. - fix issue650: new option ``--docttest-ignore-import-errors`` which diff --git a/doc/en/announce/release-2.9.0.rst b/doc/en/announce/release-2.9.0.rst index 05d9a394f..9e0856690 100644 --- a/doc/en/announce/release-2.9.0.rst +++ b/doc/en/announce/release-2.9.0.rst @@ -131,7 +131,7 @@ The py.test Development Team with same name. -.. _`traceback style docs`: https://pytest.org/latest/usage.html#modifying-python-traceback-printing +.. _`traceback style docs`: https://pytest.org/en/latest/usage.html#modifying-python-traceback-printing .. _#1422: https://github.com/pytest-dev/pytest/issues/1422 .. _#1379: https://github.com/pytest-dev/pytest/issues/1379 diff --git a/doc/en/announce/release-5.2.3.rst b/doc/en/announce/release-5.2.3.rst new file mode 100644 index 000000000..bfb62a1b8 --- /dev/null +++ b/doc/en/announce/release-5.2.3.rst @@ -0,0 +1,28 @@ +pytest-5.2.3 +======================================= + +pytest 5.2.3 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Brett Cannon +* Bruno Oliveira +* Daniel Hahler +* Daniil Galiev +* David Szotten +* Florian Bruhin +* Patrick Harmon +* Ran Benita +* Zac Hatfield-Dodds +* Zak Hassan + + +Happy testing, +The pytest Development Team diff --git a/doc/en/announce/release-5.2.4.rst b/doc/en/announce/release-5.2.4.rst new file mode 100644 index 000000000..05677e77f --- /dev/null +++ b/doc/en/announce/release-5.2.4.rst @@ -0,0 +1,22 @@ +pytest-5.2.4 +======================================= + +pytest 5.2.4 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Hugo +* Michael Shields + + +Happy testing, +The pytest Development Team diff --git a/doc/en/announce/release-5.3.0.rst b/doc/en/announce/release-5.3.0.rst new file mode 100644 index 000000000..9855a7a2d --- /dev/null +++ b/doc/en/announce/release-5.3.0.rst @@ -0,0 +1,45 @@ +pytest-5.3.0 +======================================= + +The pytest team is proud to announce the 5.3.0 release! + +pytest is a mature Python testing tool with more than a 2000 tests +against itself, passing on many different interpreters and platforms. + +This release contains a number of bugs fixes and improvements, so users are encouraged +to take a look at the CHANGELOG: + + https://docs.pytest.org/en/latest/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/latest/ + +As usual, you can upgrade from pypi via: + + pip install -U pytest + +Thanks to all who contributed to this release, among them: + +* AnjoMan +* Anthony Sottile +* Anton Lodder +* Bruno Oliveira +* Daniel Hahler +* Gregory Lee +* Josh Karpel +* JoshKarpel +* Joshua Storck +* Kale Kundert +* MarcoGorelli +* Michael Krebs +* NNRepos +* Ran Benita +* TH3CHARLie +* Tibor Arpas +* Zac Hatfield-Dodds +* 林玮 + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/conf.py b/doc/en/conf.py index 1a6ef7ca8..1e93f8e20 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -92,7 +92,7 @@ exclude_patterns = [ # The reST default role (used for this markup: `text`) to use for all documents. -# default_role = None +default_role = "literal" # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True @@ -112,6 +112,19 @@ pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] +# A list of regular expressions that match URIs that should not be checked when +# doing a linkcheck. +linkcheck_ignore = [ + "https://github.com/numpy/numpy/blob/master/doc/release/1.16.0-notes.rst#new-deprecations", + "https://blogs.msdn.microsoft.com/bharry/2017/06/28/testing-in-a-cloud-delivery-cadence/", + "http://pythontesting.net/framework/pytest-introduction/", + r"https://github.com/pytest-dev/pytest/issues/\d+", + r"https://github.com/pytest-dev/pytest/pull/\d+", +] + +# The number of worker threads to use when checking links (default=5). +linkcheck_workers = 5 + # -- Options for HTML output --------------------------------------------------- diff --git a/doc/en/contents.rst b/doc/en/contents.rst index 5d7599f50..c623d0602 100644 --- a/doc/en/contents.rst +++ b/doc/en/contents.rst @@ -27,7 +27,6 @@ Full pytest documentation unittest nose xunit_setup - report_log plugins writing_plugins logging diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 5cf3b0903..88112b12a 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -20,6 +20,37 @@ Below is a complete list of all pytest features which are considered deprecated. :ref:`standard warning filters `. +Node Construction changed to ``Node.from_parent`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.3 + +The construction of nodes new should use the named constructor ``from_parent``. +This limitation in api surface intends to enable better/simpler refactoring of the collection tree. + + +``junit_family`` default value change to "xunit2" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.2 + +The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given +that this is the version supported by default in modern tools that manipulate this type of file. + +In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option +is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``:: + + PytestDeprecationWarning: The 'junit_family' default value will change to 'xunit2' in pytest 6.0. + Add 'junit_family=legacy' to your pytest.ini file to silence this warning and make your suite compatible. + +In order to silence this warning, users just need to configure the ``junit_family`` option explicitly: + +.. code-block:: ini + + [pytest] + junit_family=legacy + + ``funcargnames`` alias for ``fixturenames`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -43,11 +74,12 @@ The ``--result-log`` option produces a stream of test reports which can be analysed at runtime, but it uses a custom format which requires users to implement their own parser. -The :ref:`--report-log ` option provides a more standard and extensible alternative, producing +The `pytest-reportlog `__ plugin provides a ``--report-log`` option, a more standard and extensible alternative, producing one JSON object per-line, and should cover the same use cases. Please try it out and provide feedback. -The plan is remove the ``--result-log`` option in pytest 6.0 after ``--result-log`` proves satisfactory -to all users and is deemed stable. +The plan is remove the ``--result-log`` option in pytest 6.0 if ``pytest-reportlog`` proves satisfactory +to all users and is deemed stable. The ``pytest-reportlog`` plugin might even be merged into the core +at some point, depending on the plans for the plugins and number of users using it. Removed Features diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index ccddb1f66..8143b3fd4 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -622,7 +622,7 @@ then you will see two tests skipped and two executed tests as expected: test_plat.py s.s. [100%] ========================= short test summary info ========================== - SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux + SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:12: cannot run on platform linux ======================= 2 passed, 2 skipped in 0.12s ======================= Note that if you specify a platform via the marker-command line option like this: diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py index 93d8285bf..d30ab3841 100644 --- a/doc/en/example/nonpython/conftest.py +++ b/doc/en/example/nonpython/conftest.py @@ -4,7 +4,7 @@ import pytest def pytest_collect_file(parent, path): if path.ext == ".yaml" and path.basename.startswith("test"): - return YamlFile(path, parent) + return YamlFile.from_parent(parent, fspath=path) class YamlFile(pytest.File): @@ -13,7 +13,7 @@ class YamlFile(pytest.File): raw = yaml.safe_load(self.fspath.open()) for name, spec in sorted(raw.items()): - yield YamlItem(name, self, spec) + yield YamlItem.from_parent(self, name=name, spec=spec) class YamlItem(pytest.Item): diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 1220cfb4d..c420761a4 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -475,10 +475,10 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ssssssssssssssssssssssss... [100%] + ssssssssssss...ssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found - SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.6' not found + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.7' not found 3 passed, 24 skipped in 0.12s Indirect parametrization of optional implementations/imports @@ -547,7 +547,7 @@ If you run this with reporting for skips enabled: test_module.py .s [100%] ========================= short test summary info ========================== - SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:13: could not import 'opt2': No module named 'opt2' + SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:12: could not import 'opt2': No module named 'opt2' ======================= 1 passed, 1 skipped in 0.12s ======================= You'll see that we don't have an ``opt2`` module and thus the second test run diff --git a/doc/en/example/py2py3/conftest.py b/doc/en/example/py2py3/conftest.py index 844510a25..0291b37b4 100644 --- a/doc/en/example/py2py3/conftest.py +++ b/doc/en/example/py2py3/conftest.py @@ -13,4 +13,4 @@ class DummyCollector(pytest.collect.File): def pytest_pycollect_makemodule(path, parent): bn = path.basename if "py3" in bn and not py3 or ("py2" in bn and py3): - return DummyCollector(path, parent=parent) + return DummyCollector.from_parent(parent, fspath=path) diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index eb978c5ea..1c06782f6 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -436,7 +436,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: items = [1, 2, 3] print("items is {!r}".format(items)) > a, b = items.pop() - E TypeError: cannot unpack non-iterable int object + E TypeError: 'int' object is not iterable failure_demo.py:181: TypeError --------------------------- Captured stdout call --------------------------- @@ -516,7 +516,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_z2_type_error(self): items = 3 > a, b = items - E TypeError: cannot unpack non-iterable int object + E TypeError: 'int' object is not iterable failure_demo.py:222: TypeError ______________________ TestMoreErrors.test_startswith ______________________ diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index a7cd06d31..1570850fc 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -300,36 +300,33 @@ behave differently if called from a test. But if you absolutely must find out if your application code is running from a test you can do something like this: +.. code-block:: python + + # content of your_module.py + + + _called_from_test = False + .. code-block:: python # content of conftest.py def pytest_configure(config): - import sys + your_module._called_from_test = True - sys._called_from_test = True - - - def pytest_unconfigure(config): - import sys - - del sys._called_from_test - -and then check for the ``sys._called_from_test`` flag: +and then check for the ``your_module._called_from_test`` flag: .. code-block:: python - if hasattr(sys, "_called_from_test"): + if your_module._called_from_test: # called from within a test run ... else: # called "normally" ... -accordingly in your application. It's also a good idea -to use your own application module rather than ``sys`` -for handling flag. +accordingly in your application. Adding info to test report header -------------------------------------------------------------- @@ -446,7 +443,7 @@ Now we can profile which test functions execute the slowest: ========================= slowest 3 test durations ========================= 0.30s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 - 0.10s call test_some_are_slow.py::test_funcfast + 0.11s call test_some_are_slow.py::test_funcfast ============================ 3 passed in 0.12s ============================= incremental testing - test steps diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 6e1e554bf..08305a5cc 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -9,9 +9,9 @@ pytest fixtures: explicit, modular, scalable -.. _`xUnit`: http://en.wikipedia.org/wiki/XUnit -.. _`purpose of test fixtures`: http://en.wikipedia.org/wiki/Test_fixture#Software -.. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection +.. _`xUnit`: https://en.wikipedia.org/wiki/XUnit +.. _`purpose of test fixtures`: https://en.wikipedia.org/wiki/Test_fixture#Software +.. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection The `purpose of test fixtures`_ is to provide a fixed baseline upon which tests can reliably and repeatedly execute. pytest fixtures diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 97347f126..2bdd68ea3 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.7/site-packages/pytest.py + This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py .. _`simpletest`: diff --git a/doc/en/projects.rst b/doc/en/projects.rst index 751d0abe9..2febcd24b 100644 --- a/doc/en/projects.rst +++ b/doc/en/projects.rst @@ -73,7 +73,6 @@ Some organisations using pytest * `Square Kilometre Array, Cape Town `_ * `Some Mozilla QA people `_ use pytest to distribute their Selenium tests -* `Tandberg `_ * `Shootq `_ * `Stups department of Heinrich Heine University Duesseldorf `_ * cellzome diff --git a/doc/en/reference.rst b/doc/en/reference.rst index f90efc3a5..9986d1129 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -59,7 +59,7 @@ pytest.raises **Tutorial**: :ref:`assertraises`. -.. autofunction:: pytest.raises(expected_exception: Exception, [match]) +.. autofunction:: pytest.raises(expected_exception: Exception [, *, match]) :with: excinfo pytest.deprecated_call diff --git a/doc/en/report_log.rst b/doc/en/report_log.rst deleted file mode 100644 index 619925180..000000000 --- a/doc/en/report_log.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. _report_log: - -Report files -============ - -.. versionadded:: 5.3 - -The ``--report-log=FILE`` option writes *report logs* into a file as the test session executes. - -Each line of the report log contains a self contained JSON object corresponding to a testing event, -such as a collection or a test result report. The file is guaranteed to be flushed after writing -each line, so systems can read and process events in real-time. - -Each JSON object contains a special key ``$report_type``, which contains a unique identifier for -that kind of report object. For future compatibility, consumers of the file should ignore reports -they don't recognize, as well as ignore unknown properties/keys in JSON objects that they do know, -as future pytest versions might enrich the objects with more properties/keys. - -.. note:: - This option is meant to the replace ``--resultlog``, which is deprecated and meant to be removed - in a future release. If you use ``--resultlog``, please try out ``--report-log`` and - provide feedback. - -Example -------- - -Consider this file: - -.. code-block:: python - - # content of test_report_example.py - - - def test_ok(): - assert 5 + 5 == 10 - - - def test_fail(): - assert 4 + 4 == 1 - - -.. code-block:: pytest - - $ pytest test_report_example.py -q --report-log=log.json - .F [100%] - ================================= FAILURES ================================= - ________________________________ test_fail _________________________________ - - def test_fail(): - > assert 4 + 4 == 1 - E assert (4 + 4) == 1 - - test_report_example.py:8: AssertionError - ------------------- generated report log file: log.json -------------------- - 1 failed, 1 passed in 0.12s - -The generated ``log.json`` will contain a JSON object per line: - -:: - - $ cat log.json - {"pytest_version": "5.2.3.dev90+gd1129cf96.d20191026", "$report_type": "Header"} - {"nodeid": "", "outcome": "passed", "longrepr": null, "result": null, "sections": [], "$report_type": "CollectReport"} - {"nodeid": "test_report_example.py", "outcome": "passed", "longrepr": null, "result": null, "sections": [], "$report_type": "CollectReport"} - {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "setup", "user_properties": [], "sections": [], "duration": 0.00021314620971679688, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "call", "user_properties": [], "sections": [], "duration": 0.00014543533325195312, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "teardown", "user_properties": [], "sections": [], "duration": 0.00016427040100097656, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "passed", "longrepr": null, "when": "setup", "user_properties": [], "sections": [], "duration": 0.00013589859008789062, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "failed", "longrepr": {"reprcrash": {"path": "$REGENDOC_TMPDIR/test_report_example.py", "lineno": 8, "message": "assert (4 + 4) == 1"}, "reprtraceback": {"reprentries": [{"type": "ReprEntry", "data": {"lines": [" def test_fail():", "> assert 4 + 4 == 1", "E assert (4 + 4) == 1"], "reprfuncargs": {"args": []}, "reprlocals": null, "reprfileloc": {"path": "test_report_example.py", "lineno": 8, "message": "AssertionError"}, "style": "long"}}], "extraline": null, "style": "long"}, "sections": [], "chain": [[{"reprentries": [{"type": "ReprEntry", "data": {"lines": [" def test_fail():", "> assert 4 + 4 == 1", "E assert (4 + 4) == 1"], "reprfuncargs": {"args": []}, "reprlocals": null, "reprfileloc": {"path": "test_report_example.py", "lineno": 8, "message": "AssertionError"}, "style": "long"}}], "extraline": null, "style": "long"}, {"path": "$REGENDOC_TMPDIR/test_report_example.py", "lineno": 8, "message": "assert (4 + 4) == 1"}, null]]}, "when": "call", "user_properties": [], "sections": [], "duration": 0.00027489662170410156, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "passed", "longrepr": null, "when": "teardown", "user_properties": [], "sections": [], "duration": 0.00016689300537109375, "$report_type": "TestReport"} diff --git a/doc/en/talks.rst b/doc/en/talks.rst index 04eb97c7f..16bdd665b 100644 --- a/doc/en/talks.rst +++ b/doc/en/talks.rst @@ -64,7 +64,7 @@ Talks and blog postings - `pytest introduction from Brian Okken (January 2013) `_ -- pycon australia 2012 pytest talk from Brianna Laugher (`video `_, `slides `_, `code `_) +- pycon australia 2012 pytest talk from Brianna Laugher (`video `_, `slides `_, `code `_) - `pycon 2012 US talk video from Holger Krekel `_ - `monkey patching done right`_ (blog post, consult `monkeypatch plugin`_ for up-to-date API) diff --git a/doc/en/usage.rst b/doc/en/usage.rst index a23cf764a..245a67b68 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -66,8 +66,8 @@ To stop the testing process after the first (N) failures: .. code-block:: bash - pytest -x # stop after first failure - pytest --maxfail=2 # stop after two failures + pytest -x # stop after first failure + pytest --maxfail=2 # stop after two failures .. _select-tests: @@ -241,7 +241,7 @@ Example: test_example.py:14: AssertionError ========================= short test summary info ========================== - SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:22: skipping this test XFAIL test_example.py::test_xfail reason: xfailing this test XPASS test_example.py::test_xpass always xfail @@ -296,7 +296,7 @@ More than one character can be used, so for example to only see failed and skipp test_example.py:14: AssertionError ========================= short test summary info ========================== FAILED test_example.py::test_fail - assert 0 - SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:22: skipping this test == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had @@ -692,7 +692,7 @@ by the `PyPy-test`_ web page to show test results over several revisions. This option is rarely used and is scheduled for removal in pytest 6.0. - If you use this option, consider using the new :ref:`--result-log `. + If you use this option, consider using the new `pytest-reportlog `__ plugin instead. See `the deprecation docs `__ for more information. diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 54bb60da1..4b8be4469 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -41,7 +41,7 @@ Running pytest now produces this output: warnings.warn(UserWarning("api v1, should use functions from v2")) -- Docs: https://docs.pytest.org/en/latest/warnings.html - ====================== 1 passed, 1 warnings in 0.12s ======================= + ======================= 1 passed, 1 warning in 0.12s ======================= The ``-W`` flag can be passed to control which warnings will be displayed or even turn them into errors: @@ -407,7 +407,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta class Test: -- Docs: https://docs.pytest.org/en/latest/warnings.html - 1 warnings in 0.12s + 1 warning in 0.12s These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings. diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 8660746bd..2f7283791 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -442,7 +442,7 @@ additionally it is possible to copy examples for an example folder before runnin testdir.copy_example("test_example.py") -- Docs: https://docs.pytest.org/en/latest/warnings.html - ====================== 2 passed, 1 warnings in 0.12s ======================= + ======================= 2 passed, 1 warning in 0.12s ======================= For more information about the result object that ``runpytest()`` returns, and the methods that it provides please check out the :py:class:`RunResult diff --git a/scripts/release.py b/scripts/release.py index 5009df359..884d9bfb1 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -79,12 +79,19 @@ def fix_formatting(): call(["pre-commit", "run", "--all-files"]) +def check_links(): + """Runs sphinx-build to check links""" + print(f"{Fore.CYAN}[generate.check_links] {Fore.RESET}Checking links") + check_call(["tox", "-e", "docs-checklinks"]) + + def pre_release(version): """Generates new docs, release announcements and creates a local tag.""" announce(version) regen() changelog(version, write_out=True) fix_formatting() + check_links() msg = "Preparing release version {}".format(version) check_call(["git", "commit", "-a", "-m", msg]) diff --git a/setup.cfg b/setup.cfg index 60e866562..42d5b9460 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,12 +57,13 @@ upload-dir = doc/en/build/html [check-manifest] ignore = - _pytest/_version.py + src/_pytest/_version.py [devpi:upload] formats = sdist.tgz,bdist_wheel [mypy] +mypy_path = src ignore_missing_imports = True no_implicit_optional = True strict_equality = True diff --git a/src/_pytest/_argcomplete.py b/src/_pytest/_argcomplete.py index 688c9077d..7ca216ecf 100644 --- a/src/_pytest/_argcomplete.py +++ b/src/_pytest/_argcomplete.py @@ -53,19 +53,22 @@ If things do not work right away: which should throw a KeyError: 'COMPLINE' (which is properly set by the global argcomplete script). """ +import argparse import os import sys from glob import glob +from typing import Any +from typing import List from typing import Optional class FastFilesCompleter: "Fast file completer class" - def __init__(self, directories=True): + def __init__(self, directories: bool = True) -> None: self.directories = directories - def __call__(self, prefix, **kwargs): + def __call__(self, prefix: str, **kwargs: Any) -> List[str]: """only called on non option completions""" if os.path.sep in prefix[1:]: prefix_dir = len(os.path.dirname(prefix) + os.path.sep) @@ -94,13 +97,13 @@ if os.environ.get("_ARGCOMPLETE"): sys.exit(-1) filescompleter = FastFilesCompleter() # type: Optional[FastFilesCompleter] - def try_argcomplete(parser): + def try_argcomplete(parser: argparse.ArgumentParser) -> None: argcomplete.autocomplete(parser, always_complete_options=False) else: - def try_argcomplete(parser): + def try_argcomplete(parser: argparse.ArgumentParser) -> None: pass filescompleter = None diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 3c2acfe7f..d1a8ec2f1 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -7,13 +7,17 @@ from inspect import CO_VARKEYWORDS from io import StringIO from traceback import format_exception_only from types import CodeType +from types import FrameType from types import TracebackType from typing import Any +from typing import Callable from typing import Dict from typing import Generic +from typing import Iterable from typing import List from typing import Optional from typing import Pattern +from typing import Sequence from typing import Set from typing import Tuple from typing import TypeVar @@ -27,9 +31,16 @@ import py import _pytest from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr +from _pytest.compat import overload if False: # TYPE_CHECKING from typing import Type + from typing_extensions import Literal + from weakref import ReferenceType # noqa: F401 + + from _pytest._code import Source + + _TracebackStyle = Literal["long", "short", "no", "native"] class Code: @@ -38,13 +49,12 @@ class Code: def __init__(self, rawcode) -> None: if not hasattr(rawcode, "co_filename"): rawcode = getrawcode(rawcode) - try: - self.filename = rawcode.co_filename - self.firstlineno = rawcode.co_firstlineno - 1 - self.name = rawcode.co_name - except AttributeError: + if not isinstance(rawcode, CodeType): raise TypeError("not a code object: {!r}".format(rawcode)) - self.raw = rawcode # type: CodeType + self.filename = rawcode.co_filename + self.firstlineno = rawcode.co_firstlineno - 1 + self.name = rawcode.co_name + self.raw = rawcode def __eq__(self, other): return self.raw == other.raw @@ -72,7 +82,7 @@ class Code: return p @property - def fullsource(self): + def fullsource(self) -> Optional["Source"]: """ return a _pytest._code.Source object for the full source file of the code """ from _pytest._code import source @@ -80,7 +90,7 @@ class Code: full, _ = source.findsource(self.raw) return full - def source(self): + def source(self) -> "Source": """ return a _pytest._code.Source object for the code object's source only """ # return source only for that part of code @@ -88,7 +98,7 @@ class Code: return _pytest._code.Source(self.raw) - def getargs(self, var=False): + def getargs(self, var: bool = False) -> Tuple[str, ...]: """ return a tuple with the argument names for the code object if 'var' is set True also return the names of the variable and @@ -107,7 +117,7 @@ class Frame: """Wrapper around a Python frame holding f_locals and f_globals in which expressions can be evaluated.""" - def __init__(self, frame): + def __init__(self, frame: FrameType) -> None: self.lineno = frame.f_lineno - 1 self.f_globals = frame.f_globals self.f_locals = frame.f_locals @@ -115,7 +125,7 @@ class Frame: self.code = Code(frame.f_code) @property - def statement(self): + def statement(self) -> "Source": """ statement this frame is at """ import _pytest._code @@ -134,7 +144,7 @@ class Frame: f_locals.update(vars) return eval(code, self.f_globals, f_locals) - def exec_(self, code, **vars): + def exec_(self, code, **vars) -> None: """ exec 'code' in the frame 'vars' are optional; additional local variables @@ -143,7 +153,7 @@ class Frame: f_locals.update(vars) exec(code, self.f_globals, f_locals) - def repr(self, object): + def repr(self, object: object) -> str: """ return a 'safe' (non-recursive, one-line) string repr for 'object' """ return saferepr(object) @@ -151,7 +161,7 @@ class Frame: def is_true(self, object): return object - def getargs(self, var=False): + def getargs(self, var: bool = False): """ return a list of tuples (name, value) for all arguments if 'var' is set True also include the variable and keyword @@ -169,35 +179,34 @@ class Frame: class TracebackEntry: """ a single entry in a traceback """ - _repr_style = None + _repr_style = None # type: Optional[Literal["short", "long"]] exprinfo = None - def __init__(self, rawentry, excinfo=None): + def __init__(self, rawentry: TracebackType, excinfo=None) -> None: self._excinfo = excinfo self._rawentry = rawentry self.lineno = rawentry.tb_lineno - 1 - def set_repr_style(self, mode): + def set_repr_style(self, mode: "Literal['short', 'long']") -> None: assert mode in ("short", "long") self._repr_style = mode @property - def frame(self): - import _pytest._code - - return _pytest._code.Frame(self._rawentry.tb_frame) + def frame(self) -> Frame: + return Frame(self._rawentry.tb_frame) @property - def relline(self): + def relline(self) -> int: return self.lineno - self.frame.code.firstlineno - def __repr__(self): + def __repr__(self) -> str: return "" % (self.frame.code.path, self.lineno + 1) @property - def statement(self): + def statement(self) -> "Source": """ _pytest._code.Source object for the current statement """ source = self.frame.code.fullsource + assert source is not None return source.getstatement(self.lineno) @property @@ -206,14 +215,14 @@ class TracebackEntry: return self.frame.code.path @property - def locals(self): + def locals(self) -> Dict[str, Any]: """ locals of underlying frame """ return self.frame.f_locals - def getfirstlinesource(self): + def getfirstlinesource(self) -> int: return self.frame.code.firstlineno - def getsource(self, astcache=None): + def getsource(self, astcache=None) -> Optional["Source"]: """ return failing source code. """ # we use the passed in astcache to not reparse asttrees # within exception info printing @@ -258,7 +267,7 @@ class TracebackEntry: return tbh(None if self._excinfo is None else self._excinfo()) return tbh - def __str__(self): + def __str__(self) -> str: try: fn = str(self.path) except py.error.Error: @@ -273,33 +282,42 @@ class TracebackEntry: return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line) @property - def name(self): + def name(self) -> str: """ co_name of underlying code """ return self.frame.code.raw.co_name -class Traceback(list): +class Traceback(List[TracebackEntry]): """ Traceback objects encapsulate and offer higher level access to Traceback entries. """ - Entry = TracebackEntry - - def __init__(self, tb, excinfo=None): + def __init__( + self, + tb: Union[TracebackType, Iterable[TracebackEntry]], + excinfo: Optional["ReferenceType[ExceptionInfo]"] = None, + ) -> None: """ initialize from given python traceback object and ExceptionInfo """ self._excinfo = excinfo - if hasattr(tb, "tb_next"): + if isinstance(tb, TracebackType): - def f(cur): - while cur is not None: - yield self.Entry(cur, excinfo=excinfo) - cur = cur.tb_next + def f(cur: TracebackType) -> Iterable[TracebackEntry]: + cur_ = cur # type: Optional[TracebackType] + while cur_ is not None: + yield TracebackEntry(cur_, excinfo=excinfo) + cur_ = cur_.tb_next - list.__init__(self, f(tb)) + super().__init__(f(tb)) else: - list.__init__(self, tb) + super().__init__(tb) - def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None): + def cut( + self, + path=None, + lineno: Optional[int] = None, + firstlineno: Optional[int] = None, + excludepath=None, + ) -> "Traceback": """ return a Traceback instance wrapping part of this Traceback by providing any combination of path, lineno and firstlineno, the @@ -325,13 +343,25 @@ class Traceback(list): return Traceback(x._rawentry, self._excinfo) return self - def __getitem__(self, key): - val = super().__getitem__(key) - if isinstance(key, type(slice(0))): - val = self.__class__(val) - return val + @overload + def __getitem__(self, key: int) -> TracebackEntry: + raise NotImplementedError() - def filter(self, fn=lambda x: not x.ishidden()): + @overload # noqa: F811 + def __getitem__(self, key: slice) -> "Traceback": # noqa: F811 + raise NotImplementedError() + + def __getitem__( # noqa: F811 + self, key: Union[int, slice] + ) -> Union[TracebackEntry, "Traceback"]: + if isinstance(key, slice): + return self.__class__(super().__getitem__(key)) + else: + return super().__getitem__(key) + + def filter( + self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden() + ) -> "Traceback": """ return a Traceback instance with certain items removed fn is a function that gets a single argument, a TracebackEntry @@ -343,7 +373,7 @@ class Traceback(list): """ return Traceback(filter(fn, self), self._excinfo) - def getcrashentry(self): + def getcrashentry(self) -> TracebackEntry: """ return last non-hidden traceback entry that lead to the exception of a traceback. """ @@ -353,7 +383,7 @@ class Traceback(list): return entry return self[-1] - def recursionindex(self): + def recursionindex(self) -> Optional[int]: """ return the index of the frame/TracebackEntry where recursion originates if appropriate, None if no recursion occurred """ @@ -449,7 +479,7 @@ class ExceptionInfo(Generic[_E]): assert tup[1] is not None, "no current exception" assert tup[2] is not None, "no current exception" exc_info = (tup[0], tup[1], tup[2]) - return cls.from_exc_info(exc_info, exprinfo) + return ExceptionInfo.from_exc_info(exc_info, exprinfo) @classmethod def for_later(cls) -> "ExceptionInfo[_E]": @@ -543,7 +573,7 @@ class ExceptionInfo(Generic[_E]): def getrepr( self, showlocals: bool = False, - style: str = "long", + style: "_TracebackStyle" = "long", abspath: bool = False, tbfilter: bool = True, funcargs: bool = False, @@ -621,16 +651,16 @@ class FormattedExcinfo: flow_marker = ">" fail_marker = "E" - showlocals = attr.ib(default=False) - style = attr.ib(default="long") - abspath = attr.ib(default=True) - tbfilter = attr.ib(default=True) - funcargs = attr.ib(default=False) - truncate_locals = attr.ib(default=True) - chain = attr.ib(default=True) + showlocals = attr.ib(type=bool, default=False) + style = attr.ib(type="_TracebackStyle", default="long") + abspath = attr.ib(type=bool, default=True) + tbfilter = attr.ib(type=bool, default=True) + funcargs = attr.ib(type=bool, default=False) + truncate_locals = attr.ib(type=bool, default=True) + chain = attr.ib(type=bool, default=True) astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False) - def _getindent(self, source): + def _getindent(self, source: "Source") -> int: # figure out indent for given source try: s = str(source.getstatement(len(source) - 1)) @@ -645,20 +675,27 @@ class FormattedExcinfo: return 0 return 4 + (len(s) - len(s.lstrip())) - def _getentrysource(self, entry): + def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]: source = entry.getsource(self.astcache) if source is not None: source = source.deindent() return source - def repr_args(self, entry): + def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]: if self.funcargs: args = [] for argname, argvalue in entry.frame.getargs(var=True): args.append((argname, saferepr(argvalue))) return ReprFuncArgs(args) + return None - def get_source(self, source, line_index=-1, excinfo=None, short=False) -> List[str]: + def get_source( + self, + source: "Source", + line_index: int = -1, + excinfo: Optional[ExceptionInfo] = None, + short: bool = False, + ) -> List[str]: """ return formatted and marked up source lines. """ import _pytest._code @@ -682,19 +719,21 @@ class FormattedExcinfo: lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) return lines - def get_exconly(self, excinfo, indent=4, markall=False): + def get_exconly( + self, excinfo: ExceptionInfo, indent: int = 4, markall: bool = False + ) -> List[str]: lines = [] - indent = " " * indent + indentstr = " " * indent # get the real exception information out exlines = excinfo.exconly(tryshort=True).split("\n") - failindent = self.fail_marker + indent[1:] + failindent = self.fail_marker + indentstr[1:] for line in exlines: lines.append(failindent + line) if not markall: - failindent = indent + failindent = indentstr return lines - def repr_locals(self, locals): + def repr_locals(self, locals: Dict[str, object]) -> Optional["ReprLocals"]: if self.showlocals: lines = [] keys = [loc for loc in locals if loc[0] != "@"] @@ -719,8 +758,11 @@ class FormattedExcinfo: # # XXX # pprint.pprint(value, stream=self.excinfowriter) return ReprLocals(lines) + return None - def repr_traceback_entry(self, entry, excinfo=None): + def repr_traceback_entry( + self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None + ) -> "ReprEntry": import _pytest._code source = self._getentrysource(entry) @@ -731,9 +773,7 @@ class FormattedExcinfo: line_index = entry.lineno - entry.getfirstlinesource() lines = [] # type: List[str] - style = entry._repr_style - if style is None: - style = self.style + style = entry._repr_style if entry._repr_style is not None else self.style if style in ("short", "long"): short = style == "short" reprargs = self.repr_args(entry) if not short else None @@ -763,7 +803,7 @@ class FormattedExcinfo: path = np return path - def repr_traceback(self, excinfo): + def repr_traceback(self, excinfo: ExceptionInfo) -> "ReprTraceback": traceback = excinfo.traceback if self.tbfilter: traceback = traceback.filter() @@ -781,7 +821,9 @@ class FormattedExcinfo: entries.append(reprentry) return ReprTraceback(entries, extraline, style=self.style) - def _truncate_recursive_traceback(self, traceback): + def _truncate_recursive_traceback( + self, traceback: Traceback + ) -> Tuple[Traceback, Optional[str]]: """ Truncate the given recursive traceback trying to find the starting point of the recursion. @@ -808,7 +850,9 @@ class FormattedExcinfo: max_frames=max_frames, total=len(traceback), ) # type: Optional[str] - traceback = traceback[:max_frames] + traceback[-max_frames:] + # Type ignored because adding two instaces of a List subtype + # currently incorrectly has type List instead of the subtype. + traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore else: if recursionindex is not None: extraline = "!!! Recursion detected (same locals & position)" @@ -865,7 +909,7 @@ class FormattedExcinfo: class TerminalRepr: - def __str__(self): + def __str__(self) -> str: # FYI this is called from pytest-xdist's serialization of exception # information. io = StringIO() @@ -873,7 +917,7 @@ class TerminalRepr: self.toterminal(tw) return io.getvalue().strip() - def __repr__(self): + def __repr__(self) -> str: return "<{} instance at {:0x}>".format(self.__class__, id(self)) def toterminal(self, tw) -> None: @@ -884,7 +928,7 @@ class ExceptionRepr(TerminalRepr): def __init__(self) -> None: self.sections = [] # type: List[Tuple[str, str, str]] - def addsection(self, name, content, sep="-"): + def addsection(self, name: str, content: str, sep: str = "-") -> None: self.sections.append((name, content, sep)) def toterminal(self, tw) -> None: @@ -894,7 +938,12 @@ class ExceptionRepr(TerminalRepr): class ExceptionChainRepr(ExceptionRepr): - def __init__(self, chain): + def __init__( + self, + chain: Sequence[ + Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]] + ], + ) -> None: super().__init__() self.chain = chain # reprcrash and reprtraceback of the outermost (the newest) exception @@ -912,7 +961,9 @@ class ExceptionChainRepr(ExceptionRepr): class ReprExceptionInfo(ExceptionRepr): - def __init__(self, reprtraceback, reprcrash): + def __init__( + self, reprtraceback: "ReprTraceback", reprcrash: "ReprFileLocation" + ) -> None: super().__init__() self.reprtraceback = reprtraceback self.reprcrash = reprcrash @@ -925,7 +976,12 @@ class ReprExceptionInfo(ExceptionRepr): class ReprTraceback(TerminalRepr): entrysep = "_ " - def __init__(self, reprentries, extraline, style): + def __init__( + self, + reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]], + extraline: Optional[str], + style: "_TracebackStyle", + ) -> None: self.reprentries = reprentries self.extraline = extraline self.style = style @@ -950,16 +1006,16 @@ class ReprTraceback(TerminalRepr): class ReprTracebackNative(ReprTraceback): - def __init__(self, tblines): + def __init__(self, tblines: Sequence[str]) -> None: self.style = "native" self.reprentries = [ReprEntryNative(tblines)] self.extraline = None class ReprEntryNative(TerminalRepr): - style = "native" + style = "native" # type: _TracebackStyle - def __init__(self, tblines): + def __init__(self, tblines: Sequence[str]) -> None: self.lines = tblines def toterminal(self, tw) -> None: @@ -967,7 +1023,14 @@ class ReprEntryNative(TerminalRepr): class ReprEntry(TerminalRepr): - def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): + def __init__( + self, + lines: Sequence[str], + reprfuncargs: Optional["ReprFuncArgs"], + reprlocals: Optional["ReprLocals"], + filelocrepr: Optional["ReprFileLocation"], + style: "_TracebackStyle", + ) -> None: self.lines = lines self.reprfuncargs = reprfuncargs self.reprlocals = reprlocals @@ -976,6 +1039,7 @@ class ReprEntry(TerminalRepr): def toterminal(self, tw) -> None: if self.style == "short": + assert self.reprfileloc is not None self.reprfileloc.toterminal(tw) for line in self.lines: red = line.startswith("E ") @@ -994,14 +1058,14 @@ class ReprEntry(TerminalRepr): tw.line("") self.reprfileloc.toterminal(tw) - def __str__(self): + def __str__(self) -> str: return "{}\n{}\n{}".format( "\n".join(self.lines), self.reprlocals, self.reprfileloc ) class ReprFileLocation(TerminalRepr): - def __init__(self, path, lineno, message): + def __init__(self, path, lineno: int, message: str) -> None: self.path = str(path) self.lineno = lineno self.message = message @@ -1018,7 +1082,7 @@ class ReprFileLocation(TerminalRepr): class ReprLocals(TerminalRepr): - def __init__(self, lines): + def __init__(self, lines: Sequence[str]) -> None: self.lines = lines def toterminal(self, tw) -> None: @@ -1027,7 +1091,7 @@ class ReprLocals(TerminalRepr): class ReprFuncArgs(TerminalRepr): - def __init__(self, args): + def __init__(self, args: Sequence[Tuple[str, object]]) -> None: self.args = args def toterminal(self, tw) -> None: @@ -1049,13 +1113,11 @@ class ReprFuncArgs(TerminalRepr): tw.line("") -def getrawcode(obj, trycall=True): +def getrawcode(obj, trycall: bool = True): """ return code object for given function. """ try: return obj.__code__ except AttributeError: - obj = getattr(obj, "im_func", obj) - obj = getattr(obj, "func_code", obj) obj = getattr(obj, "f_code", obj) obj = getattr(obj, "__code__", obj) if trycall and not hasattr(obj, "co_firstlineno"): @@ -1079,7 +1141,7 @@ _PYTEST_DIR = py.path.local(_pytest.__file__).dirpath() _PY_DIR = py.path.local(py.__file__).dirpath() -def filter_traceback(entry): +def filter_traceback(entry: TracebackEntry) -> bool: """Return True if a TracebackEntry instance should be removed from tracebacks: * dynamically generated code (no code to show up for it); * internal traceback from pytest or its internal libraries, py and pluggy. diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 1e9dd5031..ac3ee231e 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -8,6 +8,7 @@ import warnings from ast import PyCF_ONLY_AST as _AST_FLAG from bisect import bisect_right from types import FrameType +from typing import Iterator from typing import List from typing import Optional from typing import Sequence @@ -60,7 +61,7 @@ class Source: raise NotImplementedError() @overload # noqa: F811 - def __getitem__(self, key: slice) -> "Source": + def __getitem__(self, key: slice) -> "Source": # noqa: F811 raise NotImplementedError() def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: F811 @@ -73,6 +74,9 @@ class Source: newsource.lines = self.lines[key.start : key.stop] return newsource + def __iter__(self) -> Iterator[str]: + return iter(self.lines) + def __len__(self) -> int: return len(self.lines) @@ -335,7 +339,9 @@ def getstatementrange_ast( block_finder.started = source.lines[start][0].isspace() it = ((x + "\n") for x in source.lines[start:end]) try: - for tok in tokenize.generate_tokens(lambda: next(it)): + # Type ignored until next mypy release. + # https://github.com/python/typeshed/commit/c0d46a20353b733befb85d8b9cc24e5b0bcd8f9a + for tok in tokenize.generate_tokens(lambda: next(it)): # type: ignore block_finder.tokeneater(*tok) except (inspect.EndOfBlock, IndentationError): end = block_finder.last + start diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 7fded872d..884f0a21e 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -80,3 +80,24 @@ def saferepr(obj: Any, maxsize: int = 240) -> str: around the Repr/reprlib functionality of the standard 2.6 lib. """ return SafeRepr(maxsize).repr(obj) + + +class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter): + """PrettyPrinter that always dispatches (regardless of width).""" + + def _format(self, object, stream, indent, allowance, context, level): + p = self._dispatch.get(type(object).__repr__, None) + + objid = id(object) + if objid in context or p is None: + return super()._format(object, stream, indent, allowance, context, level) + + context[objid] = 1 + p(self, object, stream, indent, allowance, context, level + 1) + del context[objid] + + +def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False): + return AlwaysDispatchingPrettyPrinter( + indent=1, width=80, depth=None, compact=False + ).pformat(object) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index b84929936..6bfb876e4 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -13,7 +13,6 @@ import struct import sys import tokenize import types -from pathlib import Path from typing import Dict from typing import List from typing import Optional @@ -28,6 +27,7 @@ from _pytest.assertion.util import ( # noqa: F401 ) from _pytest.compat import fspath from _pytest.pathlib import fnmatch_ex +from _pytest.pathlib import Path from _pytest.pathlib import PurePath # pytest caches rewritten pycs in pycache dirs @@ -807,8 +807,9 @@ class AssertionRewriter(ast.NodeVisitor): ) ) + negation = ast.UnaryOp(ast.Not(), top_condition) + if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook - negation = ast.UnaryOp(ast.Not(), top_condition) msg = self.pop_format_context(ast.Str(explanation)) # Failed @@ -860,7 +861,6 @@ class AssertionRewriter(ast.NodeVisitor): else: # Original assertion rewriting # Create failure message. body = self.expl_stmts - negation = ast.UnaryOp(ast.Not(), top_condition) self.statements.append(ast.If(negation, body, [])) if assert_.msg: assertmsg = self.helper("_format_assertmsg", assert_.msg) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 7521c08e4..67f8d4618 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -13,6 +13,7 @@ from typing import Tuple import _pytest._code from _pytest import outcomes +from _pytest._io.saferepr import _pformat_dispatch from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr from _pytest.compat import ATTRS_EQ_FIELD @@ -270,15 +271,8 @@ def _compare_eq_iterable( lines_left = len(left_formatting) lines_right = len(right_formatting) if lines_left != lines_right: - if lines_left > lines_right: - max_width = min(len(x) for x in left_formatting) - else: - max_width = min(len(x) for x in right_formatting) - - right_formatting = pprint.pformat(right, width=max_width).splitlines() - lines_right = len(right_formatting) - left_formatting = pprint.pformat(left, width=max_width).splitlines() - lines_left = len(left_formatting) + left_formatting = _pformat_dispatch(left).splitlines() + right_formatting = _pformat_dispatch(right).splitlines() if lines_left > 1 or lines_right > 1: _surrounding_parens_on_own_lines(left_formatting) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 3c60fdb33..6e53545d6 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -125,13 +125,14 @@ class Cache: return if not cache_dir_exists_already: self._ensure_supporting_files() + data = json.dumps(value, indent=2, sort_keys=True) try: f = path.open("w") except (IOError, OSError): self.warn("cache could not write path {path}", path=path) else: with f: - json.dump(value, f, indent=2, sort_keys=True) + f.write(data) def _ensure_supporting_files(self): """Create supporting files in the cache dir that are not really part of the cache.""" diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 83947d3eb..8dd74b577 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -10,7 +10,14 @@ import sys from contextlib import contextmanager from inspect import Parameter from inspect import signature +from typing import Any +from typing import Callable +from typing import Generic +from typing import Optional from typing import overload +from typing import Tuple +from typing import TypeVar +from typing import Union import attr import py @@ -20,6 +27,13 @@ from _pytest._io.saferepr import saferepr from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME +if False: # TYPE_CHECKING + from typing import Type # noqa: F401 (used in type string) + + +_T = TypeVar("_T") +_S = TypeVar("_S") + NOTSET = object() @@ -29,12 +43,12 @@ MODULE_NOT_FOUND_ERROR = ( if sys.version_info >= (3, 8): - from importlib import metadata as importlib_metadata # noqa: F401 + from importlib import metadata as importlib_metadata else: import importlib_metadata # noqa: F401 -def _format_args(func): +def _format_args(func: Callable[..., Any]) -> str: return str(signature(func)) @@ -55,12 +69,12 @@ else: fspath = os.fspath -def is_generator(func): +def is_generator(func: object) -> bool: genfunc = inspect.isgeneratorfunction(func) return genfunc and not iscoroutinefunction(func) -def iscoroutinefunction(func): +def iscoroutinefunction(func: object) -> bool: """ Return True if func is a coroutine function (a function defined with async def syntax, and doesn't contain yield), or a function decorated with @@ -73,7 +87,7 @@ def iscoroutinefunction(func): return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False) -def getlocation(function, curdir=None): +def getlocation(function, curdir=None) -> str: function = get_real_func(function) fn = py.path.local(inspect.getfile(function)) lineno = function.__code__.co_firstlineno @@ -82,7 +96,7 @@ def getlocation(function, curdir=None): return "%s:%d" % (fn, lineno + 1) -def num_mock_patch_args(function): +def num_mock_patch_args(function) -> int: """ return number of arguments used up by mock arguments (if any) """ patchings = getattr(function, "patchings", None) if not patchings: @@ -101,7 +115,13 @@ def num_mock_patch_args(function): ) -def getfuncargnames(function, *, name: str = "", is_method=False, cls=None): +def getfuncargnames( + function: Callable[..., Any], + *, + name: str = "", + is_method: bool = False, + cls: Optional[type] = None +) -> Tuple[str, ...]: """Returns the names of a function's mandatory arguments. This should return the names of all function arguments that: @@ -169,7 +189,7 @@ else: from contextlib import nullcontext # noqa -def get_default_arg_names(function): +def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]: # Note: this code intentionally mirrors the code at the beginning of getfuncargnames, # to get the arguments which were excluded from its result because they had default values return tuple( @@ -188,18 +208,18 @@ _non_printable_ascii_translate_table.update( ) -def _translate_non_printable(s): +def _translate_non_printable(s: str) -> str: return s.translate(_non_printable_ascii_translate_table) STRING_TYPES = bytes, str -def _bytes_to_ascii(val): +def _bytes_to_ascii(val: bytes) -> str: return val.decode("ascii", "backslashreplace") -def ascii_escaped(val): +def ascii_escaped(val: Union[bytes, str]): """If val is pure ascii, returns it as a str(). Otherwise, escapes bytes objects into a sequence of escaped bytes: @@ -296,7 +316,7 @@ def getimfunc(func): return func -def safe_getattr(object, name, default): +def safe_getattr(object: Any, name: str, default: Any) -> Any: """ Like getattr but return default upon any Exception or any OutcomeException. Attribute access can potentially fail for 'evil' Python objects. @@ -310,7 +330,7 @@ def safe_getattr(object, name, default): return default -def safe_isclass(obj): +def safe_isclass(obj: object) -> bool: """Ignore any exception via isinstance on Python 3.""" try: return inspect.isclass(obj) @@ -331,39 +351,26 @@ COLLECT_FAKEMODULE_ATTRIBUTES = ( ) -def _setup_collect_fakemodule(): +def _setup_collect_fakemodule() -> None: from types import ModuleType import pytest - pytest.collect = ModuleType("pytest.collect") - pytest.collect.__all__ = [] # used for setns + # Types ignored because the module is created dynamically. + pytest.collect = ModuleType("pytest.collect") # type: ignore + pytest.collect.__all__ = [] # type: ignore # used for setns for attr_name in COLLECT_FAKEMODULE_ATTRIBUTES: - setattr(pytest.collect, attr_name, getattr(pytest, attr_name)) + setattr(pytest.collect, attr_name, getattr(pytest, attr_name)) # type: ignore class CaptureIO(io.TextIOWrapper): - def __init__(self): + def __init__(self) -> None: super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) - def getvalue(self): + def getvalue(self) -> str: + assert isinstance(self.buffer, io.BytesIO) return self.buffer.getvalue().decode("UTF-8") -class FuncargnamesCompatAttr: - """ helper class so that Metafunc, Function and FixtureRequest - don't need to each define the "funcargnames" compatibility attribute. - """ - - @property - def funcargnames(self): - """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" - import warnings - from _pytest.deprecated import FUNCARGNAMES - - warnings.warn(FUNCARGNAMES, stacklevel=2) - return self.fixturenames - - if sys.version_info < (3, 5, 2): # pragma: no cover def overload(f): # noqa: F811 @@ -374,3 +381,33 @@ if getattr(attr, "__version_info__", ()) >= (19, 2): ATTRS_EQ_FIELD = "eq" else: ATTRS_EQ_FIELD = "cmp" + + +if sys.version_info >= (3, 8): + from functools import cached_property +else: + + class cached_property(Generic[_S, _T]): + __slots__ = ("func", "__doc__") + + def __init__(self, func: Callable[[_S], _T]) -> None: + self.func = func + self.__doc__ = func.__doc__ + + @overload + def __get__( + self, instance: None, owner: Optional["Type[_S]"] = ... + ) -> "cached_property[_S, _T]": + raise NotImplementedError() + + @overload # noqa: F811 + def __get__( # noqa: F811 + self, instance: _S, owner: Optional["Type[_S]"] = ... + ) -> _T: + raise NotImplementedError() + + def __get__(self, instance, owner=None): # noqa: F811 + if instance is None: + return self + value = instance.__dict__[self.func.__name__] = self.func(instance) + return value diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 8e11b56e5..070d24bf5 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -8,7 +8,6 @@ import sys import types import warnings from functools import lru_cache -from pathlib import Path from types import TracebackType from typing import Any from typing import Callable @@ -40,11 +39,14 @@ from _pytest._code import filter_traceback from _pytest.compat import importlib_metadata from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import Path from _pytest.warning_types import PytestConfigWarning if False: # TYPE_CHECKING from typing import Type + from .argparsing import Argument + hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") @@ -131,13 +133,7 @@ def directory_arg(path, optname): # Plugins that cannot be disabled via "-p no:X" currently. -essential_plugins = ( # fmt: off - "mark", - "main", - "runner", - "fixtures", - "helpconfig", # Provides -p. -) # fmt: on +essential_plugins = ("mark", "main", "runner", "fixtures", "helpconfig") # Provides -p. default_plugins = essential_plugins + ( "python", @@ -154,7 +150,6 @@ default_plugins = essential_plugins + ( "assertion", "junitxml", "resultlog", - "report_log", "doctest", "cacheprovider", "freeze_support", @@ -588,7 +583,7 @@ class PytestPluginManager(PluginManager): _issue_warning_captured( PytestConfigWarning("skipped plugin {!r}: {}".format(modname, e.msg)), self.hook, - stacklevel=1, + stacklevel=2, ) else: mod = sys.modules[importspec] @@ -680,7 +675,7 @@ class Config: plugins = attr.ib() dir = attr.ib(type=Path) - def __init__(self, pluginmanager, *, invocation_params=None): + def __init__(self, pluginmanager, *, invocation_params=None) -> None: from .argparsing import Parser, FILE_OR_DIR if invocation_params is None: @@ -793,11 +788,11 @@ class Config: config.pluginmanager.consider_pluginarg(x) return config - def _processopt(self, opt): + def _processopt(self, opt: "Argument") -> None: for name in opt._short_opts + opt._long_opts: self._opt2dest[name] = opt.dest - if hasattr(opt, "default") and opt.dest: + if hasattr(opt, "default"): if not hasattr(self.option, opt.dest): setattr(self.option, opt.dest, opt.default) @@ -805,7 +800,7 @@ class Config: def pytest_load_initial_conftests(self, early_config): self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) - def _initini(self, args) -> None: + def _initini(self, args: Sequence[str]) -> None: ns, unknown_args = self._parser.parse_known_and_unknown_args( args, namespace=copy.copy(self.option) ) @@ -822,7 +817,7 @@ class Config: self._parser.addini("minversion", "minimally required pytest version") self._override_ini = ns.override_ini or () - def _consider_importhook(self, args): + def _consider_importhook(self, args: Sequence[str]) -> None: """Install the PEP 302 import hook if using assertion rewriting. Needs to parse the --assert= option from the commandline @@ -862,19 +857,19 @@ class Config: for name in _iter_rewritable_modules(package_files): hook.mark_rewrite(name) - def _validate_args(self, args, via): + def _validate_args(self, args: List[str], via: str) -> List[str]: """Validate known args.""" - self._parser._config_source_hint = via + self._parser._config_source_hint = via # type: ignore try: self._parser.parse_known_and_unknown_args( args, namespace=copy.copy(self.option) ) finally: - del self._parser._config_source_hint + del self._parser._config_source_hint # type: ignore return args - def _preparse(self, args, addopts=True): + def _preparse(self, args: List[str], addopts: bool = True) -> None: if addopts: env_addopts = os.environ.get("PYTEST_ADDOPTS", "") if len(env_addopts): @@ -938,7 +933,7 @@ class Config: ) ) - def parse(self, args, addopts=True): + def parse(self, args: List[str], addopts: bool = True) -> None: # parse given cmdline arguments into this config object. assert not hasattr( self, "args" @@ -949,7 +944,7 @@ class Config: self._preparse(args, addopts=addopts) # XXX deprecated hook: self.hook.pytest_cmdline_preparse(config=self, args=args) - self._parser.after_preparse = True + self._parser.after_preparse = True # type: ignore try: args = self._parser.parse_setoption( args, self.option, namespace=self.option @@ -974,7 +969,7 @@ class Config: def getini(self, name: str): """ return configuration value from an :ref:`ini file `. If the specified name hasn't been registered through a prior - :py:func:`parser.addini <_pytest.config.Parser.addini>` + :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>` call (usually from a plugin), a ValueError is raised. """ try: return self._inicache[name] diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 9b526ff3e..d0870ed56 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -3,15 +3,24 @@ import sys import warnings from gettext import gettext from typing import Any +from typing import Callable +from typing import cast from typing import Dict from typing import List +from typing import Mapping from typing import Optional +from typing import Sequence from typing import Tuple +from typing import Union import py from _pytest.config.exceptions import UsageError +if False: # TYPE_CHECKING + from typing import NoReturn + from typing_extensions import Literal # noqa: F401 + FILE_OR_DIR = "file_or_dir" @@ -22,9 +31,13 @@ class Parser: there's an error processing the command line arguments. """ - prog = None + prog = None # type: Optional[str] - def __init__(self, usage=None, processopt=None): + def __init__( + self, + usage: Optional[str] = None, + processopt: Optional[Callable[["Argument"], None]] = None, + ) -> None: self._anonymous = OptionGroup("custom options", parser=self) self._groups = [] # type: List[OptionGroup] self._processopt = processopt @@ -33,12 +46,14 @@ class Parser: self._ininames = [] # type: List[str] self.extra_info = {} # type: Dict[str, Any] - def processoption(self, option): + def processoption(self, option: "Argument") -> None: if self._processopt: if option.dest: self._processopt(option) - def getgroup(self, name, description="", after=None): + def getgroup( + self, name: str, description: str = "", after: Optional[str] = None + ) -> "OptionGroup": """ get (or create) a named option Group. :name: name of the option group. @@ -47,7 +62,7 @@ class Parser: The returned group object has an ``addoption`` method with the same signature as :py:func:`parser.addoption - <_pytest.config.Parser.addoption>` but will be shown in the + <_pytest.config.argparsing.Parser.addoption>` but will be shown in the respective group in the output of ``pytest. --help``. """ for group in self._groups: @@ -61,13 +76,13 @@ class Parser: self._groups.insert(i + 1, group) return group - def addoption(self, *opts, **attrs): + def addoption(self, *opts: str, **attrs: Any) -> None: """ register a command line option. :opts: option names, can be short or long options. - :attrs: same attributes which the ``add_option()`` function of the + :attrs: same attributes which the ``add_argument()`` function of the `argparse library - `_ + `_ accepts. After command line parsing options are available on the pytest config @@ -77,7 +92,11 @@ class Parser: """ self._anonymous.addoption(*opts, **attrs) - def parse(self, args, namespace=None): + def parse( + self, + args: Sequence[Union[str, py.path.local]], + namespace: Optional[argparse.Namespace] = None, + ) -> argparse.Namespace: from _pytest._argcomplete import try_argcomplete self.optparser = self._getparser() @@ -98,27 +117,37 @@ class Parser: n = option.names() a = option.attrs() arggroup.add_argument(*n, **a) + file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*") # bash like autocompletion for dirs (appending '/') # Type ignored because typeshed doesn't know about argcomplete. - optparser.add_argument( # type: ignore - FILE_OR_DIR, nargs="*" - ).completer = filescompleter + file_or_dir_arg.completer = filescompleter # type: ignore return optparser - def parse_setoption(self, args, option, namespace=None): + def parse_setoption( + self, + args: Sequence[Union[str, py.path.local]], + option: argparse.Namespace, + namespace: Optional[argparse.Namespace] = None, + ) -> List[str]: parsedoption = self.parse(args, namespace=namespace) for name, value in parsedoption.__dict__.items(): setattr(option, name, value) - return getattr(parsedoption, FILE_OR_DIR) + return cast(List[str], getattr(parsedoption, FILE_OR_DIR)) - def parse_known_args(self, args, namespace=None) -> argparse.Namespace: + def parse_known_args( + self, + args: Sequence[Union[str, py.path.local]], + namespace: Optional[argparse.Namespace] = None, + ) -> argparse.Namespace: """parses and returns a namespace object with known arguments at this point. """ return self.parse_known_and_unknown_args(args, namespace=namespace)[0] def parse_known_and_unknown_args( - self, args, namespace=None + self, + args: Sequence[Union[str, py.path.local]], + namespace: Optional[argparse.Namespace] = None, ) -> Tuple[argparse.Namespace, List[str]]: """parses and returns a namespace object with known arguments, and the remaining arguments unknown at this point. @@ -127,7 +156,13 @@ class Parser: args = [str(x) if isinstance(x, py.path.local) else x for x in args] return optparser.parse_known_args(args, namespace=namespace) - def addini(self, name, help, type=None, default=None): + def addini( + self, + name: str, + help: str, + type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None, + default=None, + ) -> None: """ register an ini-file option. :name: name of the ini-variable @@ -149,11 +184,11 @@ class ArgumentError(Exception): inconsistent arguments. """ - def __init__(self, msg, option): + def __init__(self, msg: str, option: Union["Argument", str]) -> None: self.msg = msg self.option_id = str(option) - def __str__(self): + def __str__(self) -> str: if self.option_id: return "option {}: {}".format(self.option_id, self.msg) else: @@ -170,12 +205,11 @@ class Argument: _typ_map = {"int": int, "string": str, "float": float, "complex": complex} - def __init__(self, *names, **attrs): + def __init__(self, *names: str, **attrs: Any) -> None: """store parms in private vars for use in add_argument""" self._attrs = attrs self._short_opts = [] # type: List[str] self._long_opts = [] # type: List[str] - self.dest = attrs.get("dest") if "%default" in (attrs.get("help") or ""): warnings.warn( 'pytest now uses argparse. "%default" should be' @@ -221,23 +255,25 @@ class Argument: except KeyError: pass self._set_opt_strings(names) - if not self.dest: - if self._long_opts: - self.dest = self._long_opts[0][2:].replace("-", "_") - else: - try: - self.dest = self._short_opts[0][1:] - except IndexError: - raise ArgumentError("need a long or short option", self) + dest = attrs.get("dest") # type: Optional[str] + if dest: + self.dest = dest + elif self._long_opts: + self.dest = self._long_opts[0][2:].replace("-", "_") + else: + try: + self.dest = self._short_opts[0][1:] + except IndexError: + self.dest = "???" # Needed for the error repr. + raise ArgumentError("need a long or short option", self) - def names(self): + def names(self) -> List[str]: return self._short_opts + self._long_opts - def attrs(self): + def attrs(self) -> Mapping[str, Any]: # update any attributes set by processopt attrs = "default dest help".split() - if self.dest: - attrs.append(self.dest) + attrs.append(self.dest) for attr in attrs: try: self._attrs[attr] = getattr(self, attr) @@ -250,7 +286,7 @@ class Argument: self._attrs["help"] = a return self._attrs - def _set_opt_strings(self, opts): + def _set_opt_strings(self, opts: Sequence[str]) -> None: """directly from optparse might not be necessary as this is passed to argparse later on""" @@ -293,13 +329,15 @@ class Argument: class OptionGroup: - def __init__(self, name, description="", parser=None): + def __init__( + self, name: str, description: str = "", parser: Optional[Parser] = None + ) -> None: self.name = name self.description = description self.options = [] # type: List[Argument] self.parser = parser - def addoption(self, *optnames, **attrs): + def addoption(self, *optnames: str, **attrs: Any) -> None: """ add an option to this group. if a shortened version of a long option is specified it will @@ -315,11 +353,11 @@ class OptionGroup: option = Argument(*optnames, **attrs) self._addoption_instance(option, shortupper=False) - def _addoption(self, *optnames, **attrs): + def _addoption(self, *optnames: str, **attrs: Any) -> None: option = Argument(*optnames, **attrs) self._addoption_instance(option, shortupper=True) - def _addoption_instance(self, option, shortupper=False): + def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None: if not shortupper: for opt in option._short_opts: if opt[0] == "-" and opt[1].islower(): @@ -330,9 +368,12 @@ class OptionGroup: class MyOptionParser(argparse.ArgumentParser): - def __init__(self, parser, extra_info=None, prog=None): - if not extra_info: - extra_info = {} + def __init__( + self, + parser: Parser, + extra_info: Optional[Dict[str, Any]] = None, + prog: Optional[str] = None, + ) -> None: self._parser = parser argparse.ArgumentParser.__init__( self, @@ -344,34 +385,42 @@ class MyOptionParser(argparse.ArgumentParser): ) # extra_info is a dict of (param -> value) to display if there's # an usage error to provide more contextual information to the user - self.extra_info = extra_info + self.extra_info = extra_info if extra_info else {} - def error(self, message): + def error(self, message: str) -> "NoReturn": """Transform argparse error message into UsageError.""" msg = "{}: error: {}".format(self.prog, message) if hasattr(self._parser, "_config_source_hint"): - msg = "{} ({})".format(msg, self._parser._config_source_hint) + # Type ignored because the attribute is set dynamically. + msg = "{} ({})".format(msg, self._parser._config_source_hint) # type: ignore raise UsageError(self.format_usage() + msg) - def parse_args(self, args=None, namespace=None): + # Type ignored because typeshed has a very complex type in the superclass. + def parse_args( # type: ignore + self, + args: Optional[Sequence[str]] = None, + namespace: Optional[argparse.Namespace] = None, + ) -> argparse.Namespace: """allow splitting of positional arguments""" - args, argv = self.parse_known_args(args, namespace) - if argv: - for arg in argv: + parsed, unrecognized = self.parse_known_args(args, namespace) + if unrecognized: + for arg in unrecognized: if arg and arg[0] == "-": - lines = ["unrecognized arguments: %s" % (" ".join(argv))] + lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))] for k, v in sorted(self.extra_info.items()): lines.append(" {}: {}".format(k, v)) self.error("\n".join(lines)) - getattr(args, FILE_OR_DIR).extend(argv) - return args + getattr(parsed, FILE_OR_DIR).extend(unrecognized) + return parsed if sys.version_info[:2] < (3, 9): # pragma: no cover # Backport of https://github.com/python/cpython/pull/14316 so we can # disable long --argument abbreviations without breaking short flags. - def _parse_optional(self, arg_string): + def _parse_optional( + self, arg_string: str + ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]: if not arg_string: return None if not arg_string[0] in self.prefix_chars: @@ -395,7 +444,7 @@ class MyOptionParser(argparse.ArgumentParser): options = ", ".join(option for _, option, _ in option_tuples) self.error(msg % {"option": arg_string, "matches": options}) elif len(option_tuples) == 1: - option_tuple, = option_tuples + (option_tuple,) = option_tuples return option_tuple if self._negative_number_matcher.match(arg_string): if not self._has_negative_number_optionals: @@ -409,49 +458,45 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): """shorten help for long options that differ only in extra hyphens - collapse **long** options that are the same except for extra hyphens - - special action attribute map_long_option allows suppressing additional - long options - shortcut if there are only two options and one of them is a short one - cache result on action object as this is called at least 2 times """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Use more accurate terminal width via pylib.""" if "width" not in kwargs: kwargs["width"] = py.io.get_terminal_width() super().__init__(*args, **kwargs) - def _format_action_invocation(self, action): + def _format_action_invocation(self, action: argparse.Action) -> str: orgstr = argparse.HelpFormatter._format_action_invocation(self, action) if orgstr and orgstr[0] != "-": # only optional arguments return orgstr - res = getattr(action, "_formatted_action_invocation", None) + res = getattr( + action, "_formatted_action_invocation", None + ) # type: Optional[str] if res: return res options = orgstr.split(", ") if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2): # a shortcut for '-h, --help' or '--abc', '-a' - action._formatted_action_invocation = orgstr + action._formatted_action_invocation = orgstr # type: ignore return orgstr return_list = [] - option_map = getattr(action, "map_long_option", {}) - if option_map is None: - option_map = {} short_long = {} # type: Dict[str, str] for option in options: if len(option) == 2 or option[2] == " ": continue if not option.startswith("--"): raise ArgumentError( - 'long optional argument without "--": [%s]' % (option), self + 'long optional argument without "--": [%s]' % (option), option ) xxoption = option[2:] - if xxoption.split()[0] not in option_map: - shortened = xxoption.replace("-", "") - if shortened not in short_long or len(short_long[shortened]) < len( - xxoption - ): - short_long[shortened] = xxoption + shortened = xxoption.replace("-", "") + if shortened not in short_long or len(short_long[shortened]) < len( + xxoption + ): + short_long[shortened] = xxoption # now short_long has been filled out to the longest with dashes # **and** we keep the right option ordering from add_argument for option in options: @@ -459,5 +504,6 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): return_list.append(option) if option[2:] == short_long.get(option.replace("-", "")): return_list.append(option.replace(" ", "=", 1)) - action._formatted_action_invocation = ", ".join(return_list) - return action._formatted_action_invocation + formatted_action_invocation = ", ".join(return_list) + action._formatted_action_invocation = formatted_action_invocation # type: ignore + return formatted_action_invocation diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 5186067ef..1fdc37c04 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -9,6 +9,7 @@ All constants defined in this module should be either PytestWarning instances or in case of warnings which need to format their messages. """ from _pytest.warning_types import PytestDeprecationWarning +from _pytest.warning_types import UnformattedWarning # set of plugins which have been integrated into the core; we use this list to ignore # them during registration to avoid conflicts @@ -26,7 +27,7 @@ FUNCARGNAMES = PytestDeprecationWarning( RESULT_LOG = PytestDeprecationWarning( - "--result-log is deprecated and scheduled for removal in pytest 6.0.\n" + "--result-log is deprecated, please try the new pytest-reportlog plugin.\n" "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." ) @@ -34,3 +35,13 @@ FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning( "Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them " "as a keyword argument instead." ) + +NODE_USE_FROM_PARENT = UnformattedWarning( + PytestDeprecationWarning, + "direct construction of {name} has been deprecated, please use {name}.from_parent", +) + +JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning( + "The 'junit_family' default value will change to 'xunit2' in pytest 6.0.\n" + "Add 'junit_family=legacy' to your pytest.ini file to silence this warning and make your suite compatible." +) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index f7d96257e..66fbf8396 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -108,9 +108,9 @@ def pytest_collect_file(path, parent): config = parent.config if path.ext == ".py": if config.option.doctestmodules and not _is_setup_py(config, path, parent): - return DoctestModule(path, parent) + return DoctestModule.from_parent(parent, fspath=path) elif _is_doctest(config, path, parent): - return DoctestTextfile(path, parent) + return DoctestTextfile.from_parent(parent, fspath=path) def _is_setup_py(config, path, parent): @@ -215,6 +215,10 @@ class DoctestItem(pytest.Item): self.obj = None self.fixture_request = None + @classmethod + def from_parent(cls, parent, *, name, runner, dtest): + return cls._create(name=name, parent=parent, runner=runner, dtest=dtest) + def setup(self): if self.dtest is not None: self.fixture_request = _setup_fixtures(self) @@ -370,7 +374,9 @@ class DoctestTextfile(pytest.Module): parser = doctest.DocTestParser() test = parser.get_doctest(text, globs, name, filename, 0) if test.examples: - yield DoctestItem(test.name, self, runner, test) + yield DoctestItem.from_parent( + self, name=test.name, runner=runner, dtest=test + ) def _check_all_skipped(test): @@ -467,7 +473,9 @@ class DoctestModule(pytest.Module): for test in finder.find(module, module.__name__): if test.examples: # skip empty doctests - yield DoctestItem(test.name, self, runner, test) + yield DoctestItem.from_parent( + self, name=test.name, runner=runner, dtest=test + ) def _setup_fixtures(doctest_item): diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index fc55ef2cf..44802e000 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -18,7 +18,6 @@ from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper -from _pytest.compat import FuncargnamesCompatAttr from _pytest.compat import get_real_func from _pytest.compat import get_real_method from _pytest.compat import getfslineno @@ -29,6 +28,7 @@ from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import safe_getattr from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS +from _pytest.deprecated import FUNCARGNAMES from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -36,6 +36,7 @@ if False: # TYPE_CHECKING from typing import Type from _pytest import nodes + from _pytest.main import Session @attr.s(frozen=True) @@ -44,7 +45,7 @@ class PseudoFixtureDef: scope = attr.ib() -def pytest_sessionstart(session): +def pytest_sessionstart(session: "Session"): import _pytest.python import _pytest.nodes @@ -336,7 +337,7 @@ class FuncFixtureInfo: self.names_closure[:] = sorted(closure, key=self.names_closure.index) -class FixtureRequest(FuncargnamesCompatAttr): +class FixtureRequest: """ A request for a fixture from a test or fixture function. A request object gives access to the requesting test context @@ -363,6 +364,12 @@ class FixtureRequest(FuncargnamesCompatAttr): result.extend(set(self._fixture_defs).difference(result)) return result + @property + def funcargnames(self): + """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + warnings.warn(FUNCARGNAMES, stacklevel=2) + return self.fixturenames + @property def node(self): """ underlying collection node (depends on current request scope)""" @@ -504,13 +511,11 @@ class FixtureRequest(FuncargnamesCompatAttr): values.append(fixturedef) current = current._parent_request - def _compute_fixture_value(self, fixturedef): + def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: """ Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will force the FixtureDef object to throw away any previous results and compute a new fixture value, which will be stored into the FixtureDef object itself. - - :param FixtureDef fixturedef: """ # prepare a subrequest object before calling fixture function # (latter managed by fixturedef) @@ -538,9 +543,8 @@ class FixtureRequest(FuncargnamesCompatAttr): if has_params: frame = inspect.stack()[3] frameinfo = inspect.getframeinfo(frame[0]) - source_path = frameinfo.filename + source_path = py.path.local(frameinfo.filename) source_lineno = frameinfo.lineno - source_path = py.path.local(source_path) if source_path.relto(funcitem.config.rootdir): source_path = source_path.relto(funcitem.config.rootdir) msg = ( diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 8b45c5f9b..74dff1e82 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -45,10 +45,10 @@ def pytest_addoption(parser, pluginmanager): files situated at the tests root directory due to how pytest :ref:`discovers plugins during startup `. - :arg _pytest.config.Parser parser: To add command line options, call - :py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`. + :arg _pytest.config.argparsing.Parser parser: To add command line options, call + :py:func:`parser.addoption(...) <_pytest.config.argparsing.Parser.addoption>`. To add ini-file values call :py:func:`parser.addini(...) - <_pytest.config.Parser.addini>`. + <_pytest.config.argparsing.Parser.addini>`. :arg _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager, which can be used to install :py:func:`hookspec`'s or :py:func:`hookimpl`'s @@ -148,7 +148,7 @@ def pytest_load_initial_conftests(early_config, parser, args): :param _pytest.config.Config early_config: pytest config object :param list[str] args: list of arguments passed on the command line - :param _pytest.config.Parser parser: to add command line options + :param _pytest.config.argparsing.Parser parser: to add command line options """ @@ -562,7 +562,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): @hookspec(historic=True) -def pytest_warning_captured(warning_message, when, item): +def pytest_warning_captured(warning_message, when, item, location): """ Process a warning captured by the internal pytest warnings plugin. @@ -582,6 +582,10 @@ def pytest_warning_captured(warning_message, when, item): in a future release. The item being executed if ``when`` is ``"runtest"``, otherwise ``None``. + + :param tuple location: + Holds information about the execution context of the captured warning (filename, linenumber, function). + ``function`` evaluates to when the execution context is at the module level. """ diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index fb951106f..9cf22705e 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -19,8 +19,10 @@ from datetime import datetime import py import pytest +from _pytest import deprecated from _pytest import nodes from _pytest.config import filename_arg +from _pytest.warnings import _issue_warning_captured class Junit(py.xml.Namespace): @@ -421,9 +423,7 @@ def pytest_addoption(parser): default="total", ) # choices=['total', 'call']) parser.addini( - "junit_family", - "Emit XML for schema: one of legacy|xunit1|xunit2", - default="xunit1", + "junit_family", "Emit XML for schema: one of legacy|xunit1|xunit2", default=None ) @@ -431,13 +431,17 @@ def pytest_configure(config): xmlpath = config.option.xmlpath # prevent opening xmllog on slave nodes (xdist) if xmlpath and not hasattr(config, "slaveinput"): + junit_family = config.getini("junit_family") + if not junit_family: + _issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2) + junit_family = "xunit1" config._xml = LogXML( xmlpath, config.option.junitprefix, config.getini("junit_suite_name"), config.getini("junit_logging"), config.getini("junit_duration_report"), - config.getini("junit_family"), + junit_family, config.getini("junit_log_passing_tests"), ) config.pluginmanager.register(config._xml) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index b4261c188..53bfbeb5a 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -15,6 +15,7 @@ from _pytest import nodes from _pytest.config import directory_arg from _pytest.config import hookimpl from _pytest.config import UsageError +from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit from _pytest.runner import collect_one_node from _pytest.runner import SetupState @@ -184,7 +185,7 @@ def pytest_addoption(parser): def wrap_session(config, doit): """Skeleton command line program""" - session = Session(config) + session = Session.from_config(config) session.exitstatus = ExitCode.OK initstate = 0 try: @@ -372,6 +373,7 @@ class Session(nodes.FSCollector): Interrupted = Interrupted Failed = Failed _setupstate = None # type: SetupState + _fixturemanager = None # type: FixtureManager def __init__(self, config): nodes.FSCollector.__init__( @@ -395,6 +397,10 @@ class Session(nodes.FSCollector): self.config.pluginmanager.register(self, name="session") + @classmethod + def from_config(cls, config): + return cls._create(config) + def __repr__(self): return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % ( self.__class__.__name__, diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 18ebc506a..020260dd5 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -2,6 +2,8 @@ import inspect import warnings from collections import namedtuple from collections.abc import MutableMapping +from typing import List +from typing import Optional from typing import Set import attr @@ -144,7 +146,15 @@ class Mark: #: keyword arguments of the mark decorator kwargs = attr.ib() # Dict[str, object] - def combined_with(self, other): + #: source Mark for ids with parametrize Marks + _param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False) + #: resolved/generated ids with parametrize Marks + _param_ids_generated = attr.ib(type=Optional[List[str]], default=None, repr=False) + + def _has_param_ids(self): + return "ids" in self.kwargs or len(self.args) >= 4 + + def combined_with(self, other: "Mark") -> "Mark": """ :param other: the mark to combine with :type other: Mark @@ -153,8 +163,20 @@ class Mark: combines by appending args and merging the mappings """ assert self.name == other.name + + # Remember source of ids with parametrize Marks. + param_ids_from = None # type: Optional[Mark] + if self.name == "parametrize": + if other._has_param_ids(): + param_ids_from = other + elif self._has_param_ids(): + param_ids_from = self + return Mark( - self.name, self.args + other.args, dict(self.kwargs, **other.kwargs) + self.name, + self.args + other.args, + dict(self.kwargs, **other.kwargs), + param_ids_from=param_ids_from, ) @@ -314,13 +336,19 @@ class MarkGenerator: "{!r} not found in `markers` configuration option".format(name), pytrace=False, ) - else: - warnings.warn( - "Unknown pytest.mark.%s - is this a typo? You can register " - "custom marks to avoid this warning - for details, see " - "https://docs.pytest.org/en/latest/mark.html" % name, - PytestUnknownMarkWarning, - ) + + # Raise a specific error for common misspellings of "parametrize". + if name in ["parameterize", "parametrise", "parameterise"]: + __tracebackhide__ = True + fail("Unknown '{}' mark, did you mean 'parametrize'?".format(name)) + + warnings.warn( + "Unknown pytest.mark.%s - is this a typo? You can register " + "custom marks to avoid this warning - for details, see " + "https://docs.pytest.org/en/latest/mark.html" % name, + PytestUnknownMarkWarning, + 2, + ) return MarkDecorator(Mark(name, (), {})) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index eae783f16..3eaafa91d 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -15,7 +15,10 @@ import _pytest._code from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprExceptionInfo +from _pytest.compat import cached_property from _pytest.compat import getfslineno +from _pytest.config import Config +from _pytest.deprecated import NODE_USE_FROM_PARENT from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureLookupErrorRepr @@ -71,18 +74,27 @@ def ischildnode(baseid, nodeid): return node_parts[: len(base_parts)] == base_parts -class Node: +class NodeMeta(type): + def __call__(self, *k, **kw): + warnings.warn(NODE_USE_FROM_PARENT.format(name=self.__name__), stacklevel=2) + return super().__call__(*k, **kw) + + def _create(self, *k, **kw): + return super().__call__(*k, **kw) + + +class Node(metaclass=NodeMeta): """ base class for Collector and Item the test collection tree. Collector subclasses have children, Items are terminal nodes.""" def __init__( self, name, - parent=None, - config=None, + parent: Optional["Node"] = None, + config: Optional[Config] = None, session: Optional["Session"] = None, - fspath=None, - nodeid=None, + fspath: Optional[py.path.local] = None, + nodeid: Optional[str] = None, ) -> None: #: a unique name within the scope of the parent node self.name = name @@ -91,14 +103,20 @@ class Node: self.parent = parent #: the pytest config object - self.config = config or parent.config + if config: + self.config = config + else: + if not parent: + raise TypeError("config or parent must be provided") + self.config = parent.config #: the session this node is part of - if session is None: - assert parent.session is not None - self.session = parent.session - else: + if session: self.session = session + else: + if not parent: + raise TypeError("session or parent must be provided") + self.session = parent.session #: filesystem path where this node was collected from (can be None) self.fspath = fspath or getattr(parent, "fspath", None) @@ -119,10 +137,16 @@ class Node: assert "::()" not in nodeid self._nodeid = nodeid else: + if not self.parent: + raise TypeError("nodeid or parent must be provided") self._nodeid = self.parent.nodeid if self.name != "()": self._nodeid += "::" + self.name + @classmethod + def from_parent(cls, parent, *, name): + return cls._create(parent=parent, name=name) + @property def ihook(self): """ fspath sensitive hook proxy used to call pytest hooks""" @@ -182,7 +206,7 @@ class Node: """ return list of all parent collectors up to self, starting from root of collection tree. """ chain = [] - item = self + item = self # type: Optional[Node] while item is not None: chain.append(item) item = item.parent @@ -263,7 +287,7 @@ class Node: def getparent(self, cls): """ get the next parent node (including ourself) which is an instance of the given class""" - current = self + current = self # type: Optional[Node] while current and not isinstance(current, cls): current = current.parent return current @@ -355,12 +379,14 @@ class Collector(Node): def repr_failure(self, excinfo): """ represent a collection failure. """ - if excinfo.errisinstance(self.CollectError): + if excinfo.errisinstance(self.CollectError) and not self.config.getoption( + "fulltrace", False + ): exc = excinfo.value return str(exc.args[0]) # Respect explicit tbstyle option, but default to "short" - # (None._repr_failure_py defaults to "long" without "fulltrace" option). + # (_repr_failure_py uses "long" with "fulltrace" option always). tbstyle = self.config.getoption("tbstyle", "auto") if tbstyle == "auto": tbstyle = "short" @@ -406,6 +432,10 @@ class FSCollector(Collector): super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath) + @classmethod + def from_parent(cls, parent, *, fspath): + return cls._create(parent=parent, fspath=fspath) + class File(FSCollector): """ base class for collecting tests from a file. """ @@ -448,17 +478,9 @@ class Item(Node): def reportinfo(self) -> Tuple[str, Optional[int], str]: return self.fspath, None, "" - @property + @cached_property def location(self) -> Tuple[str, Optional[int], str]: - try: - return self._location - except AttributeError: - location = self.reportinfo() - fspath = self.session._node_location_to_relpath(location[0]) - assert type(location[2]) is str - self._location = ( - fspath, - location[1], - location[2], - ) # type: Tuple[str, Optional[int], str] - return self._location + location = self.reportinfo() + fspath = self.session._node_location_to_relpath(location[0]) + assert type(location[2]) is str + return (fspath, location[1], location[2]) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 6b45e077b..d5744167c 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -312,7 +312,7 @@ class HookRecorder: return self.getfailures("pytest_collectreport") def listoutcomes( - self + self, ) -> Tuple[List[TestReport], List[TestReport], List[TestReport]]: passed = [] skipped = [] @@ -332,10 +332,17 @@ class HookRecorder: return [len(x) for x in self.listoutcomes()] def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: - realpassed, realskipped, realfailed = self.listoutcomes() - assert passed == len(realpassed) - assert skipped == len(realskipped) - assert failed == len(realfailed) + __tracebackhide__ = True + + outcomes = self.listoutcomes() + realpassed, realskipped, realfailed = outcomes + obtained = { + "passed": len(realpassed), + "skipped": len(realskipped), + "failed": len(realfailed), + } + expected = {"passed": passed, "skipped": skipped, "failed": failed} + assert obtained == expected, outcomes def clear(self) -> None: self.calls[:] = [] @@ -441,8 +448,9 @@ class RunResult: ) -> None: """Assert that the specified outcomes appear with the respective numbers (0 means it didn't occur) in the text output from a test run. - """ + __tracebackhide__ = True + d = self.parseoutcomes() obtained = { "passed": d.get("passed", 0), @@ -536,10 +544,12 @@ class Testdir: mp.delenv("TOX_ENV_DIR", raising=False) # Discard outer pytest options. mp.delenv("PYTEST_ADDOPTS", raising=False) - - # Environment (updates) for inner runs. + # Ensure no user config is used. tmphome = str(self.tmpdir) - self._env_run_update = {"HOME": tmphome, "USERPROFILE": tmphome} + mp.setenv("HOME", tmphome) + mp.setenv("USERPROFILE", tmphome) + # Do not use colors for inner runs by default. + mp.setenv("PY_COLORS", "0") def __repr__(self): return "".format(self.tmpdir) @@ -735,7 +745,7 @@ class Testdir: :param arg: a :py:class:`py.path.local` instance of the file """ - session = Session(config) + session = Session.from_config(config) assert "::" not in str(arg) p = py.path.local(arg) config.hook.pytest_sessionstart(session=session) @@ -753,7 +763,7 @@ class Testdir: """ config = self.parseconfigure(path) - session = Session(config) + session = Session.from_config(config) x = session.fspath.bestrelpath(path) config.hook.pytest_sessionstart(session=session) res = session.perform_collect([x], genitems=False)[0] @@ -845,12 +855,6 @@ class Testdir: plugins = list(plugins) finalizers = [] try: - # Do not load user config (during runs only). - mp_run = MonkeyPatch() - for k, v in self._env_run_update.items(): - mp_run.setenv(k, v) - finalizers.append(mp_run.undo) - # Any sys.module or sys.path changes done while running pytest # inline should be reverted after the test run completes to avoid # clashing with later inline tests run within the same pytest test, @@ -1083,7 +1087,6 @@ class Testdir: env["PYTHONPATH"] = os.pathsep.join( filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) ) - env.update(self._env_run_update) kw["env"] = env if stdin is Testdir.CLOSE_STDIN: @@ -1253,11 +1256,7 @@ class Testdir: pytest.skip("pexpect.spawn not available") logfile = self.tmpdir.join("spawn.out").open("wb") - # Do not load user config. - env = os.environ.copy() - env.update(self._env_run_update) - - child = pexpect.spawn(cmd, logfile=logfile, env=env) + child = pexpect.spawn(cmd, logfile=logfile) self.request.addfinalizer(logfile.close) child.timeout = expect_timeout return child @@ -1430,8 +1429,10 @@ class LineMatcher: self._log("{:>{width}}".format("and:", width=wnick), repr(nextline)) extralines.append(nextline) else: - self._log("remains unmatched: {!r}".format(line)) - pytest.fail(self._log_text.lstrip()) + msg = "remains unmatched: {!r}".format(line) + self._log(msg) + self._fail(msg) + self._log_output = [] def no_fnmatch_line(self, pat): """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``. @@ -1457,18 +1458,21 @@ class LineMatcher: __tracebackhide__ = True nomatch_printed = False wnick = len(match_nickname) + 1 - try: - for line in self.lines: - if match_func(line, pat): - self._log("%s:" % match_nickname, repr(pat)) - self._log("{:>{width}}".format("with:", width=wnick), repr(line)) - pytest.fail(self._log_text.lstrip()) - else: - if not nomatch_printed: - self._log( - "{:>{width}}".format("nomatch:", width=wnick), repr(pat) - ) - nomatch_printed = True - self._log("{:>{width}}".format("and:", width=wnick), repr(line)) - finally: - self._log_output = [] + for line in self.lines: + if match_func(line, pat): + msg = "{}: {!r}".format(match_nickname, pat) + self._log(msg) + self._log("{:>{width}}".format("with:", width=wnick), repr(line)) + self._fail(msg) + else: + if not nomatch_printed: + self._log("{:>{width}}".format("nomatch:", width=wnick), repr(pat)) + nomatch_printed = True + self._log("{:>{width}}".format("and:", width=wnick), repr(line)) + self._log_output = [] + + def _fail(self, msg): + __tracebackhide__ = True + log_text = self._log_text + self._log_output = [] + pytest.fail(log_text) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c1654b1c9..1b01f4faa 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -9,6 +9,8 @@ from collections import Counter from collections.abc import Sequence from functools import partial from textwrap import dedent +from typing import List +from typing import Optional from typing import Tuple import py @@ -31,9 +33,11 @@ from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass from _pytest.compat import STRING_TYPES from _pytest.config import hookimpl +from _pytest.deprecated import FUNCARGNAMES from _pytest.main import FSHookProxy from _pytest.mark import MARK_GEN from _pytest.mark.structures import get_unpacked_marks +from _pytest.mark.structures import Mark from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail from _pytest.outcomes import skip @@ -119,15 +123,8 @@ def pytest_cmdline_main(config): def pytest_generate_tests(metafunc): - # those alternative spellings are common - raise a specific error to alert - # the user - alt_spellings = ["parameterize", "parametrise", "parameterise"] - for mark_name in alt_spellings: - if metafunc.definition.get_closest_marker(mark_name): - msg = "{0} has '{1}' mark, spelling should be 'parametrize'" - fail(msg.format(metafunc.function.__name__, mark_name), pytrace=False) for marker in metafunc.definition.iter_markers(name="parametrize"): - metafunc.parametrize(*marker.args, **marker.kwargs) + metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) def pytest_configure(config): @@ -193,8 +190,8 @@ def path_matches_patterns(path, patterns): def pytest_pycollect_makemodule(path, parent): if path.basename == "__init__.py": - return Package(path, parent) - return Module(path, parent) + return Package.from_parent(parent, fspath=path) + return Module.from_parent(parent, fspath=path) @hookimpl(hookwrapper=True) @@ -206,7 +203,7 @@ def pytest_pycollect_makeitem(collector, name, obj): # nothing was collected elsewhere, let's do it here if safe_isclass(obj): if collector.istestclass(obj, name): - outcome.force_result(Class(name, parent=collector)) + outcome.force_result(Class.from_parent(collector, name=name, obj=obj)) elif collector.istestfunction(obj, name): # mock seems to store unbound methods (issue473), normalize it obj = getattr(obj, "__func__", obj) @@ -225,7 +222,7 @@ def pytest_pycollect_makeitem(collector, name, obj): ) elif getattr(obj, "__test__", True): if is_generator(obj): - res = Function(name, parent=collector) + res = Function.from_parent(collector, name=name) reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format( name=name ) @@ -236,10 +233,6 @@ def pytest_pycollect_makeitem(collector, name, obj): outcome.force_result(res) -def pytest_make_parametrize_id(config, val, argname=None): - return None - - class PyobjContext: module = pyobj_property("Module") cls = pyobj_property("Class") @@ -286,8 +279,7 @@ class PyobjMixin(PyobjContext): break parts.append(name) parts.reverse() - s = ".".join(parts) - return s.replace(".[", "[") + return ".".join(parts) def reportinfo(self) -> Tuple[str, int, str]: # XXX caching? @@ -389,7 +381,7 @@ class PyCollector(PyobjMixin, nodes.Collector): cls = clscol and clscol.obj or None fm = self.session._fixturemanager - definition = FunctionDefinition(name=name, parent=self, callobj=funcobj) + definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj) fixtureinfo = fm.getfixtureinfo(definition, funcobj, cls) metafunc = Metafunc( @@ -404,7 +396,7 @@ class PyCollector(PyobjMixin, nodes.Collector): self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) if not metafunc._calls: - yield Function(name, parent=self, fixtureinfo=fixtureinfo) + yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) else: # add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm) @@ -416,9 +408,9 @@ class PyCollector(PyobjMixin, nodes.Collector): for callspec in metafunc._calls: subname = "{}[{}]".format(name, callspec.id) - yield Function( + yield Function.from_parent( + self, name=subname, - parent=self, callspec=callspec, callobj=funcobj, fixtureinfo=fixtureinfo, @@ -634,7 +626,7 @@ class Package(Module): if init_module.check(file=1) and path_matches_patterns( init_module, self.config.getini("python_files") ): - yield Module(init_module, self) + yield Module.from_parent(self, fspath=init_module) pkg_prefixes = set() for path in this_path.visit(rec=self._recurse, bf=True, sort=True): # We will visit our own __init__.py file, in which case we skip it. @@ -685,6 +677,10 @@ def _get_first_non_fixture_func(obj, names): class Class(PyCollector): """ Collector for test methods. """ + @classmethod + def from_parent(cls, parent, *, name, obj=None): + return cls._create(name=name, parent=parent) + def collect(self): if not safe_getattr(self.obj, "__test__", True): return [] @@ -710,7 +706,7 @@ class Class(PyCollector): self._inject_setup_class_fixture() self._inject_setup_method_fixture() - return [Instance(name="()", parent=self)] + return [Instance.from_parent(self, name="()")] def _inject_setup_class_fixture(self): """Injects a hidden autouse, class scoped fixture into the collected class object @@ -882,7 +878,7 @@ class CallSpec2: self.marks.extend(normalize_mark_list(marks)) -class Metafunc(fixtures.FuncargnamesCompatAttr): +class Metafunc: """ Metafunc objects are passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook. They help to inspect a test function and to generate tests according to @@ -890,11 +886,14 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): test function is defined. """ - def __init__(self, definition, fixtureinfo, config, cls=None, module=None): - assert ( - isinstance(definition, FunctionDefinition) - or type(definition).__name__ == "DefinitionMock" - ) + def __init__( + self, + definition: "FunctionDefinition", + fixtureinfo, + config, + cls=None, + module=None, + ) -> None: self.definition = definition #: access to the :class:`_pytest.config.Config` object for the test session @@ -912,11 +911,25 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): #: class object where the test function is defined in or ``None``. self.cls = cls - self._calls = [] - self._ids = set() + self._calls = [] # type: List[CallSpec2] self._arg2fixturedefs = fixtureinfo.name2fixturedefs - def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None): + @property + def funcargnames(self): + """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + warnings.warn(FUNCARGNAMES, stacklevel=2) + return self.fixturenames + + def parametrize( + self, + argnames, + argvalues, + indirect=False, + ids=None, + scope=None, + *, + _param_mark: Optional[Mark] = None + ): """ Add new invocations to the underlying test function using the list of argvalues for the given argnames. Parametrization is performed during the collection phase. If you need to setup expensive resources @@ -939,13 +952,22 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): function so that it can perform more expensive setups during the setup phase of a test rather than at collection time. - :arg ids: list of string ids, or a callable. - If strings, each is corresponding to the argvalues so that they are - part of the test id. If None is given as id of specific test, the - automatically generated id for that argument will be used. - If callable, it should take one argument (a single argvalue) and return - a string or return None. If None, the automatically generated id for that - argument will be used. + :arg ids: sequence of (or generator for) ids for ``argvalues``, + or a callable to return part of the id for each argvalue. + + With sequences (and generators like ``itertools.count()``) the + returned ids should be of type ``string``, ``int``, ``float``, + ``bool``, or ``None``. + They are mapped to the corresponding index in ``argvalues``. + ``None`` means to use the auto-generated id. + + If it is a callable it will be called for each entry in + ``argvalues``, and the return value is used as part of the + auto-generated id for the whole set (where parts are joined with + dashes ("-")). + This is useful to provide more specific ids for certain items, e.g. + dates. Returning ``None`` will use an auto-generated id. + If no ids are provided they will be generated automatically from the argvalues. @@ -966,6 +988,12 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): ) del argvalues + if "request" in argnames: + fail( + "'request' is a reserved name and cannot be used in @pytest.mark.parametrize", + pytrace=False, + ) + if scope is None: scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) @@ -973,8 +1001,18 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): arg_values_types = self._resolve_arg_value_types(argnames, indirect) + # Use any already (possibly) generated ids with parametrize Marks. + if _param_mark and _param_mark._param_ids_from: + generated_ids = _param_mark._param_ids_from._param_ids_generated + if generated_ids is not None: + ids = generated_ids + ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition) + # Store used (possibly generated) ids with parametrize Marks. + if _param_mark and _param_mark._param_ids_from and generated_ids is None: + object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids) + scopenum = scope2index( scope, descr="parametrize() call in {}".format(self.function.__name__) ) @@ -1009,27 +1047,48 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): :rtype: List[str] :return: the list of ids for each argname given """ - from _pytest._io.saferepr import saferepr - idfn = None if callable(ids): idfn = ids ids = None if ids: func_name = self.function.__name__ - if len(ids) != len(parameters): - msg = "In {}: {} parameter sets specified, with different number of ids: {}" - fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False) - for id_value in ids: - if id_value is not None and not isinstance(id_value, str): - msg = "In {}: ids must be list of strings, found: {} (type: {!r})" - fail( - msg.format(func_name, saferepr(id_value), type(id_value)), - pytrace=False, - ) + ids = self._validate_ids(ids, parameters, func_name) ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item) return ids + def _validate_ids(self, ids, parameters, func_name): + try: + len(ids) + except TypeError: + try: + it = iter(ids) + except TypeError: + raise TypeError("ids must be a callable, sequence or generator") + else: + import itertools + + new_ids = list(itertools.islice(it, len(parameters))) + else: + new_ids = list(ids) + + if len(new_ids) != len(parameters): + msg = "In {}: {} parameter sets specified, with different number of ids: {}" + fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False) + for idx, id_value in enumerate(new_ids): + if id_value is not None: + if isinstance(id_value, (float, int, bool)): + new_ids[idx] = str(id_value) + elif not isinstance(id_value, str): + from _pytest._io.saferepr import saferepr + + msg = "In {}: ids must be list of string/float/int/bool, found: {} (type: {!r}) at index {}" + fail( + msg.format(func_name, saferepr(id_value), type(id_value), idx), + pytrace=False, + ) + return new_ids + def _resolve_arg_value_types(self, argnames, indirect): """Resolves if each parametrized argument must be considered a parameter to a fixture or a "funcarg" to the function, based on the ``indirect`` parameter of the parametrized() call. @@ -1143,8 +1202,7 @@ def _idval(val, argname, idx, idfn, item, config): if generated_id is not None: val = generated_id except Exception as e: - # See issue https://github.com/pytest-dev/pytest/issues/2169 - msg = "{}: error raised while trying to determine id of parameter '{}' at position {}\n" + msg = "{}: error raised while trying to determine id of parameter '{}' at position {}" msg = msg.format(item.nodeid, argname, idx) raise ValueError(msg) from e elif config: @@ -1333,7 +1391,7 @@ def write_docstring(tw, doc, indent=" "): tw.write(indent + line + "\n") -class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): +class Function(FunctionMixin, nodes.Item): """ a Function Item is responsible for setting up and executing a Python test function. """ @@ -1399,6 +1457,10 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): #: .. versionadded:: 3.0 self.originalname = originalname + @classmethod + def from_parent(cls, parent, **kw): + return cls._create(parent=parent, **kw) + def _initrequest(self): self.funcargs = {} self._request = fixtures.FixtureRequest(self) @@ -1420,6 +1482,12 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): "(compatonly) for code expecting pytest-2.2 style request objects" return self + @property + def funcargnames(self): + """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + warnings.warn(FUNCARGNAMES, stacklevel=2) + return self.fixturenames + def runtest(self): """ execute the underlying test function. """ self.ihook.pytest_pyfunc_call(pyfuncitem=self) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 52a91a905..9f206ce9b 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -552,7 +552,7 @@ def raises( @overload # noqa: F811 -def raises( +def raises( # noqa: F811 expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], func: Callable, *args: Any, diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 4967106d9..e3d7b72ec 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -60,18 +60,18 @@ def warns( *, match: "Optional[Union[str, Pattern]]" = ... ) -> "WarningsChecker": - ... # pragma: no cover + raise NotImplementedError() @overload # noqa: F811 -def warns( +def warns( # noqa: F811 expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], func: Callable, *args: Any, match: Optional[Union[str, "Pattern"]] = ..., **kwargs: Any ) -> Union[Any]: - ... # pragma: no cover + raise NotImplementedError() def warns( # noqa: F811 @@ -129,7 +129,9 @@ def warns( # noqa: F811 return func(*args[1:], **kwargs) -class WarningsRecorder(warnings.catch_warnings): +# Type ignored until next mypy release. Regression fixed by: +# https://github.com/python/typeshed/commit/41bf6a19822d6694973449d795f8bfe1d50d5a03 +class WarningsRecorder(warnings.catch_warnings): # type: ignore """A context manager to record raised warnings. Adapted from `warnings.catch_warnings`. diff --git a/src/_pytest/report_log.py b/src/_pytest/report_log.py deleted file mode 100644 index b12d0a55d..000000000 --- a/src/_pytest/report_log.py +++ /dev/null @@ -1,72 +0,0 @@ -import json -from pathlib import Path - -import pytest - - -def pytest_addoption(parser): - group = parser.getgroup("terminal reporting", "report-log plugin options") - group.addoption( - "--report-log", - action="store", - metavar="path", - default=None, - help="Path to line-based json objects of test session events.", - ) - - -def pytest_configure(config): - report_log = config.option.report_log - if report_log and not hasattr(config, "slaveinput"): - config._report_log_plugin = ReportLogPlugin(config, Path(report_log)) - config.pluginmanager.register(config._report_log_plugin) - - -def pytest_unconfigure(config): - report_log_plugin = getattr(config, "_report_log_plugin", None) - if report_log_plugin: - report_log_plugin.close() - del config._report_log_plugin - - -class ReportLogPlugin: - def __init__(self, config, log_path: Path): - self._config = config - self._log_path = log_path - - log_path.parent.mkdir(parents=True, exist_ok=True) - self._file = log_path.open("w", buffering=1, encoding="UTF-8") - - def close(self): - if self._file is not None: - self._file.close() - self._file = None - - def _write_json_data(self, data): - self._file.write(json.dumps(data) + "\n") - self._file.flush() - - def pytest_sessionstart(self): - data = {"pytest_version": pytest.__version__, "$report_type": "SessionStart"} - self._write_json_data(data) - - def pytest_sessionfinish(self, exitstatus): - data = {"exitstatus": exitstatus, "$report_type": "SessionFinish"} - self._write_json_data(data) - - def pytest_runtest_logreport(self, report): - data = self._config.hook.pytest_report_to_serializable( - config=self._config, report=report - ) - self._write_json_data(data) - - def pytest_collectreport(self, report): - data = self._config.hook.pytest_report_to_serializable( - config=self._config, report=report - ) - self._write_json_data(data) - - def pytest_terminal_summary(self, terminalreporter): - terminalreporter.write_sep( - "-", "generated report log file: {}".format(self._log_path) - ) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index c383146c3..67e28e905 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -121,7 +121,12 @@ def pytest_runtest_setup(item): def pytest_runtest_call(item): _update_current_test_var(item, "call") - sys.last_type, sys.last_value, sys.last_traceback = (None, None, None) + try: + del sys.last_type + del sys.last_value + del sys.last_traceback + except AttributeError: + pass try: item.runtest() except Exception: diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 697746f20..6fdd3aed0 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -16,7 +16,8 @@ def pytest_addoption(parser): def pytest_fixture_setup(fixturedef, request): # Will return a dummy fixture if the setuponly option is provided. if request.config.option.setupplan: - fixturedef.cached_result = (None, None, None) + my_cache_key = fixturedef.cache_key(request) + fixturedef.cached_result = (None, my_cache_key, None) return fixturedef.cached_result diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 59f0fe0f3..40e12e406 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -254,7 +254,7 @@ class TerminalReporter: # self.writer will be deprecated in pytest-3.4 self.writer = self._tw self._screen_width = self._tw.fullwidth - self.currentfspath = None # type: Optional[int] + self.currentfspath = None # type: Any self.reportchars = getreportopt(config) self.hasmarkup = self._tw.hasmarkup self.isatty = file.isatty() @@ -676,7 +676,7 @@ class TerminalReporter: self._tw.line("{}{}".format(indent + " ", line.strip())) @pytest.hookimpl(hookwrapper=True) - def pytest_sessionfinish(self, exitstatus): + def pytest_sessionfinish(self, session: Session, exitstatus: ExitCode): outcome = yield outcome.get_result() self._tw.line("") @@ -691,9 +691,13 @@ class TerminalReporter: self.config.hook.pytest_terminal_summary( terminalreporter=self, exitstatus=exitstatus, config=self.config ) + if session.shouldfail: + self.write_sep("!", session.shouldfail, red=True) if exitstatus == ExitCode.INTERRUPTED: self._report_keyboardinterrupt() del self._keyboardinterrupt_memo + elif session.shouldstop: + self.write_sep("!", session.shouldstop, red=True) self.summary_stats() @pytest.hookimpl(hookwrapper=True) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 71ff580a6..a5512e944 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -24,7 +24,7 @@ def pytest_pycollect_makeitem(collector, name, obj): except Exception: return # yes, so let's collect it - return UnitTestCase(name, parent=collector) + return UnitTestCase.from_parent(collector, name=name, obj=obj) class UnitTestCase(Class): @@ -52,7 +52,7 @@ class UnitTestCase(Class): if not getattr(x, "__test__", True): continue funcobj = getimfunc(x) - yield TestCaseFunction(name, parent=self, callobj=funcobj) + yield TestCaseFunction.from_parent(self, name=name, callobj=funcobj) foundsomething = True if not foundsomething: @@ -60,7 +60,8 @@ class UnitTestCase(Class): if runtest is not None: ut = sys.modules.get("twisted.trial.unittest", None) if ut is None or runtest != ut.TestCase.runTest: - yield TestCaseFunction("runTest", parent=self) + # TODO: callobj consistency + yield TestCaseFunction.from_parent(self, name="runTest") def _inject_setup_teardown_fixtures(self, cls): """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding @@ -201,6 +202,7 @@ class TestCaseFunction(Function): return bool(expecting_failure_class or expecting_failure_method) def runtest(self): + # TODO: move testcase reporter into separate class, this shouldnt be on item import unittest testMethod = getattr(self._testcase, self._testcase._testMethodName) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 8fdb61c2b..b6ee049ec 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -138,7 +138,7 @@ def _issue_warning_captured(warning, hook, stacklevel): """ This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured - hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891. + hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891. :param warning: the warning instance. :param hook: the hook caller @@ -149,6 +149,10 @@ def _issue_warning_captured(warning, hook, stacklevel): warnings.warn(warning, stacklevel=stacklevel) # Mypy can't infer that record=True means records is not None; help it. assert records is not None + frame = sys._getframe(stacklevel - 1) + location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name hook.pytest_warning_captured.call_historic( - kwargs=dict(warning_message=records[0], when="config", item=None) + kwargs=dict( + warning_message=records[0], when="config", item=None, location=location + ) ) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 578ab45eb..8f7be14be 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -760,7 +760,6 @@ class TestInvocationVariants: result = testdir.runpytest(str(p) + "::test", "--doctest-modules") result.stdout.fnmatch_lines(["*1 passed*"]) - @pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks") def test_cmdline_python_package_symlink(self, testdir, monkeypatch): """ test --pyargs option with packages with path containing symlink can diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 2f55720b4..f8e1ce17f 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -1,18 +1,19 @@ import sys +from types import FrameType from unittest import mock import _pytest._code import pytest -def test_ne(): +def test_ne() -> None: code1 = _pytest._code.Code(compile('foo = "bar"', "", "exec")) assert code1 == code1 code2 = _pytest._code.Code(compile('foo = "baz"', "", "exec")) assert code2 != code1 -def test_code_gives_back_name_for_not_existing_file(): +def test_code_gives_back_name_for_not_existing_file() -> None: name = "abc-123" co_code = compile("pass\n", name, "exec") assert co_code.co_filename == name @@ -21,68 +22,67 @@ def test_code_gives_back_name_for_not_existing_file(): assert code.fullsource is None -def test_code_with_class(): +def test_code_with_class() -> None: class A: pass pytest.raises(TypeError, _pytest._code.Code, A) -def x(): +def x() -> None: raise NotImplementedError() -def test_code_fullsource(): +def test_code_fullsource() -> None: code = _pytest._code.Code(x) full = code.fullsource assert "test_code_fullsource()" in str(full) -def test_code_source(): +def test_code_source() -> None: code = _pytest._code.Code(x) src = code.source() - expected = """def x(): + expected = """def x() -> None: raise NotImplementedError()""" assert str(src) == expected -def test_frame_getsourcelineno_myself(): - def func(): +def test_frame_getsourcelineno_myself() -> None: + def func() -> FrameType: return sys._getframe(0) - f = func() - f = _pytest._code.Frame(f) + f = _pytest._code.Frame(func()) source, lineno = f.code.fullsource, f.lineno + assert source is not None assert source[lineno].startswith(" return sys._getframe(0)") -def test_getstatement_empty_fullsource(): - def func(): +def test_getstatement_empty_fullsource() -> None: + def func() -> FrameType: return sys._getframe(0) - f = func() - f = _pytest._code.Frame(f) + f = _pytest._code.Frame(func()) with mock.patch.object(f.code.__class__, "fullsource", None): assert f.statement == "" -def test_code_from_func(): +def test_code_from_func() -> None: co = _pytest._code.Code(test_frame_getsourcelineno_myself) assert co.firstlineno assert co.path -def test_unicode_handling(): +def test_unicode_handling() -> None: value = "ąć".encode() - def f(): + def f() -> None: raise Exception(value) excinfo = pytest.raises(Exception, f) str(excinfo) -def test_code_getargs(): +def test_code_getargs() -> None: def f1(x): raise NotImplementedError() @@ -108,26 +108,26 @@ def test_code_getargs(): assert c4.getargs(var=True) == ("x", "y", "z") -def test_frame_getargs(): - def f1(x): +def test_frame_getargs() -> None: + def f1(x) -> FrameType: return sys._getframe(0) fr1 = _pytest._code.Frame(f1("a")) assert fr1.getargs(var=True) == [("x", "a")] - def f2(x, *y): + def f2(x, *y) -> FrameType: return sys._getframe(0) fr2 = _pytest._code.Frame(f2("a", "b", "c")) assert fr2.getargs(var=True) == [("x", "a"), ("y", ("b", "c"))] - def f3(x, **z): + def f3(x, **z) -> FrameType: return sys._getframe(0) fr3 = _pytest._code.Frame(f3("a", b="c")) assert fr3.getargs(var=True) == [("x", "a"), ("z", {"b": "c"})] - def f4(x, *y, **z): + def f4(x, *y, **z) -> FrameType: return sys._getframe(0) fr4 = _pytest._code.Frame(f4("a", "b", c="d")) @@ -135,7 +135,7 @@ def test_frame_getargs(): class TestExceptionInfo: - def test_bad_getsource(self): + def test_bad_getsource(self) -> None: try: if False: pass @@ -145,13 +145,13 @@ class TestExceptionInfo: exci = _pytest._code.ExceptionInfo.from_current() assert exci.getrepr() - def test_from_current_with_missing(self): + def test_from_current_with_missing(self) -> None: with pytest.raises(AssertionError, match="no current exception"): _pytest._code.ExceptionInfo.from_current() class TestTracebackEntry: - def test_getsource(self): + def test_getsource(self) -> None: try: if False: pass @@ -161,12 +161,13 @@ class TestTracebackEntry: exci = _pytest._code.ExceptionInfo.from_current() entry = exci.traceback[0] source = entry.getsource() + assert source is not None assert len(source) == 6 assert "assert False" in source[5] class TestReprFuncArgs: - def test_not_raise_exception_with_mixed_encoding(self, tw_mock): + def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None: from _pytest._code.code import ReprFuncArgs args = [("unicode_string", "São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")] diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 199b8716f..997b14e2f 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -3,6 +3,7 @@ import os import queue import sys import textwrap +from typing import Union import py @@ -59,9 +60,9 @@ def test_excinfo_getstatement(): except ValueError: excinfo = _pytest._code.ExceptionInfo.from_current() linenumbers = [ - _pytest._code.getrawcode(f).co_firstlineno - 1 + 4, - _pytest._code.getrawcode(f).co_firstlineno - 1 + 1, - _pytest._code.getrawcode(g).co_firstlineno - 1 + 1, + f.__code__.co_firstlineno - 1 + 4, + f.__code__.co_firstlineno - 1 + 1, + g.__code__.co_firstlineno - 1 + 1, ] values = list(excinfo.traceback) foundlinenumbers = [x.lineno for x in values] @@ -224,23 +225,25 @@ class TestTraceback_f_g_h: repr = excinfo.getrepr() assert "RuntimeError: hello" in str(repr.reprcrash) - def test_traceback_no_recursion_index(self): - def do_stuff(): + def test_traceback_no_recursion_index(self) -> None: + def do_stuff() -> None: raise RuntimeError - def reraise_me(): + def reraise_me() -> None: import sys exc, val, tb = sys.exc_info() + assert val is not None raise val.with_traceback(tb) - def f(n): + def f(n: int) -> None: try: do_stuff() except: # noqa reraise_me() excinfo = pytest.raises(RuntimeError, f, 8) + assert excinfo is not None traceback = excinfo.traceback recindex = traceback.recursionindex() assert recindex is None @@ -502,65 +505,18 @@ raise ValueError() assert repr.reprtraceback.reprentries[1].lines[0] == "> ???" assert repr.chain[0][0].reprentries[1].lines[0] == "> ???" - def test_repr_source_failing_fullsource(self): + def test_repr_source_failing_fullsource(self, monkeypatch) -> None: pr = FormattedExcinfo() - class FakeCode: - class raw: - co_filename = "?" + try: + 1 / 0 + except ZeroDivisionError: + excinfo = ExceptionInfo.from_current() - path = "?" - firstlineno = 5 + with monkeypatch.context() as m: + m.setattr(_pytest._code.Code, "fullsource", property(lambda self: None)) + repr = pr.repr_excinfo(excinfo) - def fullsource(self): - return None - - fullsource = property(fullsource) - - class FakeFrame: - code = FakeCode() - f_locals = {} - f_globals = {} - - class FakeTracebackEntry(_pytest._code.Traceback.Entry): - def __init__(self, tb, excinfo=None): - self.lineno = 5 + 3 - - @property - def frame(self): - return FakeFrame() - - class Traceback(_pytest._code.Traceback): - Entry = FakeTracebackEntry - - class FakeExcinfo(_pytest._code.ExceptionInfo): - typename = "Foo" - value = Exception() - - def __init__(self): - pass - - def exconly(self, tryshort): - return "EXC" - - def errisinstance(self, cls): - return False - - excinfo = FakeExcinfo() - - class FakeRawTB: - tb_next = None - - tb = FakeRawTB() - excinfo.traceback = Traceback(tb) - - fail = IOError() - repr = pr.repr_excinfo(excinfo) - assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" - assert repr.chain[0][0].reprentries[0].lines[0] == "> ???" - - fail = py.error.ENOENT # noqa - repr = pr.repr_excinfo(excinfo) assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" assert repr.chain[0][0].reprentries[0].lines[0] == "> ???" @@ -643,7 +599,6 @@ raise ValueError() assert lines[3] == "E world" assert not lines[4:] - loc = repr_entry.reprlocals is not None loc = repr_entry.reprfileloc assert loc.path == mod.__file__ assert loc.lineno == 3 @@ -1333,9 +1288,10 @@ raise ValueError() @pytest.mark.parametrize("style", ["short", "long"]) @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"]) def test_repr_traceback_with_unicode(style, encoding): - msg = "☹" - if encoding is not None: - msg = msg.encode(encoding) + if encoding is None: + msg = "☹" # type: Union[str, bytes] + else: + msg = "☹".encode(encoding) try: raise RuntimeError(msg) except RuntimeError: diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 5e7e1abf5..bf52dccd7 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -4,13 +4,16 @@ import ast import inspect import sys +from typing import Any +from typing import Dict +from typing import Optional import _pytest._code import pytest from _pytest._code import Source -def test_source_str_function(): +def test_source_str_function() -> None: x = Source("3") assert str(x) == "3" @@ -25,7 +28,7 @@ def test_source_str_function(): assert str(x) == "\n3" -def test_unicode(): +def test_unicode() -> None: x = Source("4") assert str(x) == "4" co = _pytest._code.compile('"å"', mode="eval") @@ -33,12 +36,12 @@ def test_unicode(): assert isinstance(val, str) -def test_source_from_function(): +def test_source_from_function() -> None: source = _pytest._code.Source(test_source_str_function) - assert str(source).startswith("def test_source_str_function():") + assert str(source).startswith("def test_source_str_function() -> None:") -def test_source_from_method(): +def test_source_from_method() -> None: class TestClass: def test_method(self): pass @@ -47,13 +50,13 @@ def test_source_from_method(): assert source.lines == ["def test_method(self):", " pass"] -def test_source_from_lines(): +def test_source_from_lines() -> None: lines = ["a \n", "b\n", "c"] source = _pytest._code.Source(lines) assert source.lines == ["a ", "b", "c"] -def test_source_from_inner_function(): +def test_source_from_inner_function() -> None: def f(): pass @@ -63,7 +66,7 @@ def test_source_from_inner_function(): assert str(source).startswith("def f():") -def test_source_putaround_simple(): +def test_source_putaround_simple() -> None: source = Source("raise ValueError") source = source.putaround( "try:", @@ -85,7 +88,7 @@ else: ) -def test_source_putaround(): +def test_source_putaround() -> None: source = Source() source = source.putaround( """ @@ -96,28 +99,29 @@ def test_source_putaround(): assert str(source).strip() == "if 1:\n x=1" -def test_source_strips(): +def test_source_strips() -> None: source = Source("") assert source == Source() assert str(source) == "" assert source.strip() == source -def test_source_strip_multiline(): +def test_source_strip_multiline() -> None: source = Source() source.lines = ["", " hello", " "] source2 = source.strip() assert source2.lines == [" hello"] -def test_syntaxerror_rerepresentation(): +def test_syntaxerror_rerepresentation() -> None: ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz") + assert ex is not None assert ex.value.lineno == 1 assert ex.value.offset in {5, 7} # cpython: 7, pypy3.6 7.1.1: 5 - assert ex.value.text.strip(), "x x" + assert ex.value.text == "xyz xyz\n" -def test_isparseable(): +def test_isparseable() -> None: assert Source("hello").isparseable() assert Source("if 1:\n pass").isparseable() assert Source(" \nif 1:\n pass").isparseable() @@ -127,7 +131,7 @@ def test_isparseable(): class TestAccesses: - def setup_class(self): + def setup_class(self) -> None: self.source = Source( """\ def f(x): @@ -137,26 +141,26 @@ class TestAccesses: """ ) - def test_getrange(self): + def test_getrange(self) -> None: x = self.source[0:2] assert x.isparseable() assert len(x.lines) == 2 assert str(x) == "def f(x):\n pass" - def test_getline(self): + def test_getline(self) -> None: x = self.source[0] assert x == "def f(x):" - def test_len(self): + def test_len(self) -> None: assert len(self.source) == 4 - def test_iter(self): + def test_iter(self) -> None: values = [x for x in self.source] assert len(values) == 4 class TestSourceParsingAndCompiling: - def setup_class(self): + def setup_class(self) -> None: self.source = Source( """\ def f(x): @@ -166,19 +170,19 @@ class TestSourceParsingAndCompiling: """ ).strip() - def test_compile(self): + def test_compile(self) -> None: co = _pytest._code.compile("x=3") - d = {} + d = {} # type: Dict[str, Any] exec(co, d) assert d["x"] == 3 - def test_compile_and_getsource_simple(self): + def test_compile_and_getsource_simple(self) -> None: co = _pytest._code.compile("x=3") exec(co) source = _pytest._code.Source(co) assert str(source) == "x=3" - def test_compile_and_getsource_through_same_function(self): + def test_compile_and_getsource_through_same_function(self) -> None: def gensource(source): return _pytest._code.compile(source) @@ -199,7 +203,7 @@ class TestSourceParsingAndCompiling: source2 = inspect.getsource(co2) assert "ValueError" in source2 - def test_getstatement(self): + def test_getstatement(self) -> None: # print str(self.source) ass = str(self.source[1:]) for i in range(1, 4): @@ -208,7 +212,7 @@ class TestSourceParsingAndCompiling: # x = s.deindent() assert str(s) == ass - def test_getstatementrange_triple_quoted(self): + def test_getstatementrange_triple_quoted(self) -> None: # print str(self.source) source = Source( """hello(''' @@ -219,7 +223,7 @@ class TestSourceParsingAndCompiling: s = source.getstatement(1) assert s == str(source) - def test_getstatementrange_within_constructs(self): + def test_getstatementrange_within_constructs(self) -> None: source = Source( """\ try: @@ -241,7 +245,7 @@ class TestSourceParsingAndCompiling: # assert source.getstatementrange(5) == (0, 7) assert source.getstatementrange(6) == (6, 7) - def test_getstatementrange_bug(self): + def test_getstatementrange_bug(self) -> None: source = Source( """\ try: @@ -255,7 +259,7 @@ class TestSourceParsingAndCompiling: assert len(source) == 6 assert source.getstatementrange(2) == (1, 4) - def test_getstatementrange_bug2(self): + def test_getstatementrange_bug2(self) -> None: source = Source( """\ assert ( @@ -272,7 +276,7 @@ class TestSourceParsingAndCompiling: assert len(source) == 9 assert source.getstatementrange(5) == (0, 9) - def test_getstatementrange_ast_issue58(self): + def test_getstatementrange_ast_issue58(self) -> None: source = Source( """\ @@ -286,38 +290,44 @@ class TestSourceParsingAndCompiling: assert getstatement(2, source).lines == source.lines[2:3] assert getstatement(3, source).lines == source.lines[3:4] - def test_getstatementrange_out_of_bounds_py3(self): + def test_getstatementrange_out_of_bounds_py3(self) -> None: source = Source("if xxx:\n from .collections import something") r = source.getstatementrange(1) assert r == (1, 2) - def test_getstatementrange_with_syntaxerror_issue7(self): + def test_getstatementrange_with_syntaxerror_issue7(self) -> None: source = Source(":") pytest.raises(SyntaxError, lambda: source.getstatementrange(0)) - def test_compile_to_ast(self): + def test_compile_to_ast(self) -> None: source = Source("x = 4") mod = source.compile(flag=ast.PyCF_ONLY_AST) assert isinstance(mod, ast.Module) compile(mod, "", "exec") - def test_compile_and_getsource(self): + def test_compile_and_getsource(self) -> None: co = self.source.compile() exec(co, globals()) - f(7) - excinfo = pytest.raises(AssertionError, f, 6) + f(7) # type: ignore + excinfo = pytest.raises(AssertionError, f, 6) # type: ignore + assert excinfo is not None frame = excinfo.traceback[-1].frame + assert isinstance(frame.code.fullsource, Source) stmt = frame.code.fullsource.getstatement(frame.lineno) assert str(stmt).strip().startswith("assert") @pytest.mark.parametrize("name", ["", None, "my"]) - def test_compilefuncs_and_path_sanity(self, name): + def test_compilefuncs_and_path_sanity(self, name: Optional[str]) -> None: def check(comp, name): co = comp(self.source, name) if not name: - expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2) + expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2) # type: ignore else: - expected = "codegen %r %s:%d>" % (name, mypath, mylineno + 2 + 2) + expected = "codegen %r %s:%d>" % ( + name, + mypath, # type: ignore + mylineno + 2 + 2, # type: ignore + ) # type: ignore fn = co.co_filename assert fn.endswith(expected) @@ -332,9 +342,9 @@ class TestSourceParsingAndCompiling: pytest.raises(SyntaxError, _pytest._code.compile, "lambda a,a: 0", mode="eval") -def test_getstartingblock_singleline(): +def test_getstartingblock_singleline() -> None: class A: - def __init__(self, *args): + def __init__(self, *args) -> None: frame = sys._getframe(1) self.source = _pytest._code.Frame(frame).statement @@ -344,22 +354,22 @@ def test_getstartingblock_singleline(): assert len(values) == 1 -def test_getline_finally(): - def c(): +def test_getline_finally() -> None: + def c() -> None: pass with pytest.raises(TypeError) as excinfo: teardown = None try: - c(1) + c(1) # type: ignore finally: if teardown: teardown() source = excinfo.traceback[-1].statement - assert str(source).strip() == "c(1)" + assert str(source).strip() == "c(1) # type: ignore" -def test_getfuncsource_dynamic(): +def test_getfuncsource_dynamic() -> None: source = """ def f(): raise ValueError @@ -368,11 +378,13 @@ def test_getfuncsource_dynamic(): """ co = _pytest._code.compile(source) exec(co, globals()) - assert str(_pytest._code.Source(f)).strip() == "def f():\n raise ValueError" - assert str(_pytest._code.Source(g)).strip() == "def g(): pass" + f_source = _pytest._code.Source(f) # type: ignore + g_source = _pytest._code.Source(g) # type: ignore + assert str(f_source).strip() == "def f():\n raise ValueError" + assert str(g_source).strip() == "def g(): pass" -def test_getfuncsource_with_multine_string(): +def test_getfuncsource_with_multine_string() -> None: def f(): c = """while True: pass @@ -387,7 +399,7 @@ def test_getfuncsource_with_multine_string(): assert str(_pytest._code.Source(f)) == expected.rstrip() -def test_deindent(): +def test_deindent() -> None: from _pytest._code.source import deindent as deindent assert deindent(["\tfoo", "\tbar"]) == ["foo", "bar"] @@ -401,7 +413,7 @@ def test_deindent(): assert lines == ["def f():", " def g():", " pass"] -def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot): +def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot) -> None: # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. source = _pytest._code.Source( @@ -423,7 +435,7 @@ if True: pass -def test_getsource_fallback(): +def test_getsource_fallback() -> None: from _pytest._code.source import getsource expected = """def x(): @@ -432,7 +444,7 @@ def test_getsource_fallback(): assert src == expected -def test_idem_compile_and_getsource(): +def test_idem_compile_and_getsource() -> None: from _pytest._code.source import getsource expected = "def x(): pass" @@ -441,15 +453,16 @@ def test_idem_compile_and_getsource(): assert src == expected -def test_findsource_fallback(): +def test_findsource_fallback() -> None: from _pytest._code.source import findsource src, lineno = findsource(x) + assert src is not None assert "test_findsource_simple" in str(src) assert src[lineno] == " def x():" -def test_findsource(): +def test_findsource() -> None: from _pytest._code.source import findsource co = _pytest._code.compile( @@ -460,25 +473,27 @@ def test_findsource(): ) src, lineno = findsource(co) + assert src is not None assert "if 1:" in str(src) - d = {} + d = {} # type: Dict[str, Any] eval(co, d) src, lineno = findsource(d["x"]) + assert src is not None assert "if 1:" in str(src) assert src[lineno] == " def x():" -def test_getfslineno(): +def test_getfslineno() -> None: from _pytest._code import getfslineno - def f(x): + def f(x) -> None: pass fspath, lineno = getfslineno(f) assert fspath.basename == "test_source.py" - assert lineno == _pytest._code.getrawcode(f).co_firstlineno - 1 # see findsource + assert lineno == f.__code__.co_firstlineno - 1 # see findsource class A: pass @@ -498,40 +513,40 @@ def test_getfslineno(): assert getfslineno(B)[1] == -1 -def test_code_of_object_instance_with_call(): +def test_code_of_object_instance_with_call() -> None: class A: pass pytest.raises(TypeError, lambda: _pytest._code.Source(A())) class WithCall: - def __call__(self): + def __call__(self) -> None: pass code = _pytest._code.Code(WithCall()) assert "pass" in str(code.source()) class Hello: - def __call__(self): + def __call__(self) -> None: pass pytest.raises(TypeError, lambda: _pytest._code.Code(Hello)) -def getstatement(lineno, source): +def getstatement(lineno: int, source) -> Source: from _pytest._code.source import getstatementrange_ast - source = _pytest._code.Source(source, deindent=False) - ast, start, end = getstatementrange_ast(lineno, source) - return source[start:end] + src = _pytest._code.Source(source, deindent=False) + ast, start, end = getstatementrange_ast(lineno, src) + return src[start:end] -def test_oneline(): +def test_oneline() -> None: source = getstatement(0, "raise ValueError") assert str(source) == "raise ValueError" -def test_comment_and_no_newline_at_end(): +def test_comment_and_no_newline_at_end() -> None: from _pytest._code.source import getstatementrange_ast source = Source( @@ -545,12 +560,12 @@ def test_comment_and_no_newline_at_end(): assert end == 2 -def test_oneline_and_comment(): +def test_oneline_and_comment() -> None: source = getstatement(0, "raise ValueError\n#hello") assert str(source) == "raise ValueError" -def test_comments(): +def test_comments() -> None: source = '''def test(): "comment 1" x = 1 @@ -576,7 +591,7 @@ comment 4 assert str(getstatement(line, source)) == '"""\ncomment 4\n"""' -def test_comment_in_statement(): +def test_comment_in_statement() -> None: source = """test(foo=1, # comment 1 bar=2) @@ -588,17 +603,17 @@ def test_comment_in_statement(): ) -def test_single_line_else(): +def test_single_line_else() -> None: source = getstatement(1, "if False: 2\nelse: 3") assert str(source) == "else: 3" -def test_single_line_finally(): +def test_single_line_finally() -> None: source = getstatement(1, "try: 1\nfinally: 3") assert str(source) == "finally: 3" -def test_issue55(): +def test_issue55() -> None: source = ( "def round_trip(dinp):\n assert 1 == dinp\n" 'def test_rt():\n round_trip("""\n""")\n' @@ -607,7 +622,7 @@ def test_issue55(): assert str(s) == ' round_trip("""\n""")' -def test_multiline(): +def test_multiline() -> None: source = getstatement( 0, """\ @@ -621,7 +636,7 @@ x = 3 class TestTry: - def setup_class(self): + def setup_class(self) -> None: self.source = """\ try: raise ValueError @@ -631,25 +646,25 @@ else: raise KeyError() """ - def test_body(self): + def test_body(self) -> None: source = getstatement(1, self.source) assert str(source) == " raise ValueError" - def test_except_line(self): + def test_except_line(self) -> None: source = getstatement(2, self.source) assert str(source) == "except Something:" - def test_except_body(self): + def test_except_body(self) -> None: source = getstatement(3, self.source) assert str(source) == " raise IndexError(1)" - def test_else(self): + def test_else(self) -> None: source = getstatement(5, self.source) assert str(source) == " raise KeyError()" class TestTryFinally: - def setup_class(self): + def setup_class(self) -> None: self.source = """\ try: raise ValueError @@ -657,17 +672,17 @@ finally: raise IndexError(1) """ - def test_body(self): + def test_body(self) -> None: source = getstatement(1, self.source) assert str(source) == " raise ValueError" - def test_finally(self): + def test_finally(self) -> None: source = getstatement(3, self.source) assert str(source) == " raise IndexError(1)" class TestIf: - def setup_class(self): + def setup_class(self) -> None: self.source = """\ if 1: y = 3 @@ -677,24 +692,24 @@ else: y = 7 """ - def test_body(self): + def test_body(self) -> None: source = getstatement(1, self.source) assert str(source) == " y = 3" - def test_elif_clause(self): + def test_elif_clause(self) -> None: source = getstatement(2, self.source) assert str(source) == "elif False:" - def test_elif(self): + def test_elif(self) -> None: source = getstatement(3, self.source) assert str(source) == " y = 5" - def test_else(self): + def test_else(self) -> None: source = getstatement(5, self.source) assert str(source) == " y = 7" -def test_semicolon(): +def test_semicolon() -> None: s = """\ hello ; pytest.skip() """ @@ -702,7 +717,7 @@ hello ; pytest.skip() assert str(source) == s.strip() -def test_def_online(): +def test_def_online() -> None: s = """\ def func(): raise ValueError(42) @@ -713,7 +728,7 @@ def something(): assert str(source) == "def func(): raise ValueError(42)" -def XXX_test_expression_multiline(): +def XXX_test_expression_multiline() -> None: source = """\ something ''' @@ -722,7 +737,7 @@ something assert str(result) == "'''\n'''" -def test_getstartingblock_multiline(): +def test_getstartingblock_multiline() -> None: class A: def __init__(self, *args): frame = sys._getframe(1) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index b8a22428f..59cb69a00 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,5 +1,8 @@ +import inspect + import pytest from _pytest import deprecated +from _pytest import nodes @pytest.mark.filterwarnings("default") @@ -16,7 +19,7 @@ def test_resultlog_is_deprecated(testdir): result = testdir.runpytest("--result-log=%s" % testdir.tmpdir.join("result.log")) result.stdout.fnmatch_lines( [ - "*--result-log is deprecated and scheduled for removal in pytest 6.0*", + "*--result-log is deprecated, please try the new pytest-reportlog plugin.", "*See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information*", ] ) @@ -44,3 +47,46 @@ def test_external_plugins_integrated(testdir, plugin): with pytest.warns(pytest.PytestConfigWarning): testdir.parseconfig("-p", plugin) + + +@pytest.mark.parametrize("junit_family", [None, "legacy", "xunit2"]) +def test_warn_about_imminent_junit_family_default_change(testdir, junit_family): + """Show a warning if junit_family is not defined and --junitxml is used (#6179)""" + testdir.makepyfile( + """ + def test_foo(): + pass + """ + ) + if junit_family: + testdir.makeini( + """ + [pytest] + junit_family={junit_family} + """.format( + junit_family=junit_family + ) + ) + + result = testdir.runpytest("--junit-xml=foo.xml") + warning_msg = ( + "*PytestDeprecationWarning: The 'junit_family' default value will change*" + ) + if junit_family: + result.stdout.no_fnmatch_line(warning_msg) + else: + result.stdout.fnmatch_lines([warning_msg]) + + +def test_node_direct_ctor_warning(): + class MockConfig: + pass + + ms = MockConfig() + with pytest.warns( + DeprecationWarning, + match="direct construction of .* has been deprecated, please use .*.from_parent", + ) as w: + nodes.Node(name="test", config=ms, session=ms, nodeid="None") + assert w[0].lineno == inspect.currentframe().f_lineno - 1 + assert w[0].filename == __file__ diff --git a/testing/python/collect.py b/testing/python/collect.py index 30f9841b5..9ac1c9d31 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -281,10 +281,10 @@ class TestFunction: from _pytest.fixtures import FixtureManager config = testdir.parseconfigure() - session = testdir.Session(config) + session = testdir.Session.from_config(config) session._fixturemanager = FixtureManager(session) - return pytest.Function(config=config, parent=session, **kwargs) + return pytest.Function.from_parent(config=config, parent=session, **kwargs) def test_function_equality(self, testdir, tmpdir): def func1(): @@ -1024,7 +1024,7 @@ class TestReportInfo: return "ABCDE", 42, "custom" def pytest_pycollect_makeitem(collector, name, obj): if name == "test_func": - return MyFunction(name, parent=collector) + return MyFunction.from_parent(name=name, parent=collector) """ ) item = testdir.getitem("def test_func(): pass") @@ -1210,6 +1210,28 @@ def test_syntax_error_with_non_ascii_chars(testdir): result.stdout.fnmatch_lines(["*ERROR collecting*", "*SyntaxError*", "*1 error in*"]) +def test_collecterror_with_fulltrace(testdir): + testdir.makepyfile("assert 0") + result = testdir.runpytest("--fulltrace") + result.stdout.fnmatch_lines( + [ + "collected 0 items / 1 error", + "", + "*= ERRORS =*", + "*_ ERROR collecting test_collecterror_with_fulltrace.py _*", + "", + "*/_pytest/python.py:*: ", + "_ _ _ _ _ _ _ _ *", + "", + "> assert 0", + "E assert 0", + "", + "test_collecterror_with_fulltrace.py:1: AssertionError", + "*! Interrupted: 1 error during collection !*", + ] + ) + + def test_skip_duplicates_by_default(testdir): """Test for issue https://github.com/pytest-dev/pytest/issues/1609 (#1609) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 6dca793e0..52fd32cc4 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -503,7 +503,7 @@ class TestRequestBasic: assert repr(req).find(req.function.__name__) != -1 def test_request_attributes_method(self, testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ import pytest class TestB(object): @@ -531,7 +531,7 @@ class TestRequestBasic: pass """ ) - item1, = testdir.genitems([modcol]) + (item1,) = testdir.genitems([modcol]) assert item1.name == "test_method" arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs assert len(arg2fixturedefs) == 1 @@ -781,7 +781,7 @@ class TestRequestBasic: def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") - item, = testdir.genitems([modcol]) + (item,) = testdir.genitems([modcol]) req = fixtures.FixtureRequest(item) assert req.fspath == modcol.fspath diff --git a/testing/python/integration.py b/testing/python/integration.py index 73419eef4..35e86e6b9 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -10,7 +10,7 @@ class TestOEJSKITSpecials: import pytest def pytest_pycollect_makeitem(collector, name, obj): if name == "MyClass": - return MyCollector(name, parent=collector) + return MyCollector.from_parent(collector, name=name) class MyCollector(pytest.Collector): def reportinfo(self): return self.fspath, 3, "xyz" @@ -40,7 +40,7 @@ class TestOEJSKITSpecials: import pytest def pytest_pycollect_makeitem(collector, name, obj): if name == "MyClass": - return MyCollector(name, parent=collector) + return MyCollector.from_parent(collector, name=name) class MyCollector(pytest.Collector): def reportinfo(self): return self.fspath, 3, "xyz" diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 5becb0f8c..9b6471cdc 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -9,10 +9,12 @@ from hypothesis import strategies import pytest from _pytest import fixtures from _pytest import python +from _pytest.outcomes import fail +from _pytest.python import _idval class TestMetafunc: - def Metafunc(self, func, config=None): + def Metafunc(self, func, config=None) -> python.Metafunc: # the unit tests of this class check if things work correctly # on the funcarg level, so we don't need a full blown # initialization @@ -23,12 +25,12 @@ class TestMetafunc: self.names_closure = names @attr.s - class DefinitionMock: + class DefinitionMock(python.FunctionDefinition): obj = attr.ib() names = fixtures.getfuncargnames(func) fixtureinfo = FixtureInfo(names) - definition = DefinitionMock(func) + definition = DefinitionMock._create(func) return python.Metafunc(definition, fixtureinfo, config) def test_no_funcargs(self, testdir): @@ -61,6 +63,39 @@ class TestMetafunc: pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5, 6])) pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5, 6])) + with pytest.raises( + TypeError, match="^ids must be a callable, sequence or generator$" + ): + metafunc.parametrize("y", [5, 6], ids=42) + + def test_parametrize_error_iterator(self): + def func(x): + raise NotImplementedError() + + class Exc(Exception): + def __repr__(self): + return "Exc(from_gen)" + + def gen(): + yield 0 + yield None + yield Exc() + + metafunc = self.Metafunc(func) + metafunc.parametrize("x", [1, 2], ids=gen()) + assert [(x.funcargs, x.id) for x in metafunc._calls] == [ + ({"x": 1}, "0"), + ({"x": 2}, "2"), + ] + with pytest.raises( + fail.Exception, + match=( + r"In func: ids must be list of string/float/int/bool, found:" + r" Exc\(from_gen\) \(type: \) at index 2" + ), + ): + metafunc.parametrize("x", [1, 2, 3], ids=gen()) + def test_parametrize_bad_scope(self, testdir): def func(x): pass @@ -72,6 +107,19 @@ class TestMetafunc: ): metafunc.parametrize("x", [1], scope="doggy") + def test_parametrize_request_name(self, testdir): + """Show proper error when 'request' is used as a parameter name in parametrize (#6183)""" + + def func(request): + raise NotImplementedError() + + metafunc = self.Metafunc(func) + with pytest.raises( + pytest.fail.Exception, + match=r"'request' is a reserved name and cannot be used in @pytest.mark.parametrize", + ): + metafunc.parametrize("request", [1]) + def test_find_parametrized_scope(self): """unittest for _find_parametrized_scope (#3941)""" from _pytest.python import _find_parametrized_scope @@ -154,6 +202,26 @@ class TestMetafunc: ("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"] ) + def test_parametrize_ids_iterator_without_mark(self): + import itertools + + def func(x, y): + pass + + it = itertools.count() + + metafunc = self.Metafunc(func) + metafunc.parametrize("x", [1, 2], ids=it) + metafunc.parametrize("y", [3, 4], ids=it) + ids = [x.id for x in metafunc._calls] + assert ids == ["0-2", "0-3", "1-2", "1-3"] + + metafunc = self.Metafunc(func) + metafunc.parametrize("x", [1, 2], ids=it) + metafunc.parametrize("y", [3, 4], ids=it) + ids = [x.id for x in metafunc._calls] + assert ids == ["4-6", "4-7", "5-6", "5-7"] + def test_parametrize_empty_list(self): """#510""" @@ -196,8 +264,6 @@ class TestMetafunc: deadline=400.0 ) # very close to std deadline and CI boxes are not reliable in CPU power def test_idval_hypothesis(self, value): - from _pytest.python import _idval - escaped = _idval(value, "a", 6, None, item=None, config=None) assert isinstance(escaped, str) escaped.encode("ascii") @@ -208,8 +274,6 @@ class TestMetafunc: escapes if they're not. """ - from _pytest.python import _idval - values = [ ("", ""), ("ascii", "ascii"), @@ -229,7 +293,6 @@ class TestMetafunc: disable_test_id_escaping_and_forfeit_all_rights_to_community_support option. (#5294) """ - from _pytest.python import _idval class MockConfig: def __init__(self, config): @@ -261,8 +324,6 @@ class TestMetafunc: "binary escape", where any byte < 127 is escaped into its hex form. - python3: bytes objects are always escaped using "binary escape". """ - from _pytest.python import _idval - values = [ (b"", ""), (b"\xc3\xb4\xff\xe4", "\\xc3\\xb4\\xff\\xe4"), @@ -276,7 +337,6 @@ class TestMetafunc: """unittest for the expected behavior to obtain ids for parametrized values that are classes or functions: their __name__. """ - from _pytest.python import _idval class TestClass: pass @@ -521,9 +581,22 @@ class TestMetafunc: @pytest.mark.parametrize("arg", ({1: 2}, {3, 4}), ids=ids) def test(arg): assert arg + + @pytest.mark.parametrize("arg", (1, 2.0, True), ids=ids) + def test_int(arg): + assert arg """ ) - assert testdir.runpytest().ret == 0 + result = testdir.runpytest("-vv", "-s") + result.stdout.fnmatch_lines( + [ + "test_parametrize_ids_returns_non_string.py::test[arg0] PASSED", + "test_parametrize_ids_returns_non_string.py::test[arg1] PASSED", + "test_parametrize_ids_returns_non_string.py::test_int[1] PASSED", + "test_parametrize_ids_returns_non_string.py::test_int[2.0] PASSED", + "test_parametrize_ids_returns_non_string.py::test_int[True] PASSED", + ] + ) def test_idmaker_with_ids(self): from _pytest.python import idmaker @@ -1173,12 +1246,12 @@ class TestMetafuncFunctional: result.stdout.fnmatch_lines(["* 1 skipped *"]) def test_parametrized_ids_invalid_type(self, testdir): - """Tests parametrized with ids as non-strings (#1857).""" + """Test error with non-strings/non-ints, without generator (#1857).""" testdir.makepyfile( """ import pytest - @pytest.mark.parametrize("x, expected", [(10, 20), (40, 80)], ids=(None, 2)) + @pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, type)) def test_ids_numbers(x,expected): assert x * 2 == expected """ @@ -1186,7 +1259,8 @@ class TestMetafuncFunctional: result = testdir.runpytest() result.stdout.fnmatch_lines( [ - "*In test_ids_numbers: ids must be list of strings, found: 2 (type: *'int'>)*" + "In test_ids_numbers: ids must be list of string/float/int/bool," + " found: (type: ) at index 2" ] ) @@ -1310,25 +1384,29 @@ class TestMetafuncFunctional: reprec = testdir.runpytest() reprec.assert_outcomes(passed=4) - @pytest.mark.parametrize("attr", ["parametrise", "parameterize", "parameterise"]) - def test_parametrize_misspelling(self, testdir, attr): + def test_parametrize_misspelling(self, testdir): """#463""" testdir.makepyfile( """ import pytest - @pytest.mark.{}("x", range(2)) + @pytest.mark.parametrise("x", range(2)) def test_foo(x): pass - """.format( - attr - ) + """ ) result = testdir.runpytest("--collectonly") result.stdout.fnmatch_lines( [ - "test_foo has '{}' mark, spelling should be 'parametrize'".format(attr), - "*1 error in*", + "collected 0 items / 1 error", + "", + "*= ERRORS =*", + "*_ ERROR collecting test_parametrize_misspelling.py _*", + "test_parametrize_misspelling.py:3: in ", + ' @pytest.mark.parametrise("x", range(2))', + "E Failed: Unknown 'parametrise' mark, did you mean 'parametrize'?", + "*! Interrupted: 1 error during collection !*", + "*= 1 error in *", ] ) @@ -1538,27 +1616,6 @@ class TestMarkersWithParametrization: assert len(skipped) == 0 assert len(fail) == 0 - @pytest.mark.xfail(reason="is this important to support??") - def test_nested_marks(self, testdir): - s = """ - import pytest - mastermark = pytest.mark.foo(pytest.mark.bar) - - @pytest.mark.parametrize(("n", "expected"), [ - (1, 2), - mastermark((1, 3)), - (2, 3), - ]) - def test_increment(n, expected): - assert n + 1 == expected - """ - items = testdir.getitems(s) - assert len(items) == 3 - for mark in ["foo", "bar"]: - assert mark not in items[0].keywords - assert mark in items[1].keywords - assert mark not in items[2].keywords - def test_simple_xfail(self, testdir): s = """ import pytest @@ -1784,3 +1841,39 @@ class TestMarkersWithParametrization: ) result = testdir.runpytest() result.assert_outcomes(passed=1) + + def test_parametrize_iterator(self, testdir): + testdir.makepyfile( + """ + import itertools + import pytest + + id_parametrize = pytest.mark.parametrize( + ids=("param%d" % i for i in itertools.count()) + ) + + @id_parametrize('y', ['a', 'b']) + def test1(y): + pass + + @id_parametrize('y', ['a', 'b']) + def test2(y): + pass + + @pytest.mark.parametrize("a, b", [(1, 2), (3, 4)], ids=itertools.count()) + def test_converted_to_str(a, b): + pass + """ + ) + result = testdir.runpytest("-vv", "-s") + result.stdout.fnmatch_lines( + [ + "test_parametrize_iterator.py::test1[param0] PASSED", + "test_parametrize_iterator.py::test1[param1] PASSED", + "test_parametrize_iterator.py::test2[param0] PASSED", + "test_parametrize_iterator.py::test2[param1] PASSED", + "test_parametrize_iterator.py::test_converted_to_str[0] PASSED", + "test_parametrize_iterator.py::test_converted_to_str[1] PASSED", + "*= 6 passed in *", + ] + ) diff --git a/testing/python/raises.py b/testing/python/raises.py index 28b0715c0..1c701796a 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -205,7 +205,7 @@ class TestRaises: with pytest.raises(AssertionError) as excinfo: with pytest.raises(AssertionError, match="'foo"): raise AssertionError("'bar") - msg, = excinfo.value.args + (msg,) = excinfo.value.args assert msg == 'Pattern "\'foo" not found in "\'bar"' def test_raises_match_wrong_type(self): diff --git a/testing/test_assertion.py b/testing/test_assertion.py index aac21a0df..e4d68ff8c 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -70,7 +70,14 @@ class TestImportHookInstallation: """ ) result = testdir.runpytest_subprocess() - result.stdout.fnmatch_lines(["*assert 1 == 0*"]) + result.stdout.fnmatch_lines( + [ + "E * AssertionError: ([[][]], [[][]], [[][]])*", + "E * assert" + " {'failed': 1, 'passed': 0, 'skipped': 0} ==" + " {'failed': 0, 'passed': 1, 'skipped': 0}", + ] + ) @pytest.mark.parametrize("mode", ["plain", "rewrite"]) def test_pytest_plugins_rewrite(self, testdir, mode): @@ -462,6 +469,29 @@ class TestAssert_reprcompare: " ]", ] + def test_list_dont_wrap_strings(self): + long_a = "a" * 10 + l1 = ["a"] + [long_a for _ in range(0, 7)] + l2 = ["should not get wrapped"] + diff = callequal(l1, l2, verbose=True) + assert diff == [ + "['a', 'aaaaaa...aaaaaaa', ...] == ['should not get wrapped']", + "At index 0 diff: 'a' != 'should not get wrapped'", + "Left contains 7 more items, first extra item: 'aaaaaaaaaa'", + "Full diff:", + " [", + "+ 'should not get wrapped',", + "- 'a',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + " ]", + ] + def test_dict_wrap(self): d1 = {"common": 1, "env": {"env1": 1}} d2 = {"common": 1, "env": {"env1": 1, "env2": 2}} @@ -479,22 +509,20 @@ class TestAssert_reprcompare: ] long_a = "a" * 80 - sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped"}} + sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 2}} d1 = {"env": {"sub": sub}} d2 = {"env": {"sub": sub}, "new": 1} diff = callequal(d1, d2, verbose=True) assert diff == [ - "{'env': {'sub...s wrapped'}}}} == {'env': {'sub...}}}, 'new': 1}", + "{'env': {'sub... wrapped '}}}} == {'env': {'sub...}}}, 'new': 1}", "Omitting 1 identical items, use -vv to show", "Right contains 1 more item:", "{'new': 1}", "Full diff:", " {", " 'env': {'sub': {'long_a': '" + long_a + "',", - " 'sub1': {'long_a': 'substring '", - " 'that '", - " 'gets '", - " 'wrapped'}}},", + " 'sub1': {'long_a': 'substring that gets wrapped substring '", + " 'that gets wrapped '}}},", "+ 'new': 1,", " }", ] diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index e2d6b89c8..8490a59e6 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -9,7 +9,6 @@ import sys import textwrap import zipfile from functools import partial -from pathlib import Path import py @@ -23,6 +22,7 @@ from _pytest.assertion.rewrite import PYC_TAIL from _pytest.assertion.rewrite import PYTEST_TAG from _pytest.assertion.rewrite import rewrite_asserts from _pytest.main import ExitCode +from _pytest.pathlib import Path def setup_module(mod): diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 3f03b5ff9..0e1194b02 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -2,7 +2,6 @@ import os import shutil import stat import sys -import textwrap import py @@ -60,18 +59,13 @@ class TestNewAPI: @pytest.mark.filterwarnings( "ignore:could not create cache path:pytest.PytestWarning" ) - def test_cache_failure_warns(self, testdir): + def test_cache_failure_warns(self, testdir, monkeypatch): + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") cache_dir = str(testdir.tmpdir.ensure_dir(".pytest_cache")) mode = os.stat(cache_dir)[stat.ST_MODE] testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) try: - testdir.makepyfile( - """ - def test_error(): - raise Exception - - """ - ) + testdir.makepyfile("def test_error(): raise Exception") result = testdir.runpytest("-rw") assert result.ret == 1 # warnings from nodeids, lastfailed, and stepwise @@ -178,12 +172,7 @@ def test_cache_reportheader_external_abspath(testdir, tmpdir_factory): "test_cache_reportheader_external_abspath_abs" ) - testdir.makepyfile( - """ - def test_hello(): - pass - """ - ) + testdir.makepyfile("def test_hello(): pass") testdir.makeini( """ [pytest] @@ -192,7 +181,6 @@ def test_cache_reportheader_external_abspath(testdir, tmpdir_factory): abscache=external_cache ) ) - result = testdir.runpytest("-v") result.stdout.fnmatch_lines( ["cachedir: {abscache}".format(abscache=external_cache)] @@ -253,36 +241,26 @@ def test_cache_show(testdir): class TestLastFailed: def test_lastfailed_usecase(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") + monkeypatch.setattr("sys.dont_write_bytecode", True) p = testdir.makepyfile( """ - def test_1(): - assert 0 - def test_2(): - assert 0 - def test_3(): - assert 1 - """ + def test_1(): assert 0 + def test_2(): assert 0 + def test_3(): assert 1 + """ ) - result = testdir.runpytest() + result = testdir.runpytest(str(p)) result.stdout.fnmatch_lines(["*2 failed*"]) - p.write( - textwrap.dedent( - """\ - def test_1(): - assert 1 - - def test_2(): - assert 1 - - def test_3(): - assert 0 - """ - ) + p = testdir.makepyfile( + """ + def test_1(): assert 1 + def test_2(): assert 1 + def test_3(): assert 0 + """ ) - result = testdir.runpytest("--lf") + result = testdir.runpytest(str(p), "--lf") result.stdout.fnmatch_lines(["*2 passed*1 desel*"]) - result = testdir.runpytest("--lf") + result = testdir.runpytest(str(p), "--lf") result.stdout.fnmatch_lines( [ "collected 3 items", @@ -290,7 +268,7 @@ class TestLastFailed: "*1 failed*2 passed*", ] ) - result = testdir.runpytest("--lf", "--cache-clear") + result = testdir.runpytest(str(p), "--lf", "--cache-clear") result.stdout.fnmatch_lines(["*1 failed*2 passed*"]) # Run this again to make sure clear-cache is robust @@ -300,21 +278,9 @@ class TestLastFailed: result.stdout.fnmatch_lines(["*1 failed*2 passed*"]) def test_failedfirst_order(self, testdir): - testdir.tmpdir.join("test_a.py").write( - textwrap.dedent( - """\ - def test_always_passes(): - assert 1 - """ - ) - ) - testdir.tmpdir.join("test_b.py").write( - textwrap.dedent( - """\ - def test_always_fails(): - assert 0 - """ - ) + testdir.makepyfile( + test_a="def test_always_passes(): pass", + test_b="def test_always_fails(): assert 0", ) result = testdir.runpytest() # Test order will be collection order; alphabetical @@ -325,16 +291,8 @@ class TestLastFailed: def test_lastfailed_failedfirst_order(self, testdir): testdir.makepyfile( - **{ - "test_a.py": """\ - def test_always_passes(): - assert 1 - """, - "test_b.py": """\ - def test_always_fails(): - assert 0 - """, - } + test_a="def test_always_passes(): assert 1", + test_b="def test_always_fails(): assert 0", ) result = testdir.runpytest() # Test order will be collection order; alphabetical @@ -345,18 +303,13 @@ class TestLastFailed: result.stdout.no_fnmatch_line("*test_a.py*") def test_lastfailed_difference_invocations(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") + monkeypatch.setattr("sys.dont_write_bytecode", True) testdir.makepyfile( - test_a="""\ - def test_a1(): - assert 0 - def test_a2(): - assert 1 - """, - test_b="""\ - def test_b1(): - assert 0 + test_a=""" + def test_a1(): assert 0 + def test_a2(): assert 1 """, + test_b="def test_b1(): assert 0", ) p = testdir.tmpdir.join("test_a.py") p2 = testdir.tmpdir.join("test_b.py") @@ -365,36 +318,19 @@ class TestLastFailed: result.stdout.fnmatch_lines(["*2 failed*"]) result = testdir.runpytest("--lf", p2) result.stdout.fnmatch_lines(["*1 failed*"]) - p2.write( - textwrap.dedent( - """\ - def test_b1(): - assert 1 - """ - ) - ) + + testdir.makepyfile(test_b="def test_b1(): assert 1") result = testdir.runpytest("--lf", p2) result.stdout.fnmatch_lines(["*1 passed*"]) result = testdir.runpytest("--lf", p) result.stdout.fnmatch_lines(["*1 failed*1 desel*"]) def test_lastfailed_usecase_splice(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") + monkeypatch.setattr("sys.dont_write_bytecode", True) testdir.makepyfile( - """\ - def test_1(): - assert 0 - """ + "def test_1(): assert 0", test_something="def test_2(): assert 0" ) p2 = testdir.tmpdir.join("test_something.py") - p2.write( - textwrap.dedent( - """\ - def test_2(): - assert 0 - """ - ) - ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*2 failed*"]) result = testdir.runpytest("--lf", p2) @@ -436,18 +372,14 @@ class TestLastFailed: def test_terminal_report_lastfailed(self, testdir): test_a = testdir.makepyfile( test_a=""" - def test_a1(): - pass - def test_a2(): - pass + def test_a1(): pass + def test_a2(): pass """ ) test_b = testdir.makepyfile( test_b=""" - def test_b1(): - assert 0 - def test_b2(): - assert 0 + def test_b1(): assert 0 + def test_b2(): assert 0 """ ) result = testdir.runpytest() @@ -492,10 +424,8 @@ class TestLastFailed: def test_terminal_report_failedfirst(self, testdir): testdir.makepyfile( test_a=""" - def test_a1(): - assert 0 - def test_a2(): - pass + def test_a1(): assert 0 + def test_a2(): pass """ ) result = testdir.runpytest() @@ -542,7 +472,6 @@ class TestLastFailed: assert list(lastfailed) == ["test_maybe.py::test_hello"] def test_lastfailed_failure_subset(self, testdir, monkeypatch): - testdir.makepyfile( test_maybe=""" import os @@ -560,6 +489,7 @@ class TestLastFailed: env = os.environ if '1' == env['FAILIMPORT']: raise ImportError('fail') + def test_hello(): assert '0' == env['FAILTEST'] @@ -613,8 +543,7 @@ class TestLastFailed: """ import pytest @pytest.mark.xfail - def test(): - assert 0 + def test(): assert 0 """ ) result = testdir.runpytest() @@ -626,8 +555,7 @@ class TestLastFailed: """ import pytest @pytest.mark.xfail(strict=True) - def test(): - pass + def test(): pass """ ) result = testdir.runpytest() @@ -641,8 +569,7 @@ class TestLastFailed: testdir.makepyfile( """ import pytest - def test(): - assert 0 + def test(): assert 0 """ ) result = testdir.runpytest() @@ -655,8 +582,7 @@ class TestLastFailed: """ import pytest @pytest.{mark} - def test(): - assert 0 + def test(): assert 0 """.format( mark=mark ) @@ -694,18 +620,14 @@ class TestLastFailed: # 1. initial run test_bar = testdir.makepyfile( test_bar=""" - def test_bar_1(): - pass - def test_bar_2(): - assert 0 + def test_bar_1(): pass + def test_bar_2(): assert 0 """ ) test_foo = testdir.makepyfile( test_foo=""" - def test_foo_3(): - pass - def test_foo_4(): - assert 0 + def test_foo_3(): pass + def test_foo_4(): assert 0 """ ) testdir.runpytest() @@ -717,10 +639,8 @@ class TestLastFailed: # 2. fix test_bar_2, run only test_bar.py testdir.makepyfile( test_bar=""" - def test_bar_1(): - pass - def test_bar_2(): - pass + def test_bar_1(): pass + def test_bar_2(): pass """ ) result = testdir.runpytest(test_bar) @@ -735,10 +655,8 @@ class TestLastFailed: # 3. fix test_foo_4, run only test_foo.py test_foo = testdir.makepyfile( test_foo=""" - def test_foo_3(): - pass - def test_foo_4(): - pass + def test_foo_3(): pass + def test_foo_4(): pass """ ) result = testdir.runpytest(test_foo, "--last-failed") @@ -752,10 +670,8 @@ class TestLastFailed: def test_lastfailed_no_failures_behavior_all_passed(self, testdir): testdir.makepyfile( """ - def test_1(): - assert True - def test_2(): - assert True + def test_1(): pass + def test_2(): pass """ ) result = testdir.runpytest() @@ -777,10 +693,8 @@ class TestLastFailed: def test_lastfailed_no_failures_behavior_empty_cache(self, testdir): testdir.makepyfile( """ - def test_1(): - assert True - def test_2(): - assert False + def test_1(): pass + def test_2(): assert 0 """ ) result = testdir.runpytest("--lf", "--cache-clear") @@ -1022,22 +936,12 @@ class TestReadme: return readme.is_file() def test_readme_passed(self, testdir): - testdir.makepyfile( - """ - def test_always_passes(): - assert 1 - """ - ) + testdir.makepyfile("def test_always_passes(): pass") testdir.runpytest() assert self.check_readme(testdir) is True def test_readme_failed(self, testdir): - testdir.makepyfile( - """ - def test_always_fails(): - assert 0 - """ - ) + testdir.makepyfile("def test_always_fails(): assert 0") testdir.runpytest() assert self.check_readme(testdir) is True diff --git a/testing/test_capture.py b/testing/test_capture.py index 85b0b05ae..94af3aef7 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -92,8 +92,6 @@ class TestCaptureManager: @pytest.mark.parametrize("method", ["fd", "sys"]) def test_capturing_unicode(testdir, method): - if hasattr(sys, "pypy_version_info") and sys.pypy_version_info < (2, 2): - pytest.xfail("does not work on pypy < 2.2") obj = "'b\u00f6y'" testdir.makepyfile( """\ diff --git a/testing/test_collection.py b/testing/test_collection.py index 83345d2c6..624e9dd4e 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -75,7 +75,7 @@ class TestCollector: pass def pytest_collect_file(path, parent): if path.ext == ".xxx": - return CustomFile(path, parent=parent) + return CustomFile.from_parent(fspath=path, parent=parent) """ ) node = testdir.getpathnode(hello) @@ -446,7 +446,7 @@ class TestSession: p.move(target) subdir.chdir() config = testdir.parseconfig(p.basename) - rcol = Session(config=config) + rcol = Session.from_config(config) assert rcol.fspath == subdir parts = rcol._parsearg(p.basename) @@ -463,7 +463,7 @@ class TestSession: # XXX migrate to collectonly? (see below) config = testdir.parseconfig(id) topdir = testdir.tmpdir - rcol = Session(config) + rcol = Session.from_config(config) assert topdir == rcol.fspath # rootid = rcol.nodeid # root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0] @@ -486,7 +486,7 @@ class TestSession: p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) items, hookrec = testdir.inline_genitems(id) - item, = items + (item,) = items assert item.name == "test_func" newid = item.nodeid assert newid == id @@ -605,9 +605,9 @@ class TestSession: testdir.makepyfile("def test_func(): pass") items, hookrec = testdir.inline_genitems() assert len(items) == 1 - item, = items + (item,) = items items2, hookrec = testdir.inline_genitems(item.nodeid) - item2, = items2 + (item2,) = items2 assert item2.name == item.name assert item2.fspath == item.fspath @@ -622,7 +622,7 @@ class TestSession: arg = p.basename + "::TestClass::test_method" items, hookrec = testdir.inline_genitems(arg) assert len(items) == 1 - item, = items + (item,) = items assert item.nodeid.endswith("TestClass::test_method") # ensure we are reporting the collection of the single test item (#2464) assert [x.name for x in self.get_reported_items(hookrec)] == ["test_method"] @@ -685,6 +685,8 @@ class Test_genitems: def test_example_items1(self, testdir): p = testdir.makepyfile( """ + import pytest + def testone(): pass @@ -693,19 +695,24 @@ class Test_genitems: pass class TestY(TestX): - pass + @pytest.mark.parametrize("arg0", [".["]) + def testmethod_two(self, arg0): + pass """ ) items, reprec = testdir.inline_genitems(p) - assert len(items) == 3 + assert len(items) == 4 assert items[0].name == "testone" assert items[1].name == "testmethod_one" assert items[2].name == "testmethod_one" + assert items[3].name == "testmethod_two[.[]" # let's also test getmodpath here assert items[0].getmodpath() == "testone" assert items[1].getmodpath() == "TestX.testmethod_one" assert items[2].getmodpath() == "TestY.testmethod_one" + # PR #6202: Fix incorrect result of getmodpath method. (Resolves issue #6189) + assert items[3].getmodpath() == "TestY.testmethod_two[.[]" s = items[0].getmodpath(stopatmodule=False) assert s.endswith("test_example_items1.testone") @@ -852,11 +859,15 @@ def test_exit_on_collection_with_maxfail_smaller_than_n_errors(testdir): res = testdir.runpytest("--maxfail=1") assert res.ret == 1 - res.stdout.fnmatch_lines( - ["*ERROR collecting test_02_import_error.py*", "*No module named *asdfa*"] + [ + "collected 1 item / 1 error", + "*ERROR collecting test_02_import_error.py*", + "*No module named *asdfa*", + "*! stopping after 1 failures !*", + "*= 1 error in *", + ] ) - res.stdout.no_fnmatch_line("*test_03*") @@ -869,7 +880,6 @@ def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir): res = testdir.runpytest("--maxfail=4") assert res.ret == 2 - res.stdout.fnmatch_lines( [ "collected 2 items / 2 errors", @@ -877,6 +887,8 @@ def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir): "*No module named *asdfa*", "*ERROR collecting test_03_import_error.py*", "*No module named *asdfa*", + "*! Interrupted: 2 errors during collection !*", + "*= 2 errors in *", ] ) @@ -1257,3 +1269,24 @@ def test_collector_respects_tbstyle(testdir): "*= 1 error in *", ] ) + + +def test_does_not_eagerly_collect_packages(testdir): + testdir.makepyfile("def test(): pass") + pydir = testdir.mkpydir("foopkg") + pydir.join("__init__.py").write("assert False") + result = testdir.runpytest() + assert result.ret == ExitCode.OK + + +def test_does_not_put_src_on_path(testdir): + # `src` is not on sys.path so it should not be importable + testdir.tmpdir.join("src/nope/__init__.py").ensure() + testdir.makepyfile( + "import pytest\n" + "def test():\n" + " with pytest.raises(ImportError):\n" + " import nope\n" + ) + result = testdir.runpytest() + assert result.ret == ExitCode.OK diff --git a/testing/test_compat.py b/testing/test_compat.py index 94dac439d..04d818b4e 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -4,6 +4,7 @@ from functools import wraps import pytest from _pytest.compat import _PytestWrapper +from _pytest.compat import cached_property from _pytest.compat import get_real_func from _pytest.compat import is_generator from _pytest.compat import safe_getattr @@ -178,3 +179,23 @@ def test_safe_isclass(): assert False, "Should be ignored" assert safe_isclass(CrappyClass()) is False + + +def test_cached_property() -> None: + ncalls = 0 + + class Class: + @cached_property + def prop(self) -> int: + nonlocal ncalls + ncalls += 1 + return ncalls + + c1 = Class() + assert ncalls == 0 + assert c1.prop == 1 + assert c1.prop == 1 + c2 = Class() + assert ncalls == 1 + assert c2.prop == 2 + assert c1.prop == 1 diff --git a/testing/test_config.py b/testing/test_config.py index d4d624348..f146b52a4 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,7 +1,6 @@ import os import sys import textwrap -from pathlib import Path import _pytest._code import pytest @@ -13,6 +12,7 @@ from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import getcfg from _pytest.main import ExitCode +from _pytest.pathlib import Path class TestParseIni: diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 0374db0b3..2918ff04c 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,12 +1,12 @@ import os import textwrap -from pathlib import Path import py import pytest from _pytest.config import PytestPluginManager from _pytest.main import ExitCode +from _pytest.pathlib import Path def ConftestWithSetinitial(path): diff --git a/testing/test_pdb.py b/testing/test_debugging.py similarity index 98% rename from testing/test_pdb.py rename to testing/test_debugging.py index 25d2292e9..8949b0de8 100644 --- a/testing/test_pdb.py +++ b/testing/test_debugging.py @@ -22,7 +22,7 @@ def pdb_env(request): if "testdir" in request.fixturenames: # Disable pdb++ with inner tests. testdir = request.getfixturevalue("testdir") - testdir._env_run_update["PDBPP_HIJACK_PDB"] = "0" + testdir.monkeypatch.setenv("PDBPP_HIJACK_PDB", "0") def runpdb_and_get_report(testdir, source): @@ -193,7 +193,7 @@ class TestPDB: ) child = testdir.spawn_pytest("-rs --pdb %s" % p1) child.expect("Skipping also with pdb active") - child.expect_exact("= \x1b[33m\x1b[1m1 skipped\x1b[0m\x1b[33m in") + child.expect_exact("= 1 skipped in") child.sendeof() self.flush(child) @@ -221,7 +221,7 @@ class TestPDB: child.sendeof() rest = child.read().decode("utf8") assert "Exit: Quitting debugger" in rest - assert "= \x1b[31m\x1b[1m1 failed\x1b[0m\x1b[31m in" in rest + assert "= 1 failed in" in rest assert "def test_1" not in rest assert "get rekt" not in rest self.flush(child) @@ -506,7 +506,7 @@ class TestPDB: rest = child.read().decode("utf8") assert "! _pytest.outcomes.Exit: Quitting debugger !" in rest - assert "= \x1b[33mno tests ran\x1b[0m\x1b[33m in" in rest + assert "= no tests ran in" in rest assert "BdbQuit" not in rest assert "UNEXPECTED EXCEPTION" not in rest @@ -725,7 +725,7 @@ class TestPDB: assert "> PDB continue (IO-capturing resumed) >" in rest else: assert "> PDB continue >" in rest - assert "= \x1b[32m\x1b[1m1 passed\x1b[0m\x1b[32m in" in rest + assert "= 1 passed in" in rest def test_pdb_used_outside_test(self, testdir): p1 = testdir.makepyfile( @@ -1041,7 +1041,7 @@ class TestTraceOption: child.sendline("q") child.expect_exact("Exit: Quitting debugger") rest = child.read().decode("utf8") - assert "= \x1b[32m\x1b[1m2 passed\x1b[0m\x1b[32m in" in rest + assert "= 2 passed in" in rest assert "reading from stdin while output" not in rest # Only printed once - not on stderr. assert "Exit: Quitting debugger" not in child.before.decode("utf8") @@ -1086,7 +1086,7 @@ class TestTraceOption: child.sendline("c") child.expect_exact("> PDB continue (IO-capturing resumed) >") rest = child.read().decode("utf8") - assert "= \x1b[32m\x1b[1m6 passed\x1b[0m\x1b[32m in" in rest + assert "= 6 passed in" in rest assert "reading from stdin while output" not in rest # Only printed once - not on stderr. assert "Exit: Quitting debugger" not in child.before.decode("utf8") @@ -1197,7 +1197,7 @@ def test_pdb_suspends_fixture_capturing(testdir, fixture): TestPDB.flush(child) assert child.exitstatus == 0 - assert "= \x1b[32m\x1b[1m1 passed\x1b[0m\x1b[32m in" in rest + assert "= 1 passed in" in rest assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 885d25941..4c2f22a3d 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,7 +1,6 @@ import os import platform from datetime import datetime -from pathlib import Path from xml.dom import minidom import py @@ -9,6 +8,7 @@ import xmlschema import pytest from _pytest.junitxml import LogXML +from _pytest.pathlib import Path from _pytest.reports import BaseReport diff --git a/testing/test_mark.py b/testing/test_mark.py index ba7599804..33276b63c 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -962,7 +962,11 @@ def test_mark_expressions_no_smear(testdir): def test_addmarker_order(): - node = Node("Test", config=mock.Mock(), session=mock.Mock(), nodeid="Test") + session = mock.Mock() + session.own_markers = [] + session.parent = None + session.nodeid = "" + node = Node.from_parent(session, name="Test") node.add_marker("foo") node.add_marker("bar") node.add_marker("baz", append=False) @@ -1011,7 +1015,7 @@ def test_markers_from_parametrize(testdir): def test_pytest_param_id_requires_string(): with pytest.raises(TypeError) as excinfo: pytest.param(id=True) - msg, = excinfo.value.args + (msg,) = excinfo.value.args assert msg == "Expected id to be a string, got : True" diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 915747378..cdccc240e 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -12,22 +12,22 @@ from _pytest.config.exceptions import UsageError @pytest.fixture -def parser(): +def parser() -> parseopt.Parser: return parseopt.Parser() class TestParser: - def test_no_help_by_default(self): + def test_no_help_by_default(self) -> None: parser = parseopt.Parser(usage="xyz") pytest.raises(UsageError, lambda: parser.parse(["-h"])) - def test_custom_prog(self, parser): + def test_custom_prog(self, parser: parseopt.Parser) -> None: """Custom prog can be set for `argparse.ArgumentParser`.""" assert parser._getparser().prog == os.path.basename(sys.argv[0]) parser.prog = "custom-prog" assert parser._getparser().prog == "custom-prog" - def test_argument(self): + def test_argument(self) -> None: with pytest.raises(parseopt.ArgumentError): # need a short or long option argument = parseopt.Argument() @@ -45,7 +45,7 @@ class TestParser: "Argument(_short_opts: ['-t'], _long_opts: ['--test'], dest: 'abc')" ) - def test_argument_type(self): + def test_argument_type(self) -> None: argument = parseopt.Argument("-t", dest="abc", type=int) assert argument.type is int argument = parseopt.Argument("-t", dest="abc", type=str) @@ -60,7 +60,7 @@ class TestParser: ) assert argument.type is str - def test_argument_processopt(self): + def test_argument_processopt(self) -> None: argument = parseopt.Argument("-t", type=int) argument.default = 42 argument.dest = "abc" @@ -68,19 +68,19 @@ class TestParser: assert res["default"] == 42 assert res["dest"] == "abc" - def test_group_add_and_get(self, parser): + def test_group_add_and_get(self, parser: parseopt.Parser) -> None: group = parser.getgroup("hello", description="desc") assert group.name == "hello" assert group.description == "desc" - def test_getgroup_simple(self, parser): + def test_getgroup_simple(self, parser: parseopt.Parser) -> None: group = parser.getgroup("hello", description="desc") assert group.name == "hello" assert group.description == "desc" group2 = parser.getgroup("hello") assert group2 is group - def test_group_ordering(self, parser): + def test_group_ordering(self, parser: parseopt.Parser) -> None: parser.getgroup("1") parser.getgroup("2") parser.getgroup("3", after="1") @@ -88,20 +88,20 @@ class TestParser: groups_names = [x.name for x in groups] assert groups_names == list("132") - def test_group_addoption(self): + def test_group_addoption(self) -> None: group = parseopt.OptionGroup("hello") group.addoption("--option1", action="store_true") assert len(group.options) == 1 assert isinstance(group.options[0], parseopt.Argument) - def test_group_addoption_conflict(self): + def test_group_addoption_conflict(self) -> None: group = parseopt.OptionGroup("hello again") group.addoption("--option1", "--option-1", action="store_true") with pytest.raises(ValueError) as err: group.addoption("--option1", "--option-one", action="store_true") assert str({"--option1"}) in str(err.value) - def test_group_shortopt_lowercase(self, parser): + def test_group_shortopt_lowercase(self, parser: parseopt.Parser) -> None: group = parser.getgroup("hello") with pytest.raises(ValueError): group.addoption("-x", action="store_true") @@ -109,30 +109,30 @@ class TestParser: group._addoption("-x", action="store_true") assert len(group.options) == 1 - def test_parser_addoption(self, parser): + def test_parser_addoption(self, parser: parseopt.Parser) -> None: group = parser.getgroup("custom options") assert len(group.options) == 0 group.addoption("--option1", action="store_true") assert len(group.options) == 1 - def test_parse(self, parser): + def test_parse(self, parser: parseopt.Parser) -> None: parser.addoption("--hello", dest="hello", action="store") args = parser.parse(["--hello", "world"]) assert args.hello == "world" assert not getattr(args, parseopt.FILE_OR_DIR) - def test_parse2(self, parser): + def test_parse2(self, parser: parseopt.Parser) -> None: args = parser.parse([py.path.local()]) assert getattr(args, parseopt.FILE_OR_DIR)[0] == py.path.local() - def test_parse_known_args(self, parser): + def test_parse_known_args(self, parser: parseopt.Parser) -> None: parser.parse_known_args([py.path.local()]) parser.addoption("--hello", action="store_true") ns = parser.parse_known_args(["x", "--y", "--hello", "this"]) assert ns.hello assert ns.file_or_dir == ["x"] - def test_parse_known_and_unknown_args(self, parser): + def test_parse_known_and_unknown_args(self, parser: parseopt.Parser) -> None: parser.addoption("--hello", action="store_true") ns, unknown = parser.parse_known_and_unknown_args( ["x", "--y", "--hello", "this"] @@ -141,7 +141,7 @@ class TestParser: assert ns.file_or_dir == ["x"] assert unknown == ["--y", "this"] - def test_parse_will_set_default(self, parser): + def test_parse_will_set_default(self, parser: parseopt.Parser) -> None: parser.addoption("--hello", dest="hello", default="x", action="store") option = parser.parse([]) assert option.hello == "x" @@ -149,25 +149,22 @@ class TestParser: parser.parse_setoption([], option) assert option.hello == "x" - def test_parse_setoption(self, parser): + def test_parse_setoption(self, parser: parseopt.Parser) -> None: parser.addoption("--hello", dest="hello", action="store") parser.addoption("--world", dest="world", default=42) - class A: - pass - - option = A() + option = argparse.Namespace() args = parser.parse_setoption(["--hello", "world"], option) assert option.hello == "world" assert option.world == 42 assert not args - def test_parse_special_destination(self, parser): + def test_parse_special_destination(self, parser: parseopt.Parser) -> None: parser.addoption("--ultimate-answer", type=int) args = parser.parse(["--ultimate-answer", "42"]) assert args.ultimate_answer == 42 - def test_parse_split_positional_arguments(self, parser): + def test_parse_split_positional_arguments(self, parser: parseopt.Parser) -> None: parser.addoption("-R", action="store_true") parser.addoption("-S", action="store_false") args = parser.parse(["-R", "4", "2", "-S"]) @@ -181,7 +178,7 @@ class TestParser: assert args.R is True assert args.S is False - def test_parse_defaultgetter(self): + def test_parse_defaultgetter(self) -> None: def defaultget(option): if not hasattr(option, "type"): return @@ -199,17 +196,17 @@ class TestParser: assert option.this == 42 assert option.no is False - def test_drop_short_helper(self): + def test_drop_short_helper(self) -> None: parser = argparse.ArgumentParser( formatter_class=parseopt.DropShorterLongHelpFormatter, allow_abbrev=False ) parser.add_argument( "-t", "--twoword", "--duo", "--two-word", "--two", help="foo" - ).map_long_option = {"two": "two-word"} + ) # throws error on --deux only! parser.add_argument( "-d", "--deuxmots", "--deux-mots", action="store_true", help="foo" - ).map_long_option = {"deux": "deux-mots"} + ) parser.add_argument("-s", action="store_true", help="single short") parser.add_argument("--abc", "-a", action="store_true", help="bar") parser.add_argument("--klm", "-k", "--kl-m", action="store_true", help="bar") @@ -221,7 +218,7 @@ class TestParser: ) parser.add_argument( "-x", "--exit-on-first", "--exitfirst", action="store_true", help="spam" - ).map_long_option = {"exitfirst": "exit-on-first"} + ) parser.add_argument("files_and_dirs", nargs="*") args = parser.parse_args(["-k", "--duo", "hallo", "--exitfirst"]) assert args.twoword == "hallo" @@ -236,32 +233,32 @@ class TestParser: args = parser.parse_args(["file", "dir"]) assert "|".join(args.files_and_dirs) == "file|dir" - def test_drop_short_0(self, parser): + def test_drop_short_0(self, parser: parseopt.Parser) -> None: parser.addoption("--funcarg", "--func-arg", action="store_true") parser.addoption("--abc-def", "--abc-def", action="store_true") parser.addoption("--klm-hij", action="store_true") with pytest.raises(UsageError): parser.parse(["--funcarg", "--k"]) - def test_drop_short_2(self, parser): + def test_drop_short_2(self, parser: parseopt.Parser) -> None: parser.addoption("--func-arg", "--doit", action="store_true") args = parser.parse(["--doit"]) assert args.func_arg is True - def test_drop_short_3(self, parser): + def test_drop_short_3(self, parser: parseopt.Parser) -> None: parser.addoption("--func-arg", "--funcarg", "--doit", action="store_true") args = parser.parse(["abcd"]) assert args.func_arg is False assert args.file_or_dir == ["abcd"] - def test_drop_short_help0(self, parser, capsys): + def test_drop_short_help0(self, parser: parseopt.Parser, capsys) -> None: parser.addoption("--func-args", "--doit", help="foo", action="store_true") parser.parse([]) help = parser.optparser.format_help() assert "--func-args, --doit foo" in help # testing would be more helpful with all help generated - def test_drop_short_help1(self, parser, capsys): + def test_drop_short_help1(self, parser: parseopt.Parser, capsys) -> None: group = parser.getgroup("general") group.addoption("--doit", "--func-args", action="store_true", help="foo") group._addoption( @@ -275,7 +272,7 @@ class TestParser: help = parser.optparser.format_help() assert "-doit, --func-args foo" in help - def test_multiple_metavar_help(self, parser): + def test_multiple_metavar_help(self, parser: parseopt.Parser) -> None: """ Help text for options with a metavar tuple should display help in the form "--preferences=value1 value2 value3" (#2004). @@ -290,7 +287,7 @@ class TestParser: assert "--preferences=value1 value2 value3" in help -def test_argcomplete(testdir, monkeypatch): +def test_argcomplete(testdir, monkeypatch) -> None: if not distutils.spawn.find_executable("bash"): pytest.skip("bash not available") script = str(testdir.tmpdir.join("test_argcomplete")) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 836b458c6..56d5a7625 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -122,7 +122,7 @@ class TestPytestPluginInteractions: def test_hook_proxy(self, testdir): """Test the gethookproxy function(#2016)""" config = testdir.parseconfig() - session = Session(config) + session = Session.from_config(config) testdir.makepyfile(**{"tests/conftest.py": "", "tests/subdir/conftest.py": ""}) conftest1 = testdir.tmpdir.join("tests/conftest.py") diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 758e999dc..3dab13b4b 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -530,7 +530,7 @@ def test_no_matching(function): ] else: assert obtained == [ - "nomatch: '{}'".format(good_pattern), + " nomatch: '{}'".format(good_pattern), " and: 'cachedir: .pytest_cache'", " and: 'collecting ... collected 1 item'", " and: ''", @@ -542,17 +542,23 @@ def test_no_matching(function): func(bad_pattern) # bad pattern does not match any line: passes -def test_pytester_addopts(request, monkeypatch): +def test_no_matching_after_match(): + lm = LineMatcher(["1", "2", "3"]) + lm.fnmatch_lines(["1", "3"]) + with pytest.raises(pytest.fail.Exception) as e: + lm.no_fnmatch_line("*") + assert str(e.value).splitlines() == ["fnmatch: '*'", " with: '1'"] + + +def test_pytester_addopts_before_testdir(request, monkeypatch): + orig = os.environ.get("PYTEST_ADDOPTS", None) monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused") - testdir = request.getfixturevalue("testdir") - - try: - assert "PYTEST_ADDOPTS" not in os.environ - finally: - testdir.finalize() - - assert os.environ["PYTEST_ADDOPTS"] == "--orig-unused" + assert "PYTEST_ADDOPTS" not in os.environ + testdir.finalize() + assert os.environ.get("PYTEST_ADDOPTS") == "--orig-unused" + monkeypatch.undo() + assert os.environ.get("PYTEST_ADDOPTS") == orig def test_run_stdin(testdir): @@ -632,14 +638,10 @@ def test_popen_default_stdin_stderr_and_stdin_None(testdir): def test_spawn_uses_tmphome(testdir): - import os - tmphome = str(testdir.tmpdir) + assert os.environ.get("HOME") == tmphome - # Does use HOME only during run. - assert os.environ.get("HOME") != tmphome - - testdir._env_run_update["CUSTOMENV"] = "42" + testdir.monkeypatch.setenv("CUSTOMENV", "42") p1 = testdir.makepyfile( """ diff --git a/testing/test_report_log.py b/testing/test_report_log.py deleted file mode 100644 index cc2a431ec..000000000 --- a/testing/test_report_log.py +++ /dev/null @@ -1,54 +0,0 @@ -import json - -import pytest -from _pytest.reports import BaseReport - - -def test_basics(testdir, tmp_path, pytestconfig): - """Basic testing of the report log functionality. - - We don't test the test reports extensively because they have been - tested already in ``test_reports``. - """ - testdir.makepyfile( - """ - def test_ok(): - pass - - def test_fail(): - assert 0 - """ - ) - - log_file = tmp_path / "log.json" - - result = testdir.runpytest("--report-log", str(log_file)) - assert result.ret == pytest.ExitCode.TESTS_FAILED - result.stdout.fnmatch_lines(["* generated report log file: {}*".format(log_file)]) - - json_objs = [json.loads(x) for x in log_file.read_text().splitlines()] - assert len(json_objs) == 10 - - # first line should be the session_start - session_start = json_objs[0] - assert session_start == { - "pytest_version": pytest.__version__, - "$report_type": "SessionStart", - } - - # last line should be the session_finish - session_start = json_objs[-1] - assert session_start == { - "exitstatus": pytest.ExitCode.TESTS_FAILED, - "$report_type": "SessionFinish", - } - - # rest of the json objects should be unserialized into report objects; we don't test - # the actual report object extensively because it has been tested in ``test_reports`` - # already. - pm = pytestconfig.pluginmanager - for json_obj in json_objs[1:-1]: - rep = pm.hook.pytest_report_from_serializable( - config=pytestconfig, data=json_obj - ) - assert isinstance(rep, BaseReport) diff --git a/testing/test_runner.py b/testing/test_runner.py index 86e9bddff..301e11898 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -900,9 +900,9 @@ def test_store_except_info_on_error(): # The next run should clear the exception info stored by the previous run ItemMightRaise.raise_error = False runner.pytest_runtest_call(ItemMightRaise()) - assert sys.last_type is None - assert sys.last_value is None - assert sys.last_traceback is None + assert not hasattr(sys, "last_type") + assert not hasattr(sys, "last_value") + assert not hasattr(sys, "last_traceback") def test_current_test_env_var(testdir, monkeypatch): diff --git a/testing/test_setupplan.py b/testing/test_setupplan.py index e323ba240..a44474dd1 100644 --- a/testing/test_setupplan.py +++ b/testing/test_setupplan.py @@ -17,3 +17,94 @@ def test_show_fixtures_and_test(testdir, dummy_yaml_custom_test): result.stdout.fnmatch_lines( ["*SETUP F arg*", "*test_arg (fixtures used: arg)", "*TEARDOWN F arg*"] ) + + +def test_show_multi_test_fixture_setup_and_teardown_correctly_simple(testdir): + """ + Verify that when a fixture lives for longer than a single test, --setup-plan + correctly displays the SETUP/TEARDOWN indicators the right number of times. + + As reported in https://github.com/pytest-dev/pytest/issues/2049 + --setup-plan was showing SETUP/TEARDOWN on every test, even when the fixture + should persist through multiple tests. + + (Note that this bug never affected actual test execution, which used the + correct fixture lifetimes. It was purely a display bug for --setup-plan, and + did not affect the related --setup-show or --setup-only.) + """ + testdir.makepyfile( + """ + import pytest + @pytest.fixture(scope = 'class') + def fix(): + return object() + class TestClass: + def test_one(self, fix): + assert False + def test_two(self, fix): + assert False + """ + ) + + result = testdir.runpytest("--setup-plan") + assert result.ret == 0 + + setup_fragment = "SETUP C fix" + setup_count = 0 + + teardown_fragment = "TEARDOWN C fix" + teardown_count = 0 + + for line in result.stdout.lines: + if setup_fragment in line: + setup_count += 1 + if teardown_fragment in line: + teardown_count += 1 + + # before the fix this tests, there would have been a setup/teardown + # message for each test, so the counts would each have been 2 + assert setup_count == 1 + assert teardown_count == 1 + + +def test_show_multi_test_fixture_setup_and_teardown_same_as_setup_show(testdir): + """ + Verify that SETUP/TEARDOWN messages match what comes out of --setup-show. + """ + testdir.makepyfile( + """ + import pytest + @pytest.fixture(scope = 'session') + def sess(): + return True + @pytest.fixture(scope = 'module') + def mod(): + return True + @pytest.fixture(scope = 'class') + def cls(): + return True + @pytest.fixture(scope = 'function') + def func(): + return True + def test_outside(sess, mod, cls, func): + assert True + class TestCls: + def test_one(self, sess, mod, cls, func): + assert True + def test_two(self, sess, mod, cls, func): + assert True + """ + ) + + plan_result = testdir.runpytest("--setup-plan") + show_result = testdir.runpytest("--setup-show") + + # the number and text of these lines should be identical + plan_lines = [ + l for l in plan_result.stdout.lines if "SETUP" in l or "TEARDOWN" in l + ] + show_lines = [ + l for l in show_result.stdout.lines if "SETUP" in l or "TEARDOWN" in l + ] + + assert plan_lines == show_lines diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 86f328a93..67714d030 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -115,7 +115,7 @@ class TestEvaluator: ) def test_skipif_class(self, testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ import pytest class TestClass(object): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 1bec577b8..fab13b07e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -154,6 +154,8 @@ class TestTerminal: "test2.py": "def test_2(): pass", } ) + # Explicitly test colored output. + testdir.monkeypatch.setenv("PY_COLORS", "1") child = testdir.spawn_pytest("-v test1.py test2.py") child.expect(r"collecting \.\.\.") @@ -963,7 +965,31 @@ class TestGenericReporting: ) result = testdir.runpytest("--maxfail=2", *option.args) result.stdout.fnmatch_lines( - ["*def test_1():*", "*def test_2():*", "*2 failed*"] + [ + "*def test_1():*", + "*def test_2():*", + "*! stopping after 2 failures !*", + "*2 failed*", + ] + ) + + def test_maxfailures_with_interrupted(self, testdir): + testdir.makepyfile( + """ + def test(request): + request.session.shouldstop = "session_interrupted" + assert 0 + """ + ) + result = testdir.runpytest("--maxfail=1", "-ra") + result.stdout.fnmatch_lines( + [ + "*= short test summary info =*", + "FAILED *", + "*! stopping after 1 failures !*", + "*! session_interrupted !*", + "*= 1 failed in*", + ] ) def test_tb_option(self, testdir, option): diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 29b6db947..eb1c1f300 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -258,7 +258,7 @@ class TestNumberedDir: registry = [] register_cleanup_lock_removal(lock, register=registry.append) - cleanup_func, = registry + (cleanup_func,) = registry assert lock.is_file() diff --git a/testing/test_unittest.py b/testing/test_unittest.py index b34f54313..4b814532b 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -383,7 +383,7 @@ def test_testcase_custom_exception_info(testdir, type): def test_testcase_totally_incompatible_exception_info(testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ from unittest import TestCase class MyTestCase(TestCase): diff --git a/testing/test_warnings.py b/testing/test_warnings.py index c4af14dac..8a9cc618f 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -1,3 +1,4 @@ +import os import warnings import pytest @@ -641,3 +642,160 @@ def test_pytest_configure_warning(testdir, recwarn): assert "INTERNALERROR" not in result.stderr.str() warning = recwarn.pop() assert str(warning.message) == "from pytest_configure" + + +class TestStackLevel: + @pytest.fixture + def capwarn(self, testdir): + class CapturedWarnings: + captured = [] + + @classmethod + def pytest_warning_captured(cls, warning_message, when, item, location): + cls.captured.append((warning_message, location)) + + testdir.plugins = [CapturedWarnings()] + + return CapturedWarnings + + def test_issue4445_rewrite(self, testdir, capwarn): + """#4445: Make sure the warning points to a reasonable location + See origin of _issue_warning_captured at: _pytest.assertion.rewrite.py:241 + """ + testdir.makepyfile(some_mod="") + conftest = testdir.makeconftest( + """ + import some_mod + import pytest + + pytest.register_assert_rewrite("some_mod") + """ + ) + testdir.parseconfig() + + # with stacklevel=5 the warning originates from register_assert_rewrite + # function in the created conftest.py + assert len(capwarn.captured) == 1 + warning, location = capwarn.captured.pop() + file, lineno, func = location + + assert "Module already imported" in str(warning.message) + assert file == str(conftest) + assert func == "" # the above conftest.py + assert lineno == 4 + + def test_issue4445_preparse(self, testdir, capwarn): + """#4445: Make sure the warning points to a reasonable location + See origin of _issue_warning_captured at: _pytest.config.__init__.py:910 + """ + testdir.makeconftest( + """ + import nothing + """ + ) + testdir.parseconfig("--help") + + # with stacklevel=2 the warning should originate from config._preparse and is + # thrown by an errorneous conftest.py + assert len(capwarn.captured) == 1 + warning, location = capwarn.captured.pop() + file, _, func = location + + assert "could not load initial conftests" in str(warning.message) + assert "config{sep}__init__.py".format(sep=os.sep) in file + assert func == "_preparse" + + def test_issue4445_import_plugin(self, testdir, capwarn): + """#4445: Make sure the warning points to a reasonable location + See origin of _issue_warning_captured at: _pytest.config.__init__.py:585 + """ + testdir.makepyfile( + some_plugin=""" + import pytest + pytest.skip("thing", allow_module_level=True) + """ + ) + testdir.syspathinsert() + testdir.parseconfig("-p", "some_plugin") + + # with stacklevel=2 the warning should originate from + # config.PytestPluginManager.import_plugin is thrown by a skipped plugin + + # During config parsing the the pluginargs are checked in a while loop + # that as a result of the argument count runs import_plugin twice, hence + # two identical warnings are captured (is this intentional?). + assert len(capwarn.captured) == 2 + warning, location = capwarn.captured.pop() + file, _, func = location + + assert "skipped plugin 'some_plugin': thing" in str(warning.message) + assert "config{sep}__init__.py".format(sep=os.sep) in file + assert func == "import_plugin" + + def test_issue4445_resultlog(self, testdir, capwarn): + """#4445: Make sure the warning points to a reasonable location + See origin of _issue_warning_captured at: _pytest.resultlog.py:35 + """ + testdir.makepyfile( + """ + def test_dummy(): + pass + """ + ) + # Use parseconfigure() because the warning in resultlog.py is triggered in + # the pytest_configure hook + testdir.parseconfigure( + "--result-log={dir}".format(dir=testdir.tmpdir.join("result.log")) + ) + + # with stacklevel=2 the warning originates from resultlog.pytest_configure + # and is thrown when --result-log is used + warning, location = capwarn.captured.pop() + file, _, func = location + + assert "--result-log is deprecated" in str(warning.message) + assert "resultlog.py" in file + assert func == "pytest_configure" + + def test_issue4445_cacheprovider_set(self, testdir, capwarn): + """#4445: Make sure the warning points to a reasonable location + See origin of _issue_warning_captured at: _pytest.cacheprovider.py:59 + """ + testdir.tmpdir.join(".pytest_cache").write("something wrong") + testdir.runpytest(plugins=[capwarn()]) + + # with stacklevel=3 the warning originates from one stacklevel above + # _issue_warning_captured in cacheprovider.Cache.set and is thrown + # when there are errors during cache folder creation + + # set is called twice (in module stepwise and in cacheprovider) so emits + # two warnings when there are errors during cache folder creation. (is this intentional?) + assert len(capwarn.captured) == 2 + warning, location = capwarn.captured.pop() + file, lineno, func = location + + assert "could not create cache path" in str(warning.message) + assert "cacheprovider.py" in file + assert func == "set" + + def test_issue4445_issue5928_mark_generator(self, testdir): + """#4445 and #5928: Make sure the warning from an unknown mark points to + the test file where this mark is used. + """ + testfile = testdir.makepyfile( + """ + import pytest + + @pytest.mark.unknown + def test_it(): + pass + """ + ) + result = testdir.runpytest_subprocess() + # with stacklevel=2 the warning should originate from the above created test file + result.stdout.fnmatch_lines_random( + [ + "*{testfile}:3*".format(testfile=str(testfile)), + "*Unknown pytest.mark.unknown*", + ] + ) diff --git a/tox.ini b/tox.ini index 6bdc5d73f..afb5cb36b 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ envlist = doctesting py37-freeze docs + docs-checklinks [testenv] commands = @@ -63,6 +64,14 @@ deps = -r{toxinidir}/doc/en/requirements.txt commands = sphinx-build -W -b html . _build +[testenv:docs-checklinks] +basepython = python3 +usedevelop = True +changedir = doc/en +deps = -r{toxinidir}/doc/en/requirements.txt +commands = + sphinx-build -W -q --keep-going -b linkcheck . _build + [testenv:doctesting] basepython = python3 skipsdist = True @@ -138,6 +147,7 @@ xfail_strict=true filterwarnings = error default:Using or importing the ABCs:DeprecationWarning:unittest2.* + default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.* ignore:Module already imported so cannot be rewritten:pytest.PytestWarning # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8). ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest))