Imported Upstream version 6.1.0 upstream/6.1.0
authorDongHun Kwak <dh0128.kwak@samsung.com>
Thu, 18 Mar 2021 02:33:41 +0000 (11:33 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Thu, 18 Mar 2021 02:33:41 +0000 (11:33 +0900)
204 files changed:
.coveragerc
.github/ISSUE_TEMPLATE.md [deleted file]
.github/ISSUE_TEMPLATE/1_bug_report.md [new file with mode: 0644]
.github/ISSUE_TEMPLATE/2_feature_request.md [new file with mode: 0644]
.github/ISSUE_TEMPLATE/config.yml [new file with mode: 0644]
.github/workflows/main.yml
.pre-commit-config.yaml
.readthedocs.yml [new file with mode: 0644]
AUTHORS
CONTRIBUTING.rst
README.rst
RELEASING.rst
doc/en/adopt.rst
doc/en/announce/index.rst
doc/en/announce/release-2.3.0.rst
doc/en/announce/release-2.5.1.rst
doc/en/announce/release-2.5.2.rst
doc/en/announce/release-2.6.0.rst
doc/en/announce/release-2.6.1.rst
doc/en/announce/release-2.6.2.rst
doc/en/announce/release-2.6.3.rst
doc/en/announce/release-2.7.0.rst
doc/en/announce/release-2.7.1.rst
doc/en/announce/release-2.7.2.rst
doc/en/announce/release-2.8.2.rst
doc/en/announce/release-2.8.3.rst
doc/en/announce/release-2.8.4.rst
doc/en/announce/release-2.8.5.rst
doc/en/announce/release-2.8.6.rst
doc/en/announce/release-2.8.7.rst
doc/en/announce/release-2.9.0.rst
doc/en/announce/release-2.9.1.rst
doc/en/announce/release-2.9.2.rst
doc/en/announce/release-3.0.0.rst
doc/en/announce/release-3.1.0.rst
doc/en/announce/release-3.10.0.rst
doc/en/announce/release-3.2.0.rst
doc/en/announce/release-3.3.0.rst
doc/en/announce/release-3.4.0.rst
doc/en/announce/release-3.5.0.rst
doc/en/announce/release-3.6.0.rst
doc/en/announce/release-3.7.0.rst
doc/en/announce/release-3.8.0.rst
doc/en/announce/release-3.9.0.rst
doc/en/announce/release-4.0.0.rst
doc/en/announce/release-4.1.0.rst
doc/en/announce/release-4.2.0.rst
doc/en/announce/release-4.3.0.rst
doc/en/announce/release-4.4.0.rst
doc/en/announce/release-4.5.0.rst
doc/en/announce/release-4.6.0.rst
doc/en/announce/release-5.0.0.rst
doc/en/announce/release-5.1.0.rst
doc/en/announce/release-5.2.0.rst
doc/en/announce/release-5.3.0.rst
doc/en/announce/release-5.4.0.rst
doc/en/announce/release-6.1.0.rst [new file with mode: 0644]
doc/en/builtin.rst
doc/en/cache.rst
doc/en/changelog.rst
doc/en/contents.rst
doc/en/customize.rst
doc/en/deprecations.rst
doc/en/example/markers.rst
doc/en/example/nonpython.rst
doc/en/example/nonpython/conftest.py
doc/en/example/pythoncollection.rst
doc/en/faq.rst [deleted file]
doc/en/fixture.rst
doc/en/funcarg_compare.rst
doc/en/getting-started.rst
doc/en/historical-notes.rst
doc/en/index.rst
doc/en/mark.rst
doc/en/monkeypatch.rst
doc/en/reference.rst
doc/en/talks.rst
doc/en/usage.rst
doc/en/warnings.rst
doc/en/writing_plugins.rst
doc/en/xunit_setup.rst
extra/get_issues.py
pyproject.toml
scripts/release-on-comment.py
scripts/release.py
setup.cfg
setup.py
src/_pytest/_argcomplete.py
src/_pytest/_code/__init__.py
src/_pytest/_code/code.py
src/_pytest/_code/source.py
src/_pytest/_io/saferepr.py
src/_pytest/_io/terminalwriter.py
src/_pytest/assertion/__init__.py
src/_pytest/assertion/rewrite.py
src/_pytest/assertion/truncate.py
src/_pytest/assertion/util.py
src/_pytest/cacheprovider.py
src/_pytest/capture.py
src/_pytest/compat.py
src/_pytest/config/__init__.py
src/_pytest/config/argparsing.py
src/_pytest/config/exceptions.py
src/_pytest/config/findpaths.py
src/_pytest/debugging.py
src/_pytest/deprecated.py
src/_pytest/doctest.py
src/_pytest/faulthandler.py
src/_pytest/fixtures.py
src/_pytest/freeze_support.py
src/_pytest/helpconfig.py
src/_pytest/hookspec.py
src/_pytest/junitxml.py
src/_pytest/logging.py
src/_pytest/main.py
src/_pytest/mark/__init__.py
src/_pytest/mark/expression.py
src/_pytest/mark/structures.py
src/_pytest/monkeypatch.py
src/_pytest/nodes.py
src/_pytest/nose.py
src/_pytest/outcomes.py
src/_pytest/pastebin.py
src/_pytest/pathlib.py
src/_pytest/pytester.py
src/_pytest/python.py
src/_pytest/python_api.py
src/_pytest/recwarn.py
src/_pytest/reports.py
src/_pytest/resultlog.py [deleted file]
src/_pytest/runner.py
src/_pytest/setuponly.py
src/_pytest/setupplan.py
src/_pytest/skipping.py
src/_pytest/store.py
src/_pytest/terminal.py
src/_pytest/timing.py
src/_pytest/tmpdir.py
src/_pytest/unittest.py
src/_pytest/warning_types.py
src/_pytest/warnings.py
src/pytest/__init__.py
src/pytest/__main__.py
src/pytest/collect.py
testing/acceptance_test.py
testing/code/test_excinfo.py
testing/code/test_source.py
testing/deprecated_test.py
testing/example_scripts/unittest/test_unittest_plain_async.py [new file with mode: 0644]
testing/freeze/create_executable.py
testing/logging/test_fixture.py
testing/logging/test_reporting.py
testing/plugins_integration/.gitignore [new file with mode: 0644]
testing/plugins_integration/README.rst [new file with mode: 0644]
testing/plugins_integration/bdd_wallet.feature [new file with mode: 0644]
testing/plugins_integration/bdd_wallet.py [new file with mode: 0644]
testing/plugins_integration/django_settings.py [new file with mode: 0644]
testing/plugins_integration/pytest.ini [new file with mode: 0644]
testing/plugins_integration/pytest_anyio_integration.py [new file with mode: 0644]
testing/plugins_integration/pytest_asyncio_integration.py [new file with mode: 0644]
testing/plugins_integration/pytest_mock_integration.py [new file with mode: 0644]
testing/plugins_integration/pytest_trio_integration.py [new file with mode: 0644]
testing/plugins_integration/pytest_twisted_integration.py [new file with mode: 0644]
testing/plugins_integration/simple_integration.py [new file with mode: 0644]
testing/python/approx.py
testing/python/collect.py
testing/python/fixtures.py
testing/python/integration.py
testing/python/metafunc.py
testing/python/raises.py
testing/test_argcomplete.py
testing/test_assertion.py
testing/test_assertrewrite.py
testing/test_cacheprovider.py
testing/test_capture.py
testing/test_collection.py
testing/test_config.py
testing/test_conftest.py
testing/test_doctest.py
testing/test_faulthandler.py
testing/test_findpaths.py
testing/test_helpconfig.py
testing/test_junitxml.py
testing/test_link_resolve.py
testing/test_main.py
testing/test_mark.py
testing/test_meta.py
testing/test_monkeypatch.py
testing/test_pastebin.py
testing/test_pathlib.py
testing/test_pluginmanager.py
testing/test_pytester.py
testing/test_reports.py
testing/test_resultlog.py [deleted file]
testing/test_runner.py
testing/test_runner_xunit.py
testing/test_setuponly.py
testing/test_setupplan.py
testing/test_skipping.py
testing/test_terminal.py
testing/test_tmpdir.py
testing/test_unittest.py
testing/test_warnings.py
tox.ini

index b06629a8a8fcd6dc275f239408193d06b2b61fed..09ab37643377185e9e6ecd1b080ddd2bfc0c26ee 100644 (file)
@@ -27,3 +27,4 @@ exclude_lines =
     ^\s*assert False(,|$)
 
     ^\s*if TYPE_CHECKING:
+    ^\s*@overload( |$)
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
deleted file mode 100644 (file)
index fb81416..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<!--
-Thanks for submitting an issue!
-
-Here's a quick checklist for what to provide:
--->
-
-- [ ] a detailed description of the bug or suggestion
-- [ ] output of `pip list` from the virtual environment you are using
-- [ ] pytest and operating system versions
-- [ ] minimal example if possible
diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.md b/.github/ISSUE_TEMPLATE/1_bug_report.md
new file mode 100644 (file)
index 0000000..0fc3e06
--- /dev/null
@@ -0,0 +1,16 @@
+---
+name: 🐛 Bug Report
+about: Report errors and problems
+
+---
+
+<!--
+Thanks for submitting an issue!
+
+Quick check-list while reporting bugs:
+-->
+
+- [ ] a detailed description of the bug or problem you are having
+- [ ] output of `pip list` from the virtual environment you are using
+- [ ] pytest and operating system versions
+- [ ] minimal example if possible
diff --git a/.github/ISSUE_TEMPLATE/2_feature_request.md b/.github/ISSUE_TEMPLATE/2_feature_request.md
new file mode 100644 (file)
index 0000000..54912b0
--- /dev/null
@@ -0,0 +1,5 @@
+---
+name: 🚀 Feature Request
+about: Ideas for new features and improvements
+
+---
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644 (file)
index 0000000..742d2e4
--- /dev/null
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+  - name: ❓ Support Question
+    url: https://github.com/pytest-dev/pytest/discussions
+    about: Use GitHub's new Discussions feature for questions
index 056c8d3dbd9bc7c39736c8ed3abb73351ef3c247..9ef1a08b99347c96065d674f3382155c460712da 100644 (file)
@@ -41,6 +41,7 @@ jobs:
 
           "docs",
           "doctesting",
+          "plugins",
         ]
 
         include:
@@ -111,6 +112,11 @@ jobs:
             tox_env: "py38-xdist"
             use_coverage: true
 
+          - name: "plugins"
+            python: "3.7"
+            os: ubuntu-latest
+            tox_env: "plugins"
+
           - name: "docs"
             python: "3.7"
             os: ubuntu-latest
@@ -131,7 +137,7 @@ jobs:
       with:
         python-version: ${{ matrix.python }}
     - name: Set up Python ${{ matrix.python }} (deadsnakes)
-      uses: deadsnakes/action@v1.0.0
+      uses: deadsnakes/action@v2.0.0
       if: matrix.python == '3.9-dev'
       with:
         python-version: ${{ matrix.python }}
index dc371720417e64e04849b96a41d4f2587da474a8..6068a2d324d8da7743d878273145f6355dc6f5df 100644 (file)
@@ -25,7 +25,9 @@ repos:
     hooks:
     -   id: flake8
         language_version: python3
-        additional_dependencies: [flake8-typing-imports==1.9.0]
+        additional_dependencies:
+          - flake8-typing-imports==1.9.0
+          - flake8-docstrings==1.5.0
 -   repo: https://github.com/asottile/reorder_python_imports
     rev: v2.3.0
     hooks:
@@ -70,9 +72,11 @@ repos:
                 _code\.|
                 builtin\.|
                 code\.|
-                io\.(BytesIO|saferepr|TerminalWriter)|
+                io\.|
                 path\.local\.sysfind|
                 process\.|
-                std\.
+                std\.|
+                error\.|
+                xml\.
             )
         types: [python]
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644 (file)
index 0000000..0176c26
--- /dev/null
@@ -0,0 +1,12 @@
+version: 2
+
+python:
+   version: 3.7
+   install:
+      - requirements: doc/en/requirements.txt
+      - method: pip
+        path: .
+
+formats:
+  - epub
+  - pdf
diff --git a/AUTHORS b/AUTHORS
index b28e5613389aa6ba932b0083a2fa0f496f1cdd5e..c8dfec4010a2523b9ed0933f32db8acd758de98d 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -151,6 +151,7 @@ Joshua Bronson
 Jurko Gospodnetić
 Justyna Janczyszyn
 Kale Kundert
+Kamran Ahmad
 Karl O. Pinc
 Katarzyna Jachim
 Katarzyna Król
index 0523c0ece7768c185ae11ee0baabaaec7bcaab33..48ba147b7db8a9dddc482958a547caf5e401679f 100644 (file)
@@ -89,6 +89,38 @@ without using a local copy.  This can be convenient for small fixes.
     The built documentation should be available in ``doc/en/_build/html``,
     where 'en' refers to the documentation language.
 
+Pytest has an API reference which in large part is
+`generated automatically <https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html>`_
+from the docstrings of the documented items. Pytest uses the
+`Sphinx docstring format <https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html>`_.
+For example:
+
+.. code-block:: python
+
+    def my_function(arg: ArgType) -> Foo:
+        """Do important stuff.
+
+        More detailed info here, in separate paragraphs from the subject line.
+        Use proper sentences -- start sentences with capital letters and end
+        with periods.
+
+        Can include annotated documentation:
+
+        :param short_arg: An argument which determines stuff.
+        :param long_arg:
+            A long explanation which spans multiple lines, overflows
+            like this.
+        :returns: The result.
+        :raises ValueError:
+            Detailed information when this can happen.
+
+        .. versionadded:: 6.0
+
+        Including types into the annotations above is not necessary when
+        type-hinting is being used (as in this example).
+        """
+
+
 .. _submitplugin:
 
 Submitting Plugins to pytest-dev
@@ -99,8 +131,6 @@ in repositories living under the ``pytest-dev`` organisations:
 
 - `pytest-dev on GitHub <https://github.com/pytest-dev>`_
 
-- `pytest-dev on Bitbucket <https://bitbucket.org/pytest-dev>`_
-
 All pytest-dev Contributors team members have write access to all contained
 repositories.  Pytest core and plugins are generally developed
 using `pull requests`_ to respective repositories.
@@ -116,16 +146,17 @@ You can submit your plugin by subscribing to the `pytest-dev mail list
 mail pointing to your existing pytest plugin repository which must have
 the following:
 
-- PyPI presence with a ``setup.py`` that contains a license, ``pytest-``
+- PyPI presence with packaging metadata that contains a ``pytest-``
   prefixed name, version number, authors, short and long description.
 
-- a ``tox.ini`` for running tests using `tox <https://tox.readthedocs.io>`_.
+- a  `tox configuration <https://tox.readthedocs.io/en/latest/config.html#configuration-discovery>`_
+  for running tests using `tox <https://tox.readthedocs.io>`_.
 
-- a ``README.txt`` describing how to use the plugin and on which
+- a ``README`` describing how to use the plugin and on which
   platforms it runs.
 
-- a ``LICENSE.txt`` file or equivalent containing the licensing
-  information, with matching info in ``setup.py``.
+- a ``LICENSE`` file containing the licensing information, with
+  matching info in its packaging metadata.
 
 - an issue tracker for bug reports and enhancement requests.
 
@@ -375,6 +406,27 @@ actual latest release). The procedure for this is:
    * Delete the PR body, it usually contains a duplicate commit message.
 
 
+Who does the backporting
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+As mentioned above, bugs should first be fixed on ``master`` (except in rare occasions
+that a bug only happens in a previous release). So who should do the backport procedure described
+above?
+
+1. If the bug was fixed by a core developer, it is the main responsibility of that core developer
+   to do the backport.
+2. However, often the merge is done by another maintainer, in which case it is nice of them to
+   do the backport procedure if they have the time.
+3. For bugs submitted by non-maintainers, it is expected that a core developer will to do
+   the backport, normally the one that merged the PR on ``master``.
+4. If a non-maintainers notices a bug which is fixed on ``master`` but has not been backported
+   (due to maintainers forgetting to apply the *needs backport* label, or just plain missing it),
+   they are also welcome to open a PR with the backport. The procedure is simple and really
+   helps with the maintenance of the project.
+
+All the above are not rules, but merely some guidelines/suggestions on what we should expect
+about backports.
+
 Handling stale issues/PRs
 -------------------------
 
index 00c85ae37ce9e32cca3768c6a3cf34392f295195..057278a926b02ff2be3c182d1398efa552f3d558 100644 (file)
@@ -22,8 +22,8 @@
 .. image:: https://travis-ci.org/pytest-dev/pytest.svg?branch=master
     :target: https://travis-ci.org/pytest-dev/pytest
 
-.. image:: https://dev.azure.com/pytest-dev/pytest/_apis/build/status/pytest-CI?branchName=master
-    :target: https://dev.azure.com/pytest-dev/pytest
+.. image:: https://github.com/pytest-dev/pytest/workflows/main/badge.svg
+    :target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Amain
 
 .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
     :target: https://github.com/psf/black
@@ -77,21 +77,21 @@ Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` stat
 Features
 --------
 
-- Detailed info on failing `assert statements <https://docs.pytest.org/en/stable/assert.html>`_ (no need to remember ``self.assert*`` names);
+- Detailed info on failing `assert statements <https://docs.pytest.org/en/stable/assert.html>`_ (no need to remember ``self.assert*`` names)
 
 - `Auto-discovery
   <https://docs.pytest.org/en/stable/goodpractices.html#python-test-discovery>`_
-  of test modules and functions;
+  of test modules and functions
 
 - `Modular fixtures <https://docs.pytest.org/en/stable/fixture.html>`_ for
-  managing small or parametrized long-lived test resources;
+  managing small or parametrized long-lived test resources
 
 - Can run `unittest <https://docs.pytest.org/en/stable/unittest.html>`_ (or trial),
-  `nose <https://docs.pytest.org/en/stable/nose.html>`_ test suites out of the box;
+  `nose <https://docs.pytest.org/en/stable/nose.html>`_ test suites out of the box
 
-- Python 3.5+ and PyPy3;
+- Python 3.5+ and PyPy3
 
-- Rich plugin architecture, with over 850+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community;
+- Rich plugin architecture, with over 850+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community
 
 
 Documentation
index f5e2528e3f24d96ff7a2e26e91b3d6e1f5a62b4a..9ff95be92edd7e1a6beb9d65db528c49bfe9822d 100644 (file)
@@ -5,37 +5,85 @@ Our current policy for releasing is to aim for a bug-fix release every few weeks
 is to get fixes and new features out instead of trying to cram a ton of features into a release and by consequence
 taking a lot of time to make a new one.
 
+The git commands assume the following remotes are setup:
+
+* ``origin``: your own fork of the repository.
+* ``upstream``: the ``pytest-dev/pytest`` official repository.
+
 Preparing: Automatic Method
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 We have developed an automated workflow for releases, that uses GitHub workflows and is triggered
-by opening an issue or issuing a comment one.
+by opening an issue.
+
+Bug-fix releases
+^^^^^^^^^^^^^^^^
+
+A bug-fix release is always done from a maintenance branch, so for example to release bug-fix
+``5.1.2``, open a new issue and add this comment to the body::
+
+    @pytestbot please prepare release from 5.1.x
+
+Where ``5.1.x`` is the maintenance branch for the ``5.1`` series.
+
+The automated workflow will publish a PR for a branch ``release-5.1.2``
+and notify it as a comment in the issue.
+
+Minor releases
+^^^^^^^^^^^^^^
+
+1. Create a new maintenance branch from ``master``::
+
+        git fetch --all
+        git branch 5.2.x upstream/master
+        git push upstream 5.2.x
 
-The comment must be in the form::
+2. Open a new issue and add this comment to the body::
 
-    @pytestbot please prepare release from BRANCH
+    @pytestbot please prepare release from 5.2.x
 
-Where ``BRANCH`` is ``master`` or one of the maintenance branches.
+The automated workflow will publish a PR for a branch ``release-5.2.0`` and
+notify it as a comment in the issue.
 
-For major releases the comment must be in the form::
+Major and release candidates
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-    @pytestbot please prepare major release from master
+1. Create a new maintenance branch from ``master``::
 
-After that, the workflow should publish a PR and notify that it has done so as a comment
-in the original issue.
+        git fetch --all
+        git branch 6.0.x upstream/master
+        git push upstream 6.0.x
+
+2. For a **major release**, open a new issue and add this comment in the body::
+
+        @pytestbot please prepare major release from 6.0.x
+
+   For a **release candidate**, the comment must be (TODO: `#7551 <https://github.com/pytest-dev/pytest/issues/7551>`__)::
+
+        @pytestbot please prepare release candidate from 6.0.x
+
+The automated workflow will publish a PR for a branch ``release-6.0.0`` and
+notify it as a comment in the issue.
+
+At this point on, this follows the same workflow as other maintenance branches: bug-fixes are merged
+into ``master`` and ported back to the maintenance branch, even for release candidates.
+
+**A note about release candidates**
+
+During release candidates we can merge small improvements into
+the maintenance branch before releasing the final major version, however we must take care
+to avoid introducing big changes at this stage.
 
 Preparing: Manual Method
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. important::
-
-    pytest releases must be prepared on **Linux** because the docs and examples expect
-    to be executed on that platform.
+**Important**: pytest releases must be prepared on **Linux** because the docs and examples expect
+to be executed on that platform.
 
 To release a version ``MAJOR.MINOR.PATCH``, follow these steps:
 
-#. For major and minor releases, create a new branch ``MAJOR.MINOR.x`` from the
-   latest ``master`` and push it to the ``pytest-dev/pytest`` repo.
+#. For major and minor releases, create a new branch ``MAJOR.MINOR.x`` from
+   ``upstream/master`` and push it to ``upstream``.
 
 #. Create a branch ``release-MAJOR.MINOR.PATCH`` from the ``MAJOR.MINOR.x`` branch.
 
@@ -56,9 +104,10 @@ Releasing
 Both automatic and manual processes described above follow the same steps from this point onward.
 
 #. After all tests pass and the PR has been approved, tag the release commit
-   in the ``MAJOR.MINOR.x`` branch and push it. This will publish to PyPI::
+   in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
 
-     git tag MAJOR.MINOR.PATCH
+     git fetch --all
+     git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
      git push git@github.com:pytest-dev/pytest.git MAJOR.MINOR.PATCH
 
    Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
@@ -69,9 +118,9 @@ Both automatic and manual processes described above follow the same steps from t
 
        git fetch --all --prune
        git checkout origin/master -b cherry-pick-release
-       git cherry-pick --no-commit -m1 origin/MAJOR.MINOR.x
-       git checkout origin/master -- changelog
-       git commit  # no arguments
+       git cherry-pick -x -m1 upstream/MAJOR.MINOR.x
+
+#. Open a PR for ``cherry-pick-release`` and merge it once CI passes. No need to wait for approvals if there were no conflicts on the previous step.
 
 #. Send an email announcement with the contents from::
 
index e3c0477bc0e236257b3e65aede4d39be5f5eaaea..82e2111ed3b3a7a00698edbef4132efdfc1e1cf8 100644 (file)
@@ -45,7 +45,7 @@ Partner projects, sign up here! (by 22 March)
 What does it mean to "adopt pytest"?
 -----------------------------------------
 
-There can be many different definitions of "success". Pytest can run many `nose and unittest`_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
+There can be many different definitions of "success". Pytest can run many nose_ and unittest_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
 
 Progressive success might look like:
 
@@ -63,7 +63,8 @@ Progressive success might look like:
 
 It may be after the month is up, the partner project decides that pytest is not right for it. That's okay - hopefully the pytest team will also learn something about its weaknesses or deficiencies.
 
-.. _`nose and unittest`: faq.html#how-does-pytest-relate-to-nose-and-unittest
+.. _nose: nose.html
+.. _unittest: unittest.html
 .. _assert: assert.html
 .. _pycmd: https://bitbucket.org/hpk42/pycmd/overview
 .. _`setUp/tearDown methods`: xunit_setup.html
index e011aaa8160ffa7ad89db119a91ef603b39f7189..753e81156ab2c9bc7852342ec0c0883aa2f6042f 100644 (file)
@@ -6,6 +6,7 @@ Release announcements
    :maxdepth: 2
 
 
+   release-6.1.0
    release-6.0.2
    release-6.0.1
    release-6.0.0
index d938192bb0db4a520137802d768abf585f9805a7..bdd92a98fde3439445844f75e38d339b1ca6dec7 100644 (file)
@@ -94,7 +94,7 @@ Changes between 2.2.4 and 2.3.0
 - pluginmanager.register(...) now raises ValueError if the
   plugin has been already registered or the name is taken
 
-- fix issue159: improve http://pytest.org/en/stable/faq.html
+- fix issue159: improve https://docs.pytest.org/en/6.0.1/faq.html
   especially with respect to the "magic" history, also mention
   pytest-django, trial and unittest integration.
 
index 22e69a836b945b36a7a37b886f7d7dcef4df44f1..ff39db2d52dcae5193bb8a9057e19168a1259bf0 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.5.1: fixes and new home page styling
 ===========================================================================
 
-pytest is a mature Python testing tool with more than 1000 tests
+pytest is a mature Python testing tool with more than 1000 tests
 against itself, passing on many different interpreters and platforms.
 
 The 2.5.1 release maintains the "zero-reported-bugs" promise by fixing
index c389f5f5403876b013aa050bae6586b2922beab9..edc4da6e19fe84df9038e7a98d67e5a7c69a1511 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.5.2: fixes
 ===========================================================================
 
-pytest is a mature Python testing tool with more than 1000 tests
+pytest is a mature Python testing tool with more than 1000 tests
 against itself, passing on many different interpreters and platforms.
 
 The 2.5.2 release fixes a few bugs with two maybe-bugs remaining and
index 36b545a28b4b750597c66b295c8bcdf768a39708..56fbd6cc1e4d9f4dfb6cfb4e30b6159a62a4231a 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.6.0: shorter tracebacks, new warning system, test runner compat
 ===========================================================================
 
-pytest is a mature Python testing tool with more than 1000 tests
+pytest is a mature Python testing tool with more than 1000 tests
 against itself, passing on many different interpreters and platforms.
 
 The 2.6.0 release should be drop-in backward compatible to 2.5.2 and
index 85d9861643a6086b54e09cb32c2ccba2cba2aea5..7469c488e5f6e531ba2ce656d2d4928c33da8520 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.6.1: fixes and new xfail feature
 ===========================================================================
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 The 2.6.1 release is drop-in compatible to 2.5.2 and actually fixes some
 regressions introduced with 2.6.0.  It also brings a little feature
index f6ce178a1070697b3893a9240925ff499d7a5603..9c3b7d96b072dd9430281d0b038bca62a4d41c7d 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.6.2: few fixes and cx_freeze support
 ===========================================================================
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 This release is drop-in compatible to 2.5.2 and 2.6.X.  It also
 brings support for including pytest with cx_freeze or similar
index 7353dfee71cd94cfda0ad06a9bc61116a9c62032..56973a2b2f7fafd55f03889323e02020a9cba1fe 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.6.3: fixes and little improvements
 ===========================================================================
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 This release is drop-in compatible to 2.5.2 and 2.6.X.
 See below for the changes and see docs at:
index 2f6d50d8b69abe227c688932bb4c19968de3e3d7..2840178a07f7785c8e249e96440edcd00107b533 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.7.0: fixes, features, speed improvements
 ===========================================================================
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 This release is supposed to be drop-in compatible to 2.6.X.
 
index fdc71eebba95f331194e5f4f19261253b52fa925..5110c085e01d934d5752f46e2a378da9b379783a 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.7.1: bug fixes
 =======================
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 This release is supposed to be drop-in compatible to 2.7.0.
 
index 1e3950de4d0caabe885a752172753f4648e4addb..93e5b64eeed03d84a5dd01f1451e41a9bc987de7 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.7.2: bug fixes
 =======================
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 This release is supposed to be drop-in compatible to 2.7.1.
 
index d702861614225dcffffc63b342f722876fc4cef8..e47263388528e057d574bd6778fd1bec75acf0c4 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.8.2: bug fixes
 =======================
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 This release is supposed to be drop-in compatible to 2.8.1.
 
index b131a7e1f14de1be933445e714ab3c8ac5ca35c9..3f357252bb619cbc49a69403cf48929c51e9392a 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.8.3: bug fixes
 =======================
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 This release is supposed to be drop-in compatible to 2.8.2.
 
index a09629cef0993cd475f9c773a9b3287423f1896c..adbdecc87ea08d80ec7195c62371c145156a6048 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.8.4
 ============
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 This release is supposed to be drop-in compatible to 2.8.2.
 
index 7409022a1371e820c4f7aa8930f0bf58d2085546..c5343d1ea7275177c9cdc9be2fafcb650cdec1ab 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.8.5
 ============
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 This release is supposed to be drop-in compatible to 2.8.4.
 
index 215fae51eac06bf537c8037d1311b005ea4e9c10..5d6565b16a39e6f30ea93be52fdd5629a010e0b4 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.8.6
 ============
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 This release is supposed to be drop-in compatible to 2.8.5.
 
index 9005f56363a31367ebb3776d982a87939af7ffc5..8236a096669731a65ebf5129efcb1aadfa115827 100644 (file)
@@ -4,7 +4,7 @@ pytest-2.8.7
 This is a hotfix release to solve a regression
 in the builtin monkeypatch plugin that got introduced in 2.8.6.
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 This release is supposed to be drop-in compatible to 2.8.5.
 
index f5d4be713476efb67251d4b292d1217ae8120a04..8c2ee05f9bf30ecb126036234076a055cc1be2eb 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.9.0
 ============
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 
 See below for the changes and see docs at:
index c71f385163823f483e84f986607fed9de053cc22..47bc2e6d38ba7512ee1a4eb1ff3fa593b98c0524 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.9.1
 ============
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 
 See below for the changes and see docs at:
index b007a6d99e8c4fd4a69c40ab51df99b2430956db..ffd8dc58ed50a8d748e1ea4353751f53694f7a11 100644 (file)
@@ -1,7 +1,7 @@
 pytest-2.9.2
 ============
 
-pytest is a mature Python testing tool with more than 1100 tests
+pytest is a mature Python testing tool with more than 1100 tests
 against itself, passing on many different interpreters and platforms.
 
 See below for the changes and see docs at:
index ca3e9e327632a0a31238cd1dff1e4b20af8d390f..5de3891148281c53d5b9889945bf2034a7d3fbfe 100644 (file)
@@ -3,7 +3,7 @@ pytest-3.0.0
 
 The pytest team is proud to announce the 3.0.0 release!
 
-pytest is a mature Python testing tool with more than 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a lot of bugs fixes and improvements, and much of
index b84fd4c3cf9dfce050bda7bfcaa4a1596a35d52a..55277067948b32731762de2fc89509e4d9aa0de6 100644 (file)
@@ -3,7 +3,7 @@ pytest-3.1.0
 
 The pytest team is proud to announce the 3.1.0 release!
 
-pytest is a mature Python testing tool with more than 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a bugs fixes and improvements, so users are encouraged
index c16c381e8d0230c9b65b05be6b6b796f4c083245..ff3c000b0e790c1dfa19bd2e6cf0b0a36fa5a20a 100644 (file)
@@ -3,7 +3,7 @@ pytest-3.10.0
 
 The pytest team is proud to announce the 3.10.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 4d5d6f1671f60f38642fb6a93e1af729a5fee752..edc66a28e789d004b125a22edd1aa9e241e8febf 100644 (file)
@@ -3,7 +3,7 @@ pytest-3.2.0
 
 The pytest team is proud to announce the 3.2.0 release!
 
-pytest is a mature Python testing tool with more than 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index e57bbac6a91d4be26cd882d77d609b42d3ea5fe3..1cbf2c448c8e7a4bcc28b6f94064ee3be96ec169 100644 (file)
@@ -3,7 +3,7 @@ pytest-3.3.0
 
 The pytest team is proud to announce the 3.3.0 release!
 
-pytest is a mature Python testing tool with more than 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index ec6725370cdb64d50fbd0e0e5a2127c342fa0471..6ab5b124a25eca9588f2e21ed7c25acdcd5dade1 100644 (file)
@@ -3,7 +3,7 @@ pytest-3.4.0
 
 The pytest team is proud to announce the 3.4.0 release!
 
-pytest is a mature Python testing tool with more than 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index ef64dc381e6efc60c79258e8fb610a67339b1553..6bc2f3cd0cb7f18d366faa4c735fbb5f138204e2 100644 (file)
@@ -3,7 +3,7 @@ pytest-3.5.0
 
 The pytest team is proud to announce the 3.5.0 release!
 
-pytest is a mature Python testing tool with more than 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 38a8b9e3f09298d52aa73479b88c51ffad29a0c2..44b178c169fdc8baeb5330753b91718e978366ed 100644 (file)
@@ -3,7 +3,7 @@ pytest-3.6.0
 
 The pytest team is proud to announce the 3.6.0 release!
 
-pytest is a mature Python testing tool with more than 1600 tests
+pytest is a mature Python testing tool with more than 1600 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index ef6c44f0aab2eaff7d3b37549f1e86f8a598dde2..89908a9101c99b3be31c965f8b601888b2731a67 100644 (file)
@@ -3,7 +3,7 @@ pytest-3.7.0
 
 The pytest team is proud to announce the 3.7.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 5369ffc7dba731b345506d7c8d94b2f920733477..8c35a44f6d5a8e5ce71cb0a5e5cc2f4dbaaf8a5c 100644 (file)
@@ -3,7 +3,7 @@ pytest-3.8.0
 
 The pytest team is proud to announce the 3.8.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 1d889e5bc85c85a5db11bf91e89282072a4b3429..0be6cf5be8a24e7904ba04359df936ce87f06d3c 100644 (file)
@@ -3,7 +3,7 @@ pytest-3.9.0
 
 The pytest team is proud to announce the 3.9.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index b91fd59542de16f837149ace67a0480a77908bb6..5eb0107758a4dcfe31bce8b8d96aa86b92f01681 100644 (file)
@@ -3,7 +3,7 @@ pytest-4.0.0
 
 The pytest team is proud to announce the 4.0.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 77aaf74af65247dd6afa70a24fc7e863baca3152..314564eeb6f70e71e8377dad465c1fb335b30845 100644 (file)
@@ -3,7 +3,7 @@ pytest-4.1.0
 
 The pytest team is proud to announce the 4.1.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 11520acf3315ae53521b3a2aa11790dff75ebc1a..bcd7f7754793ec7f838ddf30ca30194eba77d1e3 100644 (file)
@@ -3,7 +3,7 @@ pytest-4.2.0
 
 The pytest team is proud to announce the 4.2.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 9dcbe415eecdf586a3d0232e7698117f071ee558..3b0b4280922df0c0a0236792b1f88b28c76222be 100644 (file)
@@ -3,7 +3,7 @@ pytest-4.3.0
 
 The pytest team is proud to announce the 4.3.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index d4abfac22a7e1c008da2676c4a03d7bb7786da44..dc89739d0aa49f233db462c57d31febf41807d1a 100644 (file)
@@ -3,7 +3,7 @@ pytest-4.4.0
 
 The pytest team is proud to announce the 4.4.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 957e2c172859d4e7f5ebf2508aa5d5ea5651059c..d2a05d4f795219aef6616246224fef3804572d47 100644 (file)
@@ -3,7 +3,7 @@ pytest-4.5.0
 
 The pytest team is proud to announce the 4.5.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 7c7cf29d26ae8bc1fa26f5bc0bcb38a3763628ac..a82fdd47d6f5b135a9e3f1d3966e7fe5a005731b 100644 (file)
@@ -3,7 +3,7 @@ pytest-4.6.0
 
 The pytest team is proud to announce the 4.6.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 99c92a505fe0f1887bd52cf92474ceebf0070c93..f5e593e9d8885ce4ae78ae621b70e9b942a4db4f 100644 (file)
@@ -3,7 +3,7 @@ pytest-5.0.0
 
 The pytest team is proud to announce the 5.0.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 4293c581312094c97e2dc5a4e0b363a52bc908ea..9ab54ff97304b8ff6a52e2f6e28d97c7a517a550 100644 (file)
@@ -3,7 +3,7 @@ pytest-5.1.0
 
 The pytest team is proud to announce the 5.1.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index fbe27031b1e569a5f749fe1ca163225a6b3f06ba..f43767b75066c0dc55a3a75b53d3ff19ccc33a96 100644 (file)
@@ -3,7 +3,7 @@ pytest-5.2.0
 
 The pytest team is proud to announce the 5.2.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index 54c33e192b087a29d2b2195ee02fd6c56ce50ea5..e13a71f09aaf21f6cff99ccf80305914d7e5bd67 100644 (file)
@@ -3,7 +3,7 @@ pytest-5.3.0
 
 The pytest team is proud to announce the 5.3.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bugs fixes and improvements, so users are encouraged
index cb91e26ba36f9f9fb855253b1b1b0a3dd4f5b623..43dffc9290e66c5b214df39a5b1d722588678d59 100644 (file)
@@ -3,7 +3,7 @@ pytest-5.4.0
 
 The pytest team is proud to announce the 5.4.0 release!
 
-pytest is a mature Python testing tool with more than 2000 tests
+pytest is a mature Python testing tool with more than 2000 tests
 against itself, passing on many different interpreters and platforms.
 
 This release contains a number of bug fixes and improvements, so users are encouraged
diff --git a/doc/en/announce/release-6.1.0.rst b/doc/en/announce/release-6.1.0.rst
new file mode 100644 (file)
index 0000000..f4b571a
--- /dev/null
@@ -0,0 +1,44 @@
+pytest-6.1.0
+=======================================
+
+The pytest team is proud to announce the 6.1.0 release!
+
+This release contains new features, improvements, bug fixes, and breaking changes, so users
+are encouraged to take a look at the CHANGELOG carefully:
+
+    https://docs.pytest.org/en/stable/changelog.html
+
+For complete documentation, please visit:
+
+    https://docs.pytest.org/en/stable/
+
+As usual, you can upgrade from PyPI via:
+
+    pip install -U pytest
+
+Thanks to all of the contributors to this release:
+
+* Anthony Sottile
+* Bruno Oliveira
+* C. Titus Brown
+* Drew Devereux
+* Faris A Chugthai
+* Florian Bruhin
+* Hugo van Kemenade
+* Hynek Schlawack
+* Joseph Lucas
+* Kamran Ahmad
+* Mattreex
+* Maximilian Cosmo Sitter
+* Ran Benita
+* Rüdiger Busche
+* Sam Estep
+* Sorin Sbarnea
+* Thomas Grainger
+* Vipul Kumar
+* Yutaro Ikeda
+* hp310780
+
+
+Happy testing,
+The pytest Development Team
index b33ee041dd6dd0cbfce5352294c51ff3f26409be..0fd58164c7615f331244ab4089a5bedeeb194b55 100644 (file)
@@ -23,7 +23,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
         cache.get(key, default)
         cache.set(key, value)
 
-        Keys must be a ``/`` separated value, where the first part is usually the
+        Keys must be ``/`` separated strings, where the first part is usually the
         name of your plugin or application to avoid clashes with other cache users.
 
         Values can be any object handled by the json stdlib module.
@@ -57,7 +57,8 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
         ``out`` and ``err`` will be ``byte`` objects.
 
     doctest_namespace [session scope]
-        Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests.
+        Fixture that returns a :py:class:`dict` that will be injected into the
+        namespace of doctests.
 
     pytestconfig [session scope]
         Session-scoped fixture that returns the :class:`_pytest.config.Config` object.
@@ -89,8 +90,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
         automatically XML-encoded.
 
     record_testsuite_property [session scope]
-        Records a new ``<property>`` tag as child of the root ``<testsuite>``. This is suitable to
-        writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family.
+        Record a new ``<property>`` tag as child of the root ``<testsuite>``.
+
+        This is suitable to writing global information regarding the entire test
+        suite, and is compatible with ``xunit2`` JUnit family.
 
         This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
 
@@ -102,6 +105,12 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
 
         ``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
 
+        .. warning::
+
+            Currently this fixture **does not work** with the
+            `pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See issue
+            `#7767 <https://github.com/pytest-dev/pytest/issues/7767>`__ for details.
+
     caplog
         Access and control log capturing.
 
@@ -114,8 +123,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
         * caplog.clear()         -> clear captured records and formatted log output string
 
     monkeypatch
-        The returned ``monkeypatch`` fixture provides these
-        helper methods to modify objects, dictionaries or os.environ::
+        A convenient fixture for monkey-patching.
+
+        The fixture provides these methods to modify objects, dictionaries or
+        os.environ::
 
             monkeypatch.setattr(obj, name, value, raising=True)
             monkeypatch.delattr(obj, name, raising=True)
@@ -126,10 +137,9 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
             monkeypatch.syspath_prepend(path)
             monkeypatch.chdir(path)
 
-        All modifications will be undone after the requesting
-        test function or fixture has finished. The ``raising``
-        parameter determines if a KeyError or AttributeError
-        will be raised if the set/deletion operation has no target.
+        All modifications will be undone after the requesting test function or
+        fixture has finished. The ``raising`` parameter determines if a KeyError
+        or AttributeError will be raised if the set/deletion operation has no target.
 
     recwarn
         Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
@@ -140,30 +150,28 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
     tmpdir_factory [session scope]
         Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
 
-
     tmp_path_factory [session scope]
         Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
 
-
     tmpdir
-        Return a temporary directory path object
-        which is unique to each test function invocation,
-        created as a sub directory of the base temporary
-        directory.  The returned object is a `py.path.local`_
-        path object.
+        Return a temporary directory path object which is unique to each test
+        function invocation, created as a sub directory of the base temporary
+        directory.
+
+        The returned object is a `py.path.local`_ path object.
 
         .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
 
     tmp_path
-        Return a temporary directory path object
-        which is unique to each test function invocation,
-        created as a sub directory of the base temporary
-        directory.  The returned object is a :class:`pathlib.Path`
-        object.
+        Return a temporary directory path object which is unique to each test
+        function invocation, created as a sub directory of the base temporary
+        directory.
+
+        The returned object is a :class:`pathlib.Path` object.
 
         .. note::
 
-            in python < 3.6 this is a pathlib2.Path
+            In python < 3.6 this is a pathlib2.Path.
 
 
     no tests ran in 0.12s
index 076d4b1973cefce599b9657da5f7ac4ace22e283..42ca473545da7e4b41d2021f0f8b854f621c0d55 100644 (file)
@@ -264,7 +264,7 @@ the cache and nothing will be printed:
     FAILED test_caching.py::test_function - assert 42 == 23
     1 failed in 0.12s
 
-See the :fixture:`config.cache fixture <config.cache>` for more details.
+See the :fixture:`config.cache fixture <cache>` for more details.
 
 
 Inspecting Cache content
index feb113e2135ed216368dbde1418cea2e66411e93..c620d271ff9b990d294a811e723715a0b98c14f4 100644 (file)
@@ -28,6 +28,166 @@ with advance notice in the **Deprecations** section of releases.
 
 .. towncrier release notes start
 
+pytest 6.1.0 (2020-09-26)
+=========================
+
+Breaking Changes
+----------------
+
+- `#5585 <https://github.com/pytest-dev/pytest/issues/5585>`_: As per our policy, the following features which have been deprecated in the 5.X series are now
+  removed:
+
+  * The ``funcargnames`` read-only property of ``FixtureRequest``, ``Metafunc``, and ``Function`` classes. Use ``fixturenames`` attribute.
+
+  * ``@pytest.fixture`` no longer supports positional arguments, pass all arguments by keyword instead.
+
+  * Direct construction of ``Node`` subclasses now raise an error, use ``from_parent`` instead.
+
+  * The default value for ``junit_family`` has changed to ``xunit2``. If you require the old format, add ``junit_family=xunit1`` to your configuration file.
+
+  * The ``TerminalReporter`` no longer has a ``writer`` attribute. Plugin authors may use the public functions of the ``TerminalReporter`` instead of accessing the ``TerminalWriter`` object directly.
+
+  * The ``--result-log`` option has been removed. Users are recommended to use the `pytest-reportlog <https://github.com/pytest-dev/pytest-reportlog>`__ plugin instead.
+
+
+  For more information consult
+  `Deprecations and Removals <https://docs.pytest.org/en/stable/deprecations.html>`__ in the docs.
+
+
+
+Deprecations
+------------
+
+- `#6981 <https://github.com/pytest-dev/pytest/issues/6981>`_: The ``pytest.collect`` module is deprecated: all its names can be imported from ``pytest`` directly.
+
+
+- `#7097 <https://github.com/pytest-dev/pytest/issues/7097>`_: The ``pytest._fillfuncargs`` function is deprecated. This function was kept
+  for backward compatibility with an older plugin.
+
+  It's functionality is not meant to be used directly, but if you must replace
+  it, use `function._request._fillfixtures()` instead, though note this is not
+  a public API and may break in the future.
+
+
+- `#7210 <https://github.com/pytest-dev/pytest/issues/7210>`_: The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'``
+  instead.
+
+  The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue
+  if you use this and want a replacement.
+
+
+- `#7255 <https://github.com/pytest-dev/pytest/issues/7255>`_: The :func:`pytest_warning_captured <_pytest.hookspec.pytest_warning_captured>` hook is deprecated in favor
+  of :func:`pytest_warning_recorded <_pytest.hookspec.pytest_warning_recorded>`, and will be removed in a future version.
+
+
+- `#7648 <https://github.com/pytest-dev/pytest/issues/7648>`_: The ``gethookproxy()`` and ``isinitpath()`` methods of ``FSCollector`` and ``Package`` are deprecated;
+  use ``self.session.gethookproxy()`` and ``self.session.isinitpath()`` instead.
+  This should work on all pytest versions.
+
+
+
+Features
+--------
+
+- `#7667 <https://github.com/pytest-dev/pytest/issues/7667>`_: New ``--durations-min`` command-line flag controls the minimal duration for inclusion in the slowest list of tests shown by ``--durations``. Previously this was hard-coded to ``0.005s``.
+
+
+
+Improvements
+------------
+
+- `#6681 <https://github.com/pytest-dev/pytest/issues/6681>`_: Internal pytest warnings issued during the early stages of initialization are now properly handled and can filtered through :confval:`filterwarnings` or ``--pythonwarnings/-W``.
+
+  This also fixes a number of long standing issues: `#2891 <https://github.com/pytest-dev/pytest/issues/2891>`__, `#7620 <https://github.com/pytest-dev/pytest/issues/7620>`__, `#7426 <https://github.com/pytest-dev/pytest/issues/7426>`__.
+
+
+- `#7572 <https://github.com/pytest-dev/pytest/issues/7572>`_: When a plugin listed in ``required_plugins`` is missing or an unknown config key is used with ``--strict-config``, a simple error message is now shown instead of a stacktrace.
+
+
+- `#7685 <https://github.com/pytest-dev/pytest/issues/7685>`_: Added two new attributes :attr:`rootpath <_pytest.config.Config.rootpath>` and :attr:`inipath <_pytest.config.Config.inipath>` to :class:`Config <_pytest.config.Config>`.
+  These attributes are :class:`pathlib.Path` versions of the existing :attr:`rootdir <_pytest.config.Config.rootdir>` and :attr:`inifile <_pytest.config.Config.inifile>` attributes,
+  and should be preferred over them when possible.
+
+
+- `#7780 <https://github.com/pytest-dev/pytest/issues/7780>`_: Public classes which are not designed to be inherited from are now marked `@final <https://docs.python.org/3/library/typing.html#typing.final>`_.
+  Code which inherits from these classes will trigger a type-checking (e.g. mypy) error, but will still work in runtime.
+  Currently the ``final`` designation does not appear in the API Reference but hopefully will in the future.
+
+
+
+Bug Fixes
+---------
+
+- `#1953 <https://github.com/pytest-dev/pytest/issues/1953>`_: Fixed error when overwriting a parametrized fixture, while also reusing the super fixture value.
+
+  .. code-block:: python
+
+      # conftest.py
+      import pytest
+
+
+      @pytest.fixture(params=[1, 2])
+      def foo(request):
+          return request.param
+
+
+      # test_foo.py
+      import pytest
+
+
+      @pytest.fixture
+      def foo(foo):
+          return foo * 2
+
+
+- `#4984 <https://github.com/pytest-dev/pytest/issues/4984>`_: Fixed an internal error crash with ``IndexError: list index out of range`` when
+  collecting a module which starts with a decorated function, the decorator
+  raises, and assertion rewriting is enabled.
+
+
+- `#7591 <https://github.com/pytest-dev/pytest/issues/7591>`_: pylint shouldn't complain anymore about unimplemented abstract methods when inheriting from :ref:`File <non-python tests>`.
+
+
+- `#7628 <https://github.com/pytest-dev/pytest/issues/7628>`_: Fixed test collection when a full path without a drive letter was passed to pytest on Windows (for example ``\projects\tests\test.py`` instead of ``c:\projects\tests\pytest.py``).
+
+
+- `#7638 <https://github.com/pytest-dev/pytest/issues/7638>`_: Fix handling of command-line options that appear as paths but trigger an OS-level syntax error on Windows, such as the options used internally by ``pytest-xdist``.
+
+
+- `#7742 <https://github.com/pytest-dev/pytest/issues/7742>`_: Fixed INTERNALERROR when accessing locals / globals with faulty ``exec``.
+
+
+
+Improved Documentation
+----------------------
+
+- `#1477 <https://github.com/pytest-dev/pytest/issues/1477>`_: Removed faq.rst and its reference in contents.rst.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- `#7536 <https://github.com/pytest-dev/pytest/issues/7536>`_: The internal ``junitxml`` plugin has rewritten to use ``xml.etree.ElementTree``.
+  The order of attributes in XML elements might differ. Some unneeded escaping is
+  no longer performed.
+
+
+- `#7587 <https://github.com/pytest-dev/pytest/issues/7587>`_: The dependency on the ``more-itertools`` package has been removed.
+
+
+- `#7631 <https://github.com/pytest-dev/pytest/issues/7631>`_: The result type of :meth:`capfd.readouterr() <_pytest.capture.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple,
+  but should behave like one in all respects. This was done for technical reasons.
+
+
+- `#7671 <https://github.com/pytest-dev/pytest/issues/7671>`_: When collecting tests, pytest finds test classes and functions by examining the
+  attributes of python objects (modules, classes and instances). To speed up this
+  process, pytest now ignores builtin attributes (like ``__class__``,
+  ``__delattr__`` and ``__new__``) without consulting the :confval:`python_classes` and
+  :confval:`python_functions` configuration options and without passing them to plugins
+  using the :func:`pytest_pycollect_makeitem <_pytest.hookspec.pytest_pycollect_makeitem>` hook.
+
+
 pytest 6.0.2 (2020-09-04)
 =========================
 
@@ -7422,7 +7582,7 @@ Bug fixes:
 - pluginmanager.register(...) now raises ValueError if the
   plugin has been already registered or the name is taken
 
-- fix issue159: improve http://pytest.org/en/stable/faq.html
+- fix issue159: improve https://docs.pytest.org/en/6.0.1/faq.html
   especially with respect to the "magic" history, also mention
   pytest-django, trial and unittest integration.
 
index c623d0602abadbf93c721d4076cd2bbadfbd9cc6..58a08744ced2b5a7901effab281e47a5095fb5b5 100644 (file)
@@ -38,7 +38,6 @@ Full pytest documentation
    customize
    example/index
    bash-completion
-   faq
 
    backwards-compatibility
    deprecations
index e1f1b253bc9700c5f4646c60b20b5fe05e88fc60..9f7c365dc4583fb854ff1a8871072d4f1dfa9a45 100644 (file)
@@ -180,10 +180,15 @@ are never merged - the first match wins.
 The internal :class:`Config <_pytest.config.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
 will subsequently carry these attributes:
 
-- ``config.rootdir``: the determined root directory, guaranteed to exist.
+- :attr:`config.rootpath <_pytest.config.Config.rootpath>`: the determined root directory, guaranteed to exist.
 
-- ``config.inifile``: the determined ``configfile``, may be ``None`` (it is named ``inifile``
-  for historical reasons).
+- :attr:`config.inipath <_pytest.config.Config.inipath>`: the determined ``configfile``, may be ``None``
+  (it is named ``inipath`` for historical reasons).
+
+.. versionadded:: 6.1
+    The ``config.rootpath`` and ``config.inipath`` properties. They are :class:`pathlib.Path`
+    versions of the older ``config.rootdir`` and ``config.inifile``, which have type
+    ``py.path.local``, and still exist for backward compatibility.
 
 The ``rootdir`` is used as a reference directory for constructing test
 addresses ("nodeids") and can be used also by plugins for storing
index 3334b5d5fe458fef070197d6e86456677399cb37..14d1eeb98af7bc3351a164a635b984311e0e5b26 100644 (file)
@@ -16,8 +16,7 @@ Deprecated Features
 -------------------
 
 Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
-:class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using
-:ref:`standard warning filters <warnings>`.
+:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
 
 
 The ``pytest_warning_captured`` hook
@@ -30,11 +29,19 @@ This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``
 Use the ``pytest_warning_recored`` hook instead, which replaces the ``item`` parameter
 by a ``nodeid`` parameter.
 
+The ``pytest.collect`` module
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 6.0
+
+The ``pytest.collect`` module is no longer part of the public API, all its names
+should now be imported from ``pytest`` directly instead.
+
 
 The ``pytest._fillfuncargs`` function
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. deprecated:: 5.5
+.. deprecated:: 6.0
 
 This function was kept for backward compatibility with an older plugin.
 
@@ -43,6 +50,11 @@ it, use `function._request._fillfixtures()` instead, though note this is not
 a public API and may break in the future.
 
 
+Removed Features
+----------------
+
+As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
+an appropriate period of deprecation has passed.
 
 ``--no-print-logs`` command-line option
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -59,38 +71,46 @@ display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log``
 
 
 
-Node Construction changed to ``Node.from_parent``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Result log (``--result-log``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. deprecated:: 5.4
+.. deprecated:: 4.0
+.. versionremoved:: 6.0
 
-The construction of nodes now should use the named constructor ``from_parent``.
-This limitation in api surface intends to enable better/simpler refactoring of the collection tree.
+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.
 
-This means that instead of :code:`MyItem(name="foo", parent=collector, obj=42)`
-one now has to invoke :code:`MyItem.from_parent(collector, name="foo")`.
+The  `pytest-reportlog <https://github.com/pytest-dev/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.
 
-Plugins that wish to support older versions of pytest and suppress the warning can use
-`hasattr` to check if `from_parent` exists in that version:
+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.
 
-.. code-block:: python
+``pytest_collect_directory`` hook
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-    def pytest_pycollect_makeitem(collector, name, obj):
-        if hasattr(MyItem, "from_parent"):
-            item = MyItem.from_parent(collector, name="foo")
-            item.obj = 42
-            return item
-        else:
-            return MyItem(name="foo", parent=collector, obj=42)
+.. versionremoved:: 6.0
 
-Note that ``from_parent`` should only be called with keyword arguments for the parameters.
+The ``pytest_collect_directory`` has not worked properly for years (it was called
+but the results were ignored). Users may consider using :func:`pytest_collection_modifyitems <_pytest.hookspec.pytest_collection_modifyitems>` instead.
 
+TerminalReporter.writer
+~~~~~~~~~~~~~~~~~~~~~~~
 
+.. versionremoved:: 6.0
+
+The ``TerminalReporter.writer`` attribute has been deprecated and should no longer be used. This
+was inadvertently exposed as part of the public API of that plugin and ties it too much
+with ``py.io.TerminalWriter``.
+
+Plugins that used ``TerminalReporter.writer`` directly should instead use ``TerminalReporter``
+methods that provide the same functionality.
 
 ``junit_family`` default value change to "xunit2"
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. deprecated:: 5.2
+.. versionchanged:: 6.0
 
 The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, which
 is an update of the old ``xunit1`` format and is supported by default in modern tools
@@ -126,55 +146,52 @@ Services known to support the ``xunit2`` format:
 * `Jenkins <https://www.jenkins.io/>`__ with the `JUnit <https://plugins.jenkins.io/junit>`__ plugin.
 * `Azure Pipelines <https://azure.microsoft.com/en-us/services/devops/pipelines>`__.
 
+Node Construction changed to ``Node.from_parent``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-``funcargnames`` alias for ``fixturenames``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. deprecated:: 5.0
-
-The ``FixtureRequest``, ``Metafunc``, and ``Function`` classes track the names of
-their associated fixtures, with the aptly-named ``fixturenames`` attribute.
+.. versionchanged:: 6.0
 
-Prior to pytest 2.3, this attribute was named ``funcargnames``, and we have kept
-that as an alias since.  It is finally due for removal, as it is often confusing
-in places where we or plugin authors must distinguish between fixture names and
-names supplied by non-fixture things such as ``pytest.mark.parametrize``.
+The construction of nodes now should use the named constructor ``from_parent``.
+This limitation in api surface intends to enable better/simpler refactoring of the collection tree.
 
+This means that instead of :code:`MyItem(name="foo", parent=collector, obj=42)`
+one now has to invoke :code:`MyItem.from_parent(collector, name="foo")`.
 
-Result log (``--result-log``)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Plugins that wish to support older versions of pytest and suppress the warning can use
+`hasattr` to check if `from_parent` exists in that version:
 
-.. deprecated:: 4.0
+.. code-block:: python
 
-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.
+    def pytest_pycollect_makeitem(collector, name, obj):
+        if hasattr(MyItem, "from_parent"):
+            item = MyItem.from_parent(collector, name="foo")
+            item.obj = 42
+            return item
+        else:
+            return MyItem(name="foo", parent=collector, obj=42)
 
-The  `pytest-reportlog <https://github.com/pytest-dev/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.
+Note that ``from_parent`` should only be called with keyword arguments for the parameters.
 
-The plan is remove the ``--result-log`` option in pytest 6.0 if ``pytest-reportlog`` proves satisfactory
-to all users and is deemed stable. The ``pytest-reportlog`` plugin might even be merged into the core
-at some point, depending on the plans for the plugins and number of users using it.
 
-TerminalReporter.writer
-~~~~~~~~~~~~~~~~~~~~~~~
+``pytest.fixture`` arguments are keyword only
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. deprecated:: 5.4
+.. versionremoved:: 6.0
 
-The ``TerminalReporter.writer`` attribute has been deprecated and should no longer be used. This
-was inadvertently exposed as part of the public API of that plugin and ties it too much
-with ``py.io.TerminalWriter``.
+Passing arguments to pytest.fixture() as positional arguments has been removed - pass them by keyword instead.
 
-Plugins that used ``TerminalReporter.writer`` directly should instead use ``TerminalReporter``
-methods that provide the same functionality.
+``funcargnames`` alias for ``fixturenames``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
+.. versionremoved:: 6.0
 
-Removed Features
-----------------
+The ``FixtureRequest``, ``Metafunc``, and ``Function`` classes track the names of
+their associated fixtures, with the aptly-named ``fixturenames`` attribute.
 
-As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
-an appropriate period of deprecation has passed.
+Prior to pytest 2.3, this attribute was named ``funcargnames``, and we have kept
+that as an alias since.  It is finally due for removal, as it is often confusing
+in places where we or plugin authors must distinguish between fixture names and
+names supplied by non-fixture things such as ``pytest.mark.parametrize``.
 
 
 ``pytest.config`` global
@@ -358,7 +375,7 @@ Metafunc.addcall
 
 .. versionremoved:: 4.0
 
-:meth:`_pytest.python.Metafunc.addcall` was a precursor to the current parametrized mechanism. Users should use
+``_pytest.python.Metafunc.addcall`` was a precursor to the current parametrized mechanism. Users should use
 :meth:`_pytest.python.Metafunc.parametrize` instead.
 
 Example:
@@ -593,7 +610,7 @@ This has been documented as deprecated for years, but only now we are actually e
 
 .. versionremoved:: 4.0
 
-As part of a large :ref:`marker-revamp`, :meth:`_pytest.nodes.Node.get_marker` is deprecated. See
+As part of a large :ref:`marker-revamp`, ``_pytest.nodes.Node.get_marker`` is removed. See
 :ref:`the documentation <update marker code>` on tips on how to update your code.
 
 
index 38610ee3a60cb15c10f0db39e9303c76cd441967..3d55f9ebb044c2d68f84c436534cf6d7703260b9 100644 (file)
@@ -201,6 +201,11 @@ Or to select "http" and "quick" tests:
 You can use ``and``, ``or``, ``not`` and parentheses.
 
 
+In addition to the test's name, ``-k`` also matches the names of the test's parents (usually, the name of the file and class it's in),
+attributes set on the test function, markers applied to it or its parents and any :attr:`extra keywords <_pytest.nodes.Node.extra_keyword_matches>`
+explicitly added to it or its parents.
+
+
 Registering markers
 -------------------------------------
 
@@ -280,7 +285,7 @@ its test methods:
 This is equivalent to directly applying the decorator to the
 two test functions.
 
-To apply marks at the module level, use the :globalvar:`pytestmark` global variable:
+To apply marks at the module level, use the :globalvar:`pytestmark` global variable::
 
     import pytest
     pytestmark = pytest.mark.webtest
index d15b7ae8bddc76496ce6f7512634224b995637eb..464a6c6cedeff51263f2b144ce0a5d9f7890c032 100644 (file)
@@ -12,7 +12,7 @@ A basic example for specifying tests in Yaml files
 .. _`pytest-yamlwsgi`: http://bitbucket.org/aafshar/pytest-yamlwsgi/src/tip/pytest_yamlwsgi.py
 .. _`PyYAML`: https://pypi.org/project/PyYAML/
 
-Here is an example ``conftest.py`` (extracted from Ali Afshnars special purpose `pytest-yamlwsgi`_ plugin).   This ``conftest.py`` will  collect ``test*.yaml`` files and will execute the yaml-formatted content as custom tests:
+Here is an example ``conftest.py`` (extracted from Ali Afshar's special purpose `pytest-yamlwsgi`_ plugin).   This ``conftest.py`` will  collect ``test*.yaml`` files and will execute the yaml-formatted content as custom tests:
 
 .. include:: nonpython/conftest.py
     :literal:
index d30ab3841dcd868639871f6d972676510f80d938..6e5a570929035d0c159831231048b8ae4b2af898 100644 (file)
@@ -9,7 +9,8 @@ def pytest_collect_file(parent, path):
 
 class YamlFile(pytest.File):
     def collect(self):
-        import yaml  # we need a yaml parser, e.g. PyYAML
+        # We need a yaml parser, e.g. PyYAML.
+        import yaml
 
         raw = yaml.safe_load(self.fspath.open())
         for name, spec in sorted(raw.items()):
@@ -23,12 +24,12 @@ class YamlItem(pytest.Item):
 
     def runtest(self):
         for name, value in sorted(self.spec.items()):
-            # some custom test execution (dumb example follows)
+            # Some custom test execution (dumb example follows).
             if name != value:
                 raise YamlException(self, name, value)
 
     def repr_failure(self, excinfo):
-        """ called when self.runtest() raises an exception. """
+        """Called when self.runtest() raises an exception."""
         if isinstance(excinfo.value, YamlException):
             return "\n".join(
                 [
@@ -43,4 +44,4 @@ class YamlItem(pytest.Item):
 
 
 class YamlException(Exception):
-    """ custom exception for error reporting. """
+    """Custom exception for error reporting."""
index a12e2deaa77b76d6d35b4e7f7ac6b545d4550254..c2f0348395cc883e33a7cd6828a7c12d62d5f93b 100644 (file)
@@ -313,3 +313,12 @@ interpreter:
     collect_ignore = ["setup.py"]
     if sys.version_info[0] > 2:
         collect_ignore_glob = ["*_py2.py"]
+
+Since Pytest 2.6, users can prevent pytest from discovering classes that start
+with ``Test`` by setting a boolean ``__test__`` attribute to ``False``.
+
+.. code-block:: python
+
+    # Will not be discovered as a test
+    class TestClass:
+        __test__ = False
diff --git a/doc/en/faq.rst b/doc/en/faq.rst
deleted file mode 100644 (file)
index c281deb..0000000
+++ /dev/null
@@ -1,158 +0,0 @@
-Some Issues and Questions
-==================================
-
-.. note::
-
-    This FAQ is here only mostly for historic reasons.  Checkout
-    `pytest Q&A at Stackoverflow <http://stackoverflow.com/search?q=pytest>`_
-    for many questions and answers related to pytest and/or use
-    :ref:`contact channels` to get help.
-
-On naming, nosetests, licensing and magic
-------------------------------------------------
-
-How does pytest relate to nose and unittest?
-+++++++++++++++++++++++++++++++++++++++++++++++++
-
-``pytest`` and nose_ share basic philosophy when it comes
-to running and writing Python tests.  In fact, you can run many tests
-written for nose with ``pytest``.  nose_ was originally created
-as a clone of ``pytest`` when ``pytest`` was in the ``0.8`` release
-cycle.  Note that starting with pytest-2.0 support for running unittest
-test suites is majorly improved.
-
-how does pytest relate to twisted's trial?
-++++++++++++++++++++++++++++++++++++++++++++++
-
-Since some time ``pytest`` has builtin support for supporting tests
-written using trial. It does not itself start a reactor, however,
-and does not handle Deferreds returned from a test in pytest style.
-If you are using trial's unittest.TestCase chances are that you can
-just run your tests even if you return Deferreds.  In addition,
-there also is a dedicated `pytest-twisted
-<https://pypi.org/project/pytest-twisted/>`_ plugin which allows you to
-return deferreds from pytest-style tests, allowing the use of
-:ref:`fixtures <fixtures>` and other features.
-
-how does pytest work with Django?
-++++++++++++++++++++++++++++++++++++++++++++++
-
-In 2012, some work is going into the `pytest-django plugin <https://pypi.org/project/pytest-django/>`_.  It substitutes the usage of Django's
-``manage.py test`` and allows the use of all pytest features_ most of which
-are not available from Django directly.
-
-.. _features: features.html
-
-
-What's this "magic" with pytest? (historic notes)
-++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-Around 2007 (version ``0.8``) some people thought that ``pytest``
-was using too much "magic".  It had been part of the `pylib`_ which
-contains a lot of unrelated python library code.  Around 2010 there
-was a major cleanup refactoring, which removed unused or deprecated code
-and resulted in the new ``pytest`` PyPI package which strictly contains
-only test-related code.  This release also brought a complete pluginification
-such that the core is around 300 lines of code and everything else is
-implemented in plugins.  Thus ``pytest`` today is a small, universally runnable
-and customizable testing framework for Python.   Note, however, that
-``pytest`` uses metaprogramming techniques and reading its source is
-thus likely not something for Python beginners.
-
-A second "magic" issue was the assert statement debugging feature.
-Nowadays, ``pytest`` explicitly rewrites assert statements in test modules
-in order to provide more useful :ref:`assert feedback <assertfeedback>`.
-This completely avoids previous issues of confusing assertion-reporting.
-It also means, that you can use Python's ``-O`` optimization without losing
-assertions in test modules.
-
-You can also turn off all assertion interaction using the
-``--assert=plain`` option.
-
-.. _`py namespaces`: index.html
-.. _`py/__init__.py`: http://bitbucket.org/hpk42/py-trunk/src/trunk/py/__init__.py
-
-
-Why can I use both ``pytest`` and ``py.test`` commands?
-+++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-pytest used to be part of the py package, which provided several developer
-utilities, all starting with ``py.<TAB>``, thus providing nice TAB-completion.
-If you install ``pip install pycmd`` you get these tools from a separate
-package. Once ``pytest`` became a separate package, the ``py.test`` name was
-retained due to avoid a naming conflict with another tool. This conflict was
-eventually resolved, and the ``pytest`` command was therefore introduced. In
-future versions of pytest, we may deprecate and later remove the ``py.test``
-command to avoid perpetuating the confusion.
-
-pytest fixtures, parametrized tests
--------------------------------------------------------
-
-.. _funcargs: funcargs.html
-
-Is using pytest fixtures versus xUnit setup a style question?
-+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-For simple applications and for people experienced with nose_ or
-unittest-style test setup using `xUnit style setup`_ probably
-feels natural.  For larger test suites, parametrized testing
-or setup of complex test resources using fixtures_ may feel more natural.
-Moreover, fixtures are ideal for writing advanced test support
-code (like e.g. the monkeypatch_, the tmpdir_ or capture_ fixtures)
-because the support code can register setup/teardown functions
-in a managed class/module/function scope.
-
-.. _monkeypatch: monkeypatch.html
-.. _tmpdir: tmpdir.html
-.. _capture: capture.html
-.. _fixtures: fixture.html
-
-.. _`why pytest_pyfuncarg__ methods?`:
-
-.. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration
-
-Can I yield multiple values from a fixture function?
-++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-There are two conceptual reasons why yielding from a factory function
-is not possible:
-
-* If multiple factories yielded values there would
-  be no natural place to determine the combination
-  policy - in real-world examples some combinations
-  often should not run.
-
-* Calling factories for obtaining test function arguments
-  is part of setting up and running a test.  At that
-  point it is not possible to add new test calls to
-  the test collection anymore.
-
-However, with pytest-2.3 you can use the :ref:`@pytest.fixture` decorator
-and specify ``params`` so that all tests depending on the factory-created
-resource will run multiple times with different parameters.
-
-You can also use the ``pytest_generate_tests`` hook to
-implement the `parametrization scheme of your choice`_. See also
-:ref:`paramexamples` for more examples.
-
-.. _`parametrization scheme of your choice`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/
-
-pytest interaction with other packages
----------------------------------------------------
-
-Issues with pytest, multiprocess and setuptools?
-+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
-On Windows the multiprocess package will instantiate sub processes
-by pickling and thus implicitly re-import a lot of local modules.
-Unfortunately, setuptools-0.6.11 does not ``if __name__=='__main__'``
-protect its generated command line script.  This leads to infinite
-recursion when running a test that instantiates Processes.
-
-As of mid-2013, there shouldn't be a problem anymore when you
-use the standard setuptools (note that distribute has been merged
-back into setuptools which is now shipped directly with virtualenv).
-
-.. _nose: https://nose.readthedocs.io/en/latest/
-.. _pylib: https://py.readthedocs.io/en/latest/
-.. _`xUnit style setup`: xunit_setup.html
index dd1f416cbdb9d2fca01ebd68969f54fa87724753..90e88d876203cc1353d0cfeb3f265ddbc857ad3e 100644 (file)
@@ -121,7 +121,7 @@ Fixtures as Function arguments
 Test functions can receive fixture objects by naming them as an input
 argument. For each argument name, a fixture function with that name provides
 the fixture object.  Fixture functions are registered by marking them with
-:py:func:`@pytest.fixture <_pytest.python.fixture>`.  Let's look at a simple
+:py:func:`@pytest.fixture <pytest.fixture>`.  Let's look at a simple
 self-contained test module containing a fixture and a test function
 using it:
 
@@ -144,7 +144,7 @@ using it:
         assert 0  # for demo purposes
 
 Here, the ``test_ehlo`` needs the ``smtp_connection`` fixture value.  pytest
-will discover and call the :py:func:`@pytest.fixture <_pytest.python.fixture>`
+will discover and call the :py:func:`@pytest.fixture <pytest.fixture>`
 marked ``smtp_connection`` fixture function.  Running the test looks like this:
 
 .. code-block:: pytest
@@ -252,7 +252,7 @@ Scope: sharing fixtures across classes, modules, packages or session
 Fixtures requiring network access depend on connectivity and are
 usually time-expensive to create.  Extending the previous example, we
 can add a ``scope="module"`` parameter to the
-:py:func:`@pytest.fixture <_pytest.python.fixture>` invocation
+:py:func:`@pytest.fixture <pytest.fixture>` invocation
 to cause the decorated ``smtp_connection`` fixture function to only be invoked
 once per test *module* (the default is to invoke once per test *function*).
 Multiple test functions in a test module will thus
@@ -592,7 +592,7 @@ will not be executed.
 Fixtures can introspect the requesting test context
 -------------------------------------------------------------
 
-Fixture functions can accept the :py:class:`request <FixtureRequest>` object
+Fixture functions can accept the :py:class:`request <_pytest.fixtures.FixtureRequest>` object
 to introspect the "requesting" test function, class or module context.
 Further extending the previous ``smtp_connection`` fixture example, let's
 read an optional server URL from the test module which uses our fixture:
@@ -664,7 +664,7 @@ from the module namespace.
 Using markers to pass data to fixtures
 -------------------------------------------------------------
 
-Using the :py:class:`request <FixtureRequest>` object, a fixture can also access
+Using the :py:class:`request <_pytest.fixtures.FixtureRequest>` object, a fixture can also access
 markers which are applied to a test function. This can be useful to pass data
 into a fixture from a test:
 
@@ -775,7 +775,7 @@ through the special :py:class:`request <FixtureRequest>` object:
         smtp_connection.close()
 
 The main change is the declaration of ``params`` with
-:py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values
+:py:func:`@pytest.fixture <pytest.fixture>`, a list of values
 for each of which the fixture function will execute and can access
 a value via ``request.param``.  No test function code needs to change.
 So let's just do another run:
@@ -1136,8 +1136,8 @@ and teared down after every test that used it.
 
 .. _`usefixtures`:
 
-Using fixtures from classes, modules or projects
-----------------------------------------------------------------------
+Use fixtures in classes and modules with ``usefixtures``
+--------------------------------------------------------
 
 .. regendoc:wipe
 
@@ -1531,3 +1531,37 @@ Given the tests file structure is:
 In the example above, a parametrized fixture is overridden with a non-parametrized version, and
 a non-parametrized fixture is overridden with a parametrized version for certain test module.
 The same applies for the test folder level obviously.
+
+
+Using fixtures from other projects
+----------------------------------
+
+Usually projects that provide pytest support will use :ref:`entry points <setuptools entry points>`,
+so just installing those projects into an environment will make those fixtures available for use.
+
+In case you want to use fixtures from a project that does not use entry points, you can
+define :globalvar:`pytest_plugins` in your top ``conftest.py`` file to register that module
+as a plugin.
+
+Suppose you have some fixtures in ``mylibrary.fixtures`` and you want to reuse them into your
+``app/tests`` directory.
+
+All you need to do is to define :globalvar:`pytest_plugins` in ``app/tests/conftest.py``
+pointing to that module.
+
+.. code-block:: python
+
+    pytest_plugins = "mylibrary.fixtures"
+
+This effectively registers ``mylibrary.fixtures`` as a plugin, making all its fixtures and
+hooks available to tests in ``app/tests``.
+
+.. note::
+
+    Sometimes users will *import* fixtures from other projects for use, however this is not
+    recommended: importing fixtures into a module will register them in pytest
+    as *defined* in that module.
+
+    This has minor consequences, such as appearing multiple times in ``pytest --help``,
+    but it is not **recommended** because this behavior might change/stop working
+    in future versions.
index 33b19ab0f35e090c6d7b365eb2f13880b06929d1..0c4913edff8704258a36319ef1be38bc79e27444 100644 (file)
@@ -51,7 +51,7 @@ There are several limitations and difficulties with this approach:
    performs parametrization at the places where the resource
    is used.  Moreover, you need to modify the factory to use an
    ``extrakey`` parameter containing ``request.param`` to the
-   :py:func:`~python.Request.cached_setup` call.
+   ``Request.cached_setup`` call.
 
 3. Multiple parametrized session-scoped resources will be active
    at the same time, making it hard for them to affect global state
@@ -113,7 +113,7 @@ This new way of parametrizing funcarg factories should in many cases
 allow to re-use already written factories because effectively
 ``request.param`` was already used when test functions/classes were
 parametrized via
-:py:func:`~_pytest.python.Metafunc.parametrize(indirect=True)` calls.
+:py:func:`metafunc.parametrize(indirect=True) <_pytest.python.Metafunc.parametrize>` calls.
 
 Of course it's perfectly fine to combine parametrization and scoping:
 
index caca6f511de6e60109add9d31f6b3fb1e5204b6d..c8730b9a370f3488c0e8e65a7bb4d9a02108242d 100644 (file)
@@ -28,7 +28,7 @@ Install ``pytest``
 .. code-block:: bash
 
     $ pytest --version
-    pytest 6.0.2
+    pytest 6.1.0
 
 .. _`simpletest`:
 
index ba96d32ab8798dbb481eaee377588ce98cffe857..4f8722c1c16af8e212327a671dbba39e6cf88507 100644 (file)
@@ -112,7 +112,7 @@ More details can be found in the `original PR <https://github.com/pytest-dev/pyt
 .. note::
 
     in a future major release of pytest we will introduce class based markers,
-    at which point markers will no longer be limited to instances of :py:class:`Mark`.
+    at which point markers will no longer be limited to instances of :py:class:`~_pytest.mark.Mark`.
 
 
 cache plugin integrated into the core
index f1cb533d78741222729343dd3f96e7e3afb58536..a57e9bbacee089cf6c7b0da2dc061893469a0f65 100644 (file)
@@ -1,5 +1,11 @@
 :orphan:
 
+.. sidebar:: Next Open Trainings
+
+   - `Professional testing with Python <https://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_, via Python Academy, February 1-3 2021, Leipzig (Germany) and remote.
+
+   Also see `previous talks and blogposts <talks.html>`_.
+
 .. _features:
 
 pytest: helps you write better programs
@@ -55,17 +61,17 @@ See :ref:`Getting Started <getstarted>` for more examples.
 Features
 --------
 
-- Detailed info on failing :ref:`assert statements <assert>` (no need to remember ``self.assert*`` names);
+- Detailed info on failing :ref:`assert statements <assert>` (no need to remember ``self.assert*`` names)
 
-- :ref:`Auto-discovery <test discovery>` of test modules and functions;
+- :ref:`Auto-discovery <test discovery>` of test modules and functions
 
-- :ref:`Modular fixtures <fixture>` for managing small or parametrized long-lived test resources;
+- :ref:`Modular fixtures <fixture>` for managing small or parametrized long-lived test resources
 
-- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box;
+- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
 
-- Python 3.5+ and PyPy 3;
+- Python 3.5+ and PyPy 3
 
-- Rich plugin architecture, with over 315+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community;
+- Rich plugin architecture, with over 315+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community
 
 
 Documentation
index 6fb665fdfd200d9774e3d001e5cfcab0edc67f24..7370342a96540e15c9818c6489f8a6acd3b196db 100644 (file)
@@ -43,7 +43,17 @@ You can register custom marks in your ``pytest.ini`` file like this:
         slow: marks tests as slow (deselect with '-m "not slow"')
         serial
 
-Note that everything after the ``:`` is an optional description.
+or in your ``pyproject.toml`` file like this:
+
+.. code-block:: toml
+
+    [tool.pytest.ini_options]
+    markers = [
+        "slow: marks tests as slow (deselect with '-m \"not slow\"')",
+        "serial",
+    ]
+
+Note that everything past the ``:`` after the mark name is an optional description.
 
 Alternatively, you can register new markers programmatically in a
 :ref:`pytest_configure <initialization-hooks>` hook:
@@ -66,7 +76,7 @@ Raising errors on unknown marks
 
 Unregistered marks applied with the ``@pytest.mark.name_of_the_mark`` decorator
 will always emit a warning in order to avoid silently doing something
-surprising due to mis-typed names. As described in the previous section, you can disable
+surprising due to mistyped names. As described in the previous section, you can disable
 the warning for custom marks by registering them in your ``pytest.ini`` file or
 using a custom ``pytest_configure`` hook.
 
index 939fb7ed611287bf248affef8970b86633260205..9480f008f7cdfb899cdf974beac4dcf20ee94e98 100644 (file)
@@ -33,25 +33,25 @@ Consider the following scenarios:
 
 1. Modifying the behavior of a function or the property of a class for a test e.g.
 there is an API call or database connection you will not make for a test but you know
-what the expected output should be. Use :py:meth:`monkeypatch.setattr` to patch the
+what the expected output should be. Use :py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` to patch the
 function or property with your desired testing behavior. This can include your own functions.
-Use :py:meth:`monkeypatch.delattr` to remove the function or property for the test.
+Use :py:meth:`monkeypatch.delattr <MonkeyPatch.delattr>` to remove the function or property for the test.
 
 2. Modifying the values of dictionaries e.g. you have a global configuration that
-you want to modify for certain test cases. Use :py:meth:`monkeypatch.setitem` to patch the
-dictionary for the test. :py:meth:`monkeypatch.delitem` can be used to remove items.
+you want to modify for certain test cases. Use :py:meth:`monkeypatch.setitem <MonkeyPatch.setitem>` to patch the
+dictionary for the test. :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` can be used to remove items.
 
 3. Modifying environment variables for a test e.g. to test program behavior if an
 environment variable is missing, or to set multiple values to a known variable.
-:py:meth:`monkeypatch.setenv` and :py:meth:`monkeypatch.delenv` can be used for
+:py:meth:`monkeypatch.setenv <MonkeyPatch.setenv>` and :py:meth:`monkeypatch.delenv <MonkeyPatch.delenv>` can be used for
 these patches.
 
 4. Use ``monkeypatch.setenv("PATH", value, prepend=os.pathsep)`` to modify ``$PATH``, and
-:py:meth:`monkeypatch.chdir` to change the context of the current working directory
+:py:meth:`monkeypatch.chdir <MonkeyPatch.chdir>` to change the context of the current working directory
 during a test.
 
-5. Use :py:meth:`monkeypatch.syspath_prepend` to modify ``sys.path`` which will also
-call :py:meth:`pkg_resources.fixup_namespace_packages` and :py:meth:`importlib.invalidate_caches`.
+5. Use :py:meth:`monkeypatch.syspath_prepend <MonkeyPatch.syspath_prepend>` to modify ``sys.path`` which will also
+call ``pkg_resources.fixup_namespace_packages`` and :py:func:`importlib.invalidate_caches`.
 
 See the `monkeypatch blog post`_ for some introduction material
 and a discussion of its motivation.
@@ -66,10 +66,10 @@ testing, you do not want your test to depend on the running user. ``monkeypatch`
 can be used to patch functions dependent on the user to always return a
 specific value.
 
-In this example, :py:meth:`monkeypatch.setattr` is used to patch ``Path.home``
+In this example, :py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` is used to patch ``Path.home``
 so that the known testing path ``Path("/abc")`` is always used when the test is run.
 This removes any dependency on the running user for testing purposes.
-:py:meth:`monkeypatch.setattr` must be called before the function which will use
+:py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` must be called before the function which will use
 the patched function is called.
 After the test function finishes the ``Path.home`` modification will be undone.
 
@@ -102,7 +102,7 @@ After the test function finishes the ``Path.home`` modification will be undone.
 Monkeypatching returned objects: building mock classes
 ------------------------------------------------------
 
-:py:meth:`monkeypatch.setattr` can be used in conjunction with classes to mock returned
+:py:meth:`monkeypatch.setattr <MonkeyPatch.setattr>` can be used in conjunction with classes to mock returned
 objects from functions instead of values.
 Imagine a simple function to take an API url and return the json response.
 
@@ -330,7 +330,7 @@ This behavior can be moved into ``fixture`` structures and shared across tests:
 Monkeypatching dictionaries
 ---------------------------
 
-:py:meth:`monkeypatch.setitem` can be used to safely set the values of dictionaries
+:py:meth:`monkeypatch.setitem <MonkeyPatch.setitem>` can be used to safely set the values of dictionaries
 to specific values during tests. Take this simplified connection string example:
 
 .. code-block:: python
@@ -367,7 +367,7 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific
         result = app.create_connection_string()
         assert result == expected
 
-You can use the :py:meth:`monkeypatch.delitem` to remove values.
+You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove values.
 
 .. code-block:: python
 
index 7f99cdc90fdd7babdbed210a6067798efffddfc2..d1540a8ff2a965a98eac999d5aa668abf52a3cb5 100644 (file)
@@ -182,7 +182,7 @@ Mark a test function as using the given fixture names.
 
 .. py:function:: pytest.mark.usefixtures(*names)
 
-    :param args: the names of the fixture to use, as strings
+    :param args: The names of the fixture to use, as strings.
 
 .. note::
 
@@ -209,8 +209,10 @@ Marks a test function as *expected to fail*.
         Condition for marking the test function as xfail (``True/False`` or a
         :ref:`condition string <string conditions>`). If a bool, you also have
         to specify ``reason`` (see :ref:`condition string <string conditions>`).
-    :keyword str reason: Reason why the test function is marked as xfail.
-    :keyword Exception raises: Exception subclass expected to be raised by the test function; other exceptions will fail the test.
+    :keyword str reason:
+        Reason why the test function is marked as xfail.
+    :keyword Type[Exception] raises:
+        Exception subclass expected to be raised by the test function; other exceptions will fail the test.
     :keyword bool run:
         If the test function should actually be executed. If ``False``, the function will always xfail and will
         not be executed (useful if a function is segfaulting).
@@ -224,7 +226,7 @@ Marks a test function as *expected to fail*.
           a new release of a library fixes a known bug).
 
 
-custom marks
+Custom marks
 ~~~~~~~~~~~~
 
 Marks are created dynamically using the factory object ``pytest.mark`` and applied as a decorator.
@@ -238,7 +240,7 @@ For example:
         ...
 
 Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to the collected
-:class:`Item <_pytest.nodes.Item>`, which can then be accessed by fixtures or hooks with
+:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
 :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`. The ``mark`` object will have the following attributes:
 
 .. code-block:: python
@@ -473,7 +475,7 @@ caplog
 .. autofunction:: _pytest.logging.caplog()
     :no-auto-options:
 
-    This returns a :class:`_pytest.logging.LogCaptureFixture` instance.
+    Returns a :class:`_pytest.logging.LogCaptureFixture` instance.
 
 .. autoclass:: _pytest.logging.LogCaptureFixture
     :members:
@@ -491,7 +493,7 @@ monkeypatch
 .. autofunction:: _pytest.monkeypatch.monkeypatch()
     :no-auto-options:
 
-    This returns a :class:`MonkeyPatch` instance.
+    Returns a :class:`MonkeyPatch` instance.
 
 .. autoclass:: _pytest.monkeypatch.MonkeyPatch
     :members:
@@ -537,14 +539,11 @@ recwarn
 .. autofunction:: recwarn()
     :no-auto-options:
 
-.. autoclass:: _pytest.recwarn.WarningsRecorder()
+.. autoclass:: WarningsRecorder()
     :members:
 
 Each recorded warning is an instance of :class:`warnings.WarningMessage`.
 
-.. note::
-    :class:`RecordedWarning` was changed from a plain class to a namedtuple in pytest 3.1
-
 .. note::
     ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated
     differently; see :ref:`ensuring_function_triggers`.
@@ -654,7 +653,6 @@ Collection hooks
 
 .. autofunction:: pytest_collection
 .. autofunction:: pytest_ignore_collect
-.. autofunction:: pytest_collect_directory
 .. autofunction:: pytest_collect_file
 .. autofunction:: pytest_pycollect_makemodule
 
@@ -675,7 +673,7 @@ items, delete or otherwise amend the test items:
 Test running (runtest) hooks
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-All runtest related hooks receive a :py:class:`pytest.Item <_pytest.main.Item>` object.
+All runtest related hooks receive a :py:class:`pytest.Item <pytest.Item>` object.
 
 .. autofunction:: pytest_runtestloop
 .. autofunction:: pytest_runtest_protocol
@@ -687,8 +685,8 @@ All runtest related hooks receive a :py:class:`pytest.Item <_pytest.main.Item>`
 .. autofunction:: pytest_runtest_makereport
 
 For deeper understanding you may look at the default implementation of
-these hooks in :py:mod:`_pytest.runner` and maybe also
-in :py:mod:`_pytest.pdb` which interacts with :py:mod:`_pytest.capture`
+these hooks in ``_pytest.runner`` and maybe also
+in ``_pytest.pdb`` which interacts with ``_pytest.capture``
 and its input/output capturing in order to immediately drop
 into interactive debugging when a test failure occurs.
 
@@ -751,14 +749,14 @@ CallInfo
 Class
 ~~~~~
 
-.. autoclass:: _pytest.python.Class()
+.. autoclass:: pytest.Class()
     :members:
     :show-inheritance:
 
 Collector
 ~~~~~~~~~
 
-.. autoclass:: _pytest.nodes.Collector()
+.. autoclass:: pytest.Collector()
     :members:
     :show-inheritance:
 
@@ -786,13 +784,13 @@ ExceptionInfo
 ExitCode
 ~~~~~~~~
 
-.. autoclass:: _pytest.config.ExitCode
+.. autoclass:: pytest.ExitCode
     :members:
 
 File
 ~~~~
 
-.. autoclass:: _pytest.nodes.File()
+.. autoclass:: pytest.File()
     :members:
     :show-inheritance:
 
@@ -814,14 +812,14 @@ FSCollector
 Function
 ~~~~~~~~
 
-.. autoclass:: _pytest.python.Function()
+.. autoclass:: pytest.Function()
     :members:
     :show-inheritance:
 
 Item
 ~~~~
 
-.. autoclass:: _pytest.nodes.Item()
+.. autoclass:: pytest.Item()
     :members:
     :show-inheritance:
 
@@ -855,7 +853,7 @@ Metafunc
 Module
 ~~~~~~
 
-.. autoclass:: _pytest.python.Module()
+.. autoclass:: pytest.Module()
     :members:
     :show-inheritance:
 
@@ -871,12 +869,6 @@ Parser
 .. autoclass:: _pytest.config.argparsing.Parser()
     :members:
 
-PluginManager
-~~~~~~~~~~~~~
-
-.. autoclass:: pluggy.PluginManager()
-    :members:
-
 
 PytestPluginManager
 ~~~~~~~~~~~~~~~~~~~
@@ -884,12 +876,13 @@ PytestPluginManager
 .. autoclass:: _pytest.config.PytestPluginManager()
     :members:
     :undoc-members:
+    :inherited-members:
     :show-inheritance:
 
 Session
 ~~~~~~~
 
-.. autoclass:: _pytest.main.Session()
+.. autoclass:: pytest.Session()
     :members:
     :show-inheritance:
 
@@ -1031,7 +1024,7 @@ When set (regardless of value), pytest will use color in terminal output.
 Exceptions
 ----------
 
-.. autoclass:: _pytest.config.UsageError()
+.. autoclass:: pytest.UsageError()
     :show-inheritance:
 
 .. _`warnings ref`:
@@ -1659,3 +1652,296 @@ passed multiple times. The expected format is ``name=value``. For example::
 
         [pytest]
         xfail_strict = True
+
+
+.. _`command-line-flags`:
+
+Command-line Flags
+------------------
+
+All the command-line flags can be obtained by running ``pytest --help``::
+
+    $ pytest --help
+    usage: pytest [options] [file_or_dir] [file_or_dir] [...]
+
+    positional arguments:
+      file_or_dir
+
+    general:
+      -k EXPRESSION         only run tests which match the given substring
+                            expression. An expression is a python evaluatable
+                            expression where all names are substring-matched
+                            against test names and their parent classes.
+                            Example: -k 'test_method or test_other' matches all
+                            test functions and classes whose name contains
+                            'test_method' or 'test_other', while -k 'not
+                            test_method' matches those that don't contain
+                            'test_method' in their names. -k 'not test_method
+                            and not test_other' will eliminate the matches.
+                            Additionally keywords are matched to classes and
+                            functions containing extra names in their
+                            'extra_keyword_matches' set, as well as functions
+                            which have names assigned directly to them. The
+                            matching is case-insensitive.
+      -m MARKEXPR           only run tests matching given mark expression.
+                            For example: -m 'mark1 and not mark2'.
+      --markers             show markers (builtin, plugin and per-project ones).
+      -x, --exitfirst       exit instantly on first error or failed test.
+      --fixtures, --funcargs
+                            show available fixtures, sorted by plugin appearance
+                            (fixtures with leading '_' are only shown with '-v')
+      --fixtures-per-test   show fixtures per test
+      --pdb                 start the interactive Python debugger on errors or
+                            KeyboardInterrupt.
+      --pdbcls=modulename:classname
+                            start a custom interactive Python debugger on
+                            errors. For example:
+                            --pdbcls=IPython.terminal.debugger:TerminalPdb
+      --trace               Immediately break when running each test.
+      --capture=method      per-test capturing method: one of fd|sys|no|tee-sys.
+      -s                    shortcut for --capture=no.
+      --runxfail            report the results of xfail tests as if they were
+                            not marked
+      --lf, --last-failed   rerun only the tests that failed at the last run (or
+                            all if none failed)
+      --ff, --failed-first  run all tests, but run the last failures first.
+                            This may re-order tests and thus lead to repeated
+                            fixture setup/teardown.
+      --nf, --new-first     run tests from new files first, then the rest of the
+                            tests sorted by file mtime
+      --cache-show=[CACHESHOW]
+                            show cache contents, don't perform collection or
+                            tests. Optional argument: glob (default: '*').
+      --cache-clear         remove all cache contents at start of test run.
+      --lfnf={all,none}, --last-failed-no-failures={all,none}
+                            which tests to run with no previously (known)
+                            failures.
+      --sw, --stepwise      exit on test failure and continue from last failing
+                            test next time
+      --stepwise-skip       ignore the first failing test but stop on the next
+                            failing test
+
+    reporting:
+      --durations=N         show N slowest setup/test durations (N=0 for all).
+      --durations-min=N     Minimal duration in seconds for inclusion in slowest
+                            list. Default 0.005
+      -v, --verbose         increase verbosity.
+      --no-header           disable header
+      --no-summary          disable summary
+      -q, --quiet           decrease verbosity.
+      --verbosity=VERBOSE   set verbosity. Default is 0.
+      -r chars              show extra test summary info as specified by chars:
+                            (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed,
+                            (p)assed, (P)assed with output, (a)ll except passed
+                            (p/P), or (A)ll. (w)arnings are enabled by default
+                            (see --disable-warnings), 'N' can be used to reset
+                            the list. (default: 'fE').
+      --disable-warnings, --disable-pytest-warnings
+                            disable warnings summary
+      -l, --showlocals      show locals in tracebacks (disabled by default).
+      --tb=style            traceback print mode
+                            (auto/long/short/line/native/no).
+      --show-capture={no,stdout,stderr,log,all}
+                            Controls how captured stdout/stderr/log is shown on
+                            failed tests. Default is 'all'.
+      --full-trace          don't cut any tracebacks (default is to cut).
+      --color=color         color terminal output (yes/no/auto).
+      --code-highlight={yes,no}
+                            Whether code should be highlighted (only if --color
+                            is also enabled)
+      --pastebin=mode       send failed|all info to bpaste.net pastebin service.
+      --junit-xml=path      create junit-xml style report file at given path.
+      --junit-prefix=str    prepend prefix to classnames in junit-xml output
+
+    pytest-warnings:
+      -W PYTHONWARNINGS, --pythonwarnings=PYTHONWARNINGS
+                            set which warnings to report, see -W option of
+                            python itself.
+      --maxfail=num         exit after first num failures or errors.
+      --strict-config       any warnings encountered while parsing the `pytest`
+                            section of the configuration file raise errors.
+      --strict-markers, --strict
+                            markers not registered in the `markers` section of
+                            the configuration file raise errors.
+      -c file               load configuration from `file` instead of trying to
+                            locate one of the implicit configuration files.
+      --continue-on-collection-errors
+                            Force test execution even if collection errors
+                            occur.
+      --rootdir=ROOTDIR     Define root directory for tests. Can be relative
+                            path: 'root_dir', './root_dir',
+                            'root_dir/another_dir/'; absolute path:
+                            '/home/user/root_dir'; path with variables:
+                            '$HOME/root_dir'.
+
+    collection:
+      --collect-only, --co  only collect tests, don't execute them.
+      --pyargs              try to interpret all arguments as python packages.
+      --ignore=path         ignore path during collection (multi-allowed).
+      --ignore-glob=path    ignore path pattern during collection (multi-
+                            allowed).
+      --deselect=nodeid_prefix
+                            deselect item (via node id prefix) during collection
+                            (multi-allowed).
+      --confcutdir=dir      only load conftest.py's relative to specified dir.
+      --noconftest          Don't load any conftest.py files.
+      --keep-duplicates     Keep duplicate tests.
+      --collect-in-virtualenv
+                            Don't ignore tests in a local virtualenv directory
+      --import-mode={prepend,append,importlib}
+                            prepend/append to sys.path when importing test
+                            modules and conftest files, default is to prepend.
+      --doctest-modules     run doctests in all .py modules
+      --doctest-report={none,cdiff,ndiff,udiff,only_first_failure}
+                            choose another output format for diffs on doctest
+                            failure
+      --doctest-glob=pat    doctests file matching pattern, default: test*.txt
+      --doctest-ignore-import-errors
+                            ignore doctest ImportErrors
+      --doctest-continue-on-failure
+                            for a given doctest, continue to run after the first
+                            failure
+
+    test session debugging and configuration:
+      --basetemp=dir        base temporary directory for this test run.(warning:
+                            this directory is removed if it exists)
+      -V, --version         display pytest version and information about
+                            plugins.When given twice, also display information
+                            about plugins.
+      -h, --help            show help message and configuration info
+      -p name               early-load given plugin module name or entry point
+                            (multi-allowed).
+                            To avoid loading of plugins, use the `no:` prefix,
+                            e.g. `no:doctest`.
+      --trace-config        trace considerations of conftest.py files.
+      --debug               store internal tracing debug information in
+                            'pytestdebug.log'.
+      -o OVERRIDE_INI, --override-ini=OVERRIDE_INI
+                            override ini option with "option=value" style, e.g.
+                            `-o xfail_strict=True -o cache_dir=cache`.
+      --assert=MODE         Control assertion debugging tools.
+                            'plain' performs no assertion debugging.
+                            'rewrite' (the default) rewrites assert statements
+                            in test modules on import to provide assert
+                            expression information.
+      --setup-only          only setup fixtures, do not execute tests.
+      --setup-show          show setup of fixtures while executing tests.
+      --setup-plan          show what fixtures and tests would be executed but
+                            don't execute anything.
+
+    logging:
+      --log-level=LEVEL     level of messages to catch/display.
+                            Not set by default, so it depends on the root/parent
+                            log handler's effective level, where it is "WARNING"
+                            by default.
+      --log-format=LOG_FORMAT
+                            log format as used by the logging module.
+      --log-date-format=LOG_DATE_FORMAT
+                            log date format as used by the logging module.
+      --log-cli-level=LOG_CLI_LEVEL
+                            cli logging level.
+      --log-cli-format=LOG_CLI_FORMAT
+                            log format as used by the logging module.
+      --log-cli-date-format=LOG_CLI_DATE_FORMAT
+                            log date format as used by the logging module.
+      --log-file=LOG_FILE   path to a file when logging will be written to.
+      --log-file-level=LOG_FILE_LEVEL
+                            log file logging level.
+      --log-file-format=LOG_FILE_FORMAT
+                            log format as used by the logging module.
+      --log-file-date-format=LOG_FILE_DATE_FORMAT
+                            log date format as used by the logging module.
+      --log-auto-indent=LOG_AUTO_INDENT
+                            Auto-indent multiline messages passed to the logging
+                            module. Accepts true|on, false|off or an integer.
+
+    [pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:
+
+      markers (linelist):   markers for test functions
+      empty_parameter_set_mark (string):
+                            default marker for empty parametersets
+      norecursedirs (args): directory patterns to avoid for recursion
+      testpaths (args):     directories to search for tests when no files or
+                            directories are given in the command line.
+      filterwarnings (linelist):
+                            Each line specifies a pattern for
+                            warnings.filterwarnings. Processed after
+                            -W/--pythonwarnings.
+      usefixtures (args):   list of default fixtures to be used with this
+                            project
+      python_files (args):  glob-style file patterns for Python test module
+                            discovery
+      python_classes (args):
+                            prefixes or glob names for Python test class
+                            discovery
+      python_functions (args):
+                            prefixes or glob names for Python test function and
+                            method discovery
+      disable_test_id_escaping_and_forfeit_all_rights_to_community_support (bool):
+                            disable string escape non-ascii characters, might
+                            cause unwanted side effects(use at your own risk)
+      console_output_style (string):
+                            console output: "classic", or with additional
+                            progress information ("progress" (percentage) |
+                            "count").
+      xfail_strict (bool):  default for the strict parameter of xfail markers
+                            when not given explicitly (default: False)
+      enable_assertion_pass_hook (bool):
+                            Enables the pytest_assertion_pass hook.Make sure to
+                            delete any previously generated pyc cache files.
+      junit_suite_name (string):
+                            Test suite name for JUnit report
+      junit_logging (string):
+                            Write captured log messages to JUnit report: one of
+                            no|log|system-out|system-err|out-err|all
+      junit_log_passing_tests (bool):
+                            Capture log information for passing tests to JUnit
+                            report:
+      junit_duration_report (string):
+                            Duration time to report: one of total|call
+      junit_family (string):
+                            Emit XML for schema: one of legacy|xunit1|xunit2
+      doctest_optionflags (args):
+                            option flags for doctests
+      doctest_encoding (string):
+                            encoding used for doctest files
+      cache_dir (string):   cache directory path.
+      log_level (string):   default value for --log-level
+      log_format (string):  default value for --log-format
+      log_date_format (string):
+                            default value for --log-date-format
+      log_cli (bool):       enable log display during test run (also known as
+                            "live logging").
+      log_cli_level (string):
+                            default value for --log-cli-level
+      log_cli_format (string):
+                            default value for --log-cli-format
+      log_cli_date_format (string):
+                            default value for --log-cli-date-format
+      log_file (string):    default value for --log-file
+      log_file_level (string):
+                            default value for --log-file-level
+      log_file_format (string):
+                            default value for --log-file-format
+      log_file_date_format (string):
+                            default value for --log-file-date-format
+      log_auto_indent (string):
+                            default value for --log-auto-indent
+      faulthandler_timeout (string):
+                            Dump the traceback of all threads if a test takes
+                            more than TIMEOUT seconds to finish.
+      addopts (args):       extra command line options
+      minversion (string):  minimally required pytest version
+      required_plugins (args):
+                            plugins that must be present for pytest to run
+
+    environment variables:
+      PYTEST_ADDOPTS           extra command line options
+      PYTEST_PLUGINS           comma-separated plugins to load during startup
+      PYTEST_DISABLE_PLUGIN_AUTOLOAD set to disable plugin auto-loading
+      PYTEST_DEBUG             set to enable debug tracing of pytest's internals
+
+
+    to see available markers type: pytest --markers
+    to see available fixtures type: pytest --fixtures
+    (shown according to specified file_or_dir or current dir if not specified; fixtures with leading '_' are only shown with the '-v' option
index 253dfe78ed8a78227a01818da413f0cb8e6961c9..216ccb8dd8a46e5f0d6ad133c22d6b9d510cc999 100644 (file)
@@ -2,13 +2,6 @@
 Talks and Tutorials
 ==========================
 
-.. sidebar:: Next Open Trainings
-
-   - `Free 1h webinar: "pytest: Test Driven Development für Python" <https://mylearning.ch/kurse/online-kurse/tech-webinar/>`_ (German), online, August 18 2020.
-   - `"pytest: Test Driven Development (nicht nur) für Python" <https://workshoptage.ch/workshops/2020/pytest-test-driven-development-nicht-nur-fuer-python/>`_ (German) at the `CH Open Workshoptage <https://workshoptage.ch/>`_, September 8 2020, HSLU Campus Rotkreuz (ZG), Switzerland.
-
-.. _`funcargs`: funcargs.html
-
 Books
 ---------------------------------------------
 
@@ -21,6 +14,16 @@ Books
 Talks and blog postings
 ---------------------------------------------
 
+- Webinar: `pytest: Test Driven Development für Python (German) <https://bruhin.software/ins-pytest/>`_, Florian Bruhin, via mylearning.ch, 2020
+
+- Webinar: `Simplify Your Tests with Fixtures <https://blog.jetbrains.com/pycharm/2020/08/webinar-recording-simplify-your-tests-with-fixtures-with-oliver-bestwalter/>`_, Oliver Bestwalter, via JetBrains, 2020
+
+- Training: `Introduction to pytest - simple, rapid and fun testing with Python <https://www.youtube.com/watch?v=CMuSn9cofbI>`_, Florian Bruhin, PyConDE 2019
+
+- Abridged metaprogramming classics - this episode: pytest, Oliver Bestwalter, PyConDE 2019 (`repository <https://github.com/obestwalter/abridged-meta-programming-classics>`__, `recording <https://www.youtube.com/watch?v=zHpeMTJsBRk&feature=youtu.be>`__)
+
+- Testing PySide/PyQt code easily using the pytest framework, Florian Bruhin, Qt World Summit 2019 (`slides <https://bruhin.software/talks/qtws19.pdf>`__, `recording <https://www.youtube.com/watch?v=zdsBS5BXGqQ>`__)
+
 - `pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyBCN June 2019 <https://www.slideshare.net/AndreuVallbonaPlazas/pybcn-pytest-recomendaciones-paquetes-bsicos-para-testing-en-python-y-django>`_.
 
 - pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyconES 2017 (`slides in english <http://talks.apsl.io/testing-pycones-2017/>`_, `video in spanish <https://www.youtube.com/watch?v=K20GeR-lXDk>`_)
@@ -52,8 +55,6 @@ Talks and blog postings
 - `pytest: helps you write better Django apps, Andreas Pelme, DjangoCon
   Europe 2014 <https://www.youtube.com/watch?v=aaArYVh6XSM>`_.
 
-- :ref:`fixtures`
-
 - `Testing Django Applications with pytest, Andreas Pelme, EuroPython
   2013 <https://www.youtube.com/watch?v=aUf8Fkb7TaY>`_.
 
index aafdeb55f45cf0dd361ac8b7897a320c663cea65..3c03db4540fd50f2dbea96b17b11a7fc707259d5 100644 (file)
@@ -33,7 +33,7 @@ Running ``pytest`` can result in six different exit codes:
 :Exit code 4: pytest command line usage error
 :Exit code 5: No tests were collected
 
-They are represented by the :class:`_pytest.config.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using:
+They are represented by the :class:`pytest.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using:
 
 .. code-block:: python
 
@@ -57,6 +57,8 @@ Getting help on version, option names, environment variables
     pytest -h | --help # show help on command line and config file options
 
 
+The full command-line flags can be found in the :ref:`reference <command-line-flags>`.
+
 .. _maxfail:
 
 Stopping after the first (or N) failures
@@ -426,14 +428,15 @@ Pytest supports the use of ``breakpoint()`` with the following behaviours:
 Profiling test execution duration
 -------------------------------------
 
+.. versionchanged:: 6.0
 
-To get a list of the slowest 10 test durations:
+To get a list of the slowest 10 test durations over 1.0s long:
 
 .. code-block:: bash
 
-    pytest --durations=10
+    pytest --durations=10 --durations-min=1.0
 
-By default, pytest will not show test durations that are too small (<0.01s) unless ``-vv`` is passed on the command-line.
+By default, pytest will not show test durations that are too small (<0.005s) unless ``-vv`` is passed on the command-line.
 
 
 .. _faulthandler:
index d1e27ecad21dd98a28b75f61385d0492de0b620f..7232b676d249fea6c396b9106019f4e383eb8d0a 100644 (file)
@@ -68,16 +68,30 @@ them into errors:
     FAILED test_show_warnings.py::test_one - UserWarning: api v1, should use ...
     1 failed in 0.12s
 
-The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option.
-For example, the configuration below will ignore all user warnings, but will transform
+The same option can be set in the ``pytest.ini`` or ``pyproject.toml`` file using the
+``filterwarnings`` ini option. For example, the configuration below will ignore all
+user warnings and specific deprecation warnings matching a regex, but will transform
 all other warnings into errors.
 
 .. code-block:: ini
 
+    # pytest.ini
     [pytest]
     filterwarnings =
         error
         ignore::UserWarning
+        ignore:function ham\(\) is deprecated:DeprecationWarning
+
+.. code-block:: toml
+
+    # pyproject.toml
+    [tool.pytest.ini_options]
+    filterwarnings = [
+        "error",
+        "ignore::UserWarning",
+        # note the use of single quote below to denote "raw" strings in TOML
+        'ignore:function ham\(\) is deprecated:DeprecationWarning',
+    ]
 
 
 When a warning matches more than one option in the list, the action for the last matching option
@@ -353,7 +367,7 @@ warnings, or index into it to get a particular recorded warning.
 
 .. currentmodule:: _pytest.warnings
 
-Full API: :class:`WarningsRecorder`.
+Full API: :class:`~_pytest.recwarn.WarningsRecorder`.
 
 .. _custom_failure_messages:
 
index cf4dbf99feaa2872ded41c69700591c5e1b7615a..625ced7bd2f1283853c76fb48374f1d019404536 100644 (file)
@@ -404,7 +404,7 @@ return a result object, with which we can assert the tests' outcomes.
         result.assert_outcomes(passed=4)
 
 
-additionally it is possible to copy examples for an example folder before running pytest on it
+Additionally it is possible to copy examples for an example folder before running pytest on it.
 
 .. code-block:: ini
 
@@ -614,6 +614,11 @@ among each other.
 Declaring new hooks
 ------------------------
 
+.. note::
+
+    This is a quick overview on how to add new hooks and how they work in general, but a more complete
+    overview can be found in `the pluggy documentation <https://pluggy.readthedocs.io/en/latest/>`__.
+
 .. currentmodule:: _pytest.hookspec
 
 Plugins and ``conftest.py`` files may declare new hooks that can then be
@@ -627,7 +632,7 @@ Hooks are usually declared as do-nothing functions that contain only
 documentation describing when the hook will be called and what return values
 are expected. The names of the functions must start with `pytest_` otherwise pytest won't recognize them.
 
-Here's an example. Let's assume this code is in the ``hooks.py`` module.
+Here's an example. Let's assume this code is in the ``sample_hook.py`` module.
 
 .. code-block:: python
 
@@ -643,10 +648,10 @@ class or module can then be passed to the ``pluginmanager`` using the ``pytest_a
 .. code-block:: python
 
     def pytest_addhooks(pluginmanager):
-        """ This example assumes the hooks are grouped in the 'hooks' module. """
-        from my_app.tests import hooks
+        """ This example assumes the hooks are grouped in the 'sample_hook' module. """
+        from my_app.tests import sample_hook
 
-        pluginmanager.add_hookspecs(hooks)
+        pluginmanager.add_hookspecs(sample_hook)
 
 For a real world example, see `newhooks.py`_ from `xdist <https://github.com/pytest-dev/pytest-xdist>`_.
 
index 83545223ae3425f1ca24bad38d55d1ed9abad04e..8b3366f62ae48ac7e3cb2a6ac53e4b2499607f08 100644 (file)
@@ -12,7 +12,7 @@ fixtures (setup and teardown test state) on a per-module/class/function basis.
 .. note::
 
     While these setup/teardown methods are simple and familiar to those
-    coming from a ``unittest`` or nose ``background``, you may also consider
+    coming from a ``unittest`` or ``nose`` background, you may also consider
     using pytest's more powerful :ref:`fixture mechanism
     <fixture>` which leverages the concept of dependency injection, allowing
     for a more modular and more scalable approach for managing test state,
index c264b26446da42a6d2b696eb5a34e2aed6a7d8cc..4aaa3c3ec31c13554ed6cd0f8c78128c4f96dd7e 100644 (file)
@@ -1,6 +1,6 @@
 import json
+from pathlib import Path
 
-import py
 import requests
 
 issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"
@@ -31,12 +31,12 @@ def get_issues():
 
 
 def main(args):
-    cachefile = py.path.local(args.cache)
+    cachefile = Path(args.cache)
     if not cachefile.exists() or args.refresh:
         issues = get_issues()
-        cachefile.write(json.dumps(issues))
+        cachefile.write_text(json.dumps(issues), "utf-8")
     else:
-        issues = json.loads(cachefile.read())
+        issues = json.loads(cachefile.read_text("utf-8"))
 
     open_issues = [x for x in issues if x["state"] == "open"]
 
index 493213d841e8717c982ff1142585e303831f101c..aee467fcf12da484831df04b0beced89fecb9156 100644 (file)
@@ -1,16 +1,19 @@
 [build-system]
 requires = [
   # sync with setup.py until we discard non-pep-517/518
-  "setuptools>=40.0",
-  "setuptools-scm",
+  "setuptools>=42.0",
+  "setuptools-scm[toml]>=3.4",
   "wheel",
 ]
 build-backend = "setuptools.build_meta"
 
+[tool.setuptools_scm]
+write_to = "src/_pytest/_version.py"
+
 [tool.pytest.ini_options]
 minversion = "2.0"
 addopts = "-rfEX -p pytester --strict-markers"
-python_files = ["test_*.py", "*_test.py", "testing/*/*.py"]
+python_files = ["test_*.py", "*_test.py", "testing/python/*.py"]
 python_classes = ["Test", "Acceptance"]
 python_functions = ["test"]
 # NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
index ae727e3dee33bdf2e64e913d6119691913613a95..7c662113e06e95bcbcb28aac2db460ade6b0859e 100644 (file)
@@ -2,8 +2,8 @@
 This script is part of the pytest release process which is triggered by comments
 in issues.
 
-This script is started by the `release-on-comment.yml` workflow, which is triggered by two comment
-related events:
+This script is started by the `release-on-comment.yml` workflow, which always executes on
+`master` and is triggered by two comment related events:
 
 * https://help.github.com/en/actions/reference/events-that-trigger-workflows#issue-comment-event-issue_comment
 * https://help.github.com/en/actions/reference/events-that-trigger-workflows#issues-event-issues
@@ -30,7 +30,7 @@ import argparse
 import json
 import os
 import re
-import sys
+import traceback
 from pathlib import Path
 from subprocess import CalledProcessError
 from subprocess import check_call
@@ -94,7 +94,6 @@ def print_and_exit(msg) -> None:
 
 
 def trigger_release(payload_path: Path, token: str) -> None:
-    error_contents = ""  # to be used to store error output in case any command fails
     payload, base_branch, is_major = validate_and_get_issue_comment_payload(
         payload_path
     )
@@ -119,6 +118,7 @@ def trigger_release(payload_path: Path, token: str) -> None:
         issue.create_comment(str(e))
         print_and_exit(f"{Fore.RED}{e}")
 
+    error_contents = ""
     try:
         print(f"Version: {Fore.CYAN}{version}")
 
@@ -146,11 +146,12 @@ def trigger_release(payload_path: Path, token: str) -> None:
 
         print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.")
 
+        # important to use tox here because we have changed branches, so dependencies
+        # might have changed as well
+        cmdline = ["tox", "-e", "release", "--", version, "--skip-check-links"]
+        print("Running", " ".join(cmdline))
         run(
-            [sys.executable, "scripts/release.py", version, "--skip-check-links"],
-            text=True,
-            check=True,
-            capture_output=True,
+            cmdline, text=True, check=True, capture_output=True,
         )
 
         oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git"
@@ -178,43 +179,31 @@ def trigger_release(payload_path: Path, token: str) -> None:
         )
         print(f"Notified in original comment {Fore.CYAN}{comment.url}{Fore.RESET}.")
 
-        print(f"{Fore.GREEN}Success.")
     except CalledProcessError as e:
-        error_contents = e.output
-    except Exception as e:
-        error_contents = str(e)
-        link = f"https://github.com/{SLUG}/actions/runs/{os.environ['GITHUB_RUN_ID']}"
-        issue.create_comment(
-            dedent(
-                f"""
-            Sorry, the request to prepare release `{version}` from {base_branch} failed with:
-
-            ```
-            {e}
-            ```
-
-            See: {link}.
-            """
-            )
-        )
-        print_and_exit(f"{Fore.RED}{e}")
+        error_contents = f"CalledProcessError\noutput:\n{e.output}\nstderr:\n{e.stderr}"
+    except Exception:
+        error_contents = f"Exception:\n{traceback.format_exc()}"
 
     if error_contents:
         link = f"https://github.com/{SLUG}/actions/runs/{os.environ['GITHUB_RUN_ID']}"
-        issue.create_comment(
-            dedent(
-                f"""
-                Sorry, the request to prepare release `{version}` from {base_branch} failed with:
-
-                ```
-                {error_contents}
-                ```
-
-                See: {link}.
-                """
-            )
+        msg = ERROR_COMMENT.format(
+            version=version, base_branch=base_branch, contents=error_contents, link=link
         )
+        issue.create_comment(msg)
         print_and_exit(f"{Fore.RED}{error_contents}")
+    else:
+        print(f"{Fore.GREEN}Success.")
+
+
+ERROR_COMMENT = """\
+The request to prepare release `{version}` from {base_branch} failed with:
+
+```
+{contents}
+```
+
+See: {link}.
+"""
 
 
 def find_next_version(base_branch: str, is_major: bool) -> str:
index 443b868f3d706fbf27c9f49a3e2f2fc15251dbb3..5e3158ab52b35affb4e2f8c8622b90117611aeb4 100644 (file)
@@ -1,6 +1,4 @@
-"""
-Invoke development tasks.
-"""
+"""Invoke development tasks."""
 import argparse
 import os
 from pathlib import Path
index 31123f28e2ea2b2dd0722a9a83e0e455fe53d542..f4170f15ae2ecf10492cd77d8949d1a0749eef2b 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -42,7 +42,6 @@ packages =
 install_requires =
     attrs>=17.4.0
     iniconfig
-    more-itertools>=4.0.0
     packaging
     pluggy>=0.12,<1.0
     py>=1.8.2
@@ -97,10 +96,13 @@ formats = sdist.tgz,bdist_wheel
 [mypy]
 mypy_path = src
 check_untyped_defs = True
+disallow_any_generics = True
 ignore_missing_imports = True
 no_implicit_optional = True
 show_error_codes = True
 strict_equality = True
 warn_redundant_casts = True
 warn_return_any = True
+warn_unreachable = True
 warn_unused_configs = True
+no_implicit_reexport = True
index 4475e30a71e6c06422663d29a302f87a49f8d473..7f1a1763ca9cebc7bc16576d353d3284ee5d3c7d 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -1,9 +1,4 @@
 from setuptools import setup
 
-
-def main():
-    setup(use_scm_version={"write_to": "src/_pytest/_version.py"})
-
-
 if __name__ == "__main__":
-    main()
+    setup()
index 7ca216ecf969b0ddc4b97f18cbed277483880e31..3dbdf9318be72544df1016eef3866cfaca7780d0 100644 (file)
@@ -1,7 +1,8 @@
-"""allow bash-completion for argparse with argcomplete if installed
-needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
+"""Allow bash-completion for argparse with argcomplete if installed.
+
+Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
 to find the magic string, so _ARGCOMPLETE env. var is never set, and
-this does not need special code.
+this does not need special code).
 
 Function try_argcomplete(parser) should be called directly before
 the call to ArgumentParser.parse_args().
@@ -10,8 +11,7 @@ The filescompleter is what you normally would use on the positional
 arguments specification, in order to get "dirname/" after "dirn<TAB>"
 instead of the default "dirname ":
 
-   optparser.add_argument(Config._file_or_dir, nargs='*'
-                               ).completer=filescompleter
+   optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter
 
 Other, application specific, completers should go in the file
 doing the add_argument calls as they need to be specified as .completer
@@ -20,35 +20,43 @@ attribute points to will not be used).
 
 SPEEDUP
 =======
+
 The generic argcomplete script for bash-completion
-(/etc/bash_completion.d/python-argcomplete.sh )
+(/etc/bash_completion.d/python-argcomplete.sh)
 uses a python program to determine startup script generated by pip.
 You can speed up completion somewhat by changing this script to include
   # PYTHON_ARGCOMPLETE_OK
 so the the python-argcomplete-check-easy-install-script does not
 need to be called to find the entry point of the code and see if that is
-marked  with PYTHON_ARGCOMPLETE_OK
+marked  with PYTHON_ARGCOMPLETE_OK.
 
 INSTALL/DEBUGGING
 =================
+
 To include this support in another application that has setup.py generated
 scripts:
-- add the line:
+
+- Add the line:
     # PYTHON_ARGCOMPLETE_OK
-  near the top of the main python entry point
-- include in the file calling parse_args():
+  near the top of the main python entry point.
+
+- Include in the file calling parse_args():
     from _argcomplete import try_argcomplete, filescompleter
-   , call try_argcomplete just before parse_args(), and optionally add
-   filescompleter to the positional arguments' add_argument()
+  Call try_argcomplete just before parse_args(), and optionally add
+  filescompleter to the positional arguments' add_argument().
+
 If things do not work right away:
-- switch on argcomplete debugging with (also helpful when doing custom
+
+- Switch on argcomplete debugging with (also helpful when doing custom
   completers):
     export _ARC_DEBUG=1
-- run:
+
+- Run:
     python-argcomplete-check-easy-install-script $(which appname)
     echo $?
-  will echo 0 if the magic line has been found, 1 if not
-- sometimes it helps to find early on errors using:
+  will echo 0 if the magic line has been found, 1 if not.
+
+- Sometimes it helps to find early on errors using:
     _ARGCOMPLETE=1 _ARC_DEBUG=1 appname
   which should throw a KeyError: 'COMPLINE' (which is properly set by the
   global argcomplete script).
@@ -63,13 +71,13 @@ from typing import Optional
 
 
 class FastFilesCompleter:
-    "Fast file completer class"
+    """Fast file completer class."""
 
     def __init__(self, directories: bool = True) -> None:
         self.directories = directories
 
     def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
-        """only called on non option completions"""
+        # Only called on non option completions.
         if os.path.sep in prefix[1:]:
             prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
         else:
@@ -77,7 +85,7 @@ class FastFilesCompleter:
         completion = []
         globbed = []
         if "*" not in prefix and "?" not in prefix:
-            # we are on unix, otherwise no bash
+            # We are on unix, otherwise no bash.
             if not prefix or prefix[-1] == os.path.sep:
                 globbed.extend(glob(prefix + ".*"))
             prefix += "*"
@@ -85,7 +93,7 @@ class FastFilesCompleter:
         for x in sorted(globbed):
             if os.path.isdir(x):
                 x += "/"
-            # append stripping the prefix (like bash, not like compgen)
+            # Append stripping the prefix (like bash, not like compgen).
             completion.append(x[prefix_dir:])
         return completion
 
index 136da31959e8754f63a6f4ec152d7431e1a3fa4f..511d0dde661dfb97378f76f1d17cbc55e150ef80 100644 (file)
@@ -4,9 +4,9 @@ from .code import ExceptionInfo
 from .code import filter_traceback
 from .code import Frame
 from .code import getfslineno
-from .code import getrawcode
 from .code import Traceback
 from .code import TracebackEntry
+from .source import getrawcode
 from .source import Source
 
 __all__ = [
index 14a4681ab49f4e4d6253d5217d3adbd94e72af59..5063e6604776a66876634e5a0225994ced1ad515 100644 (file)
@@ -38,9 +38,11 @@ from _pytest._io import TerminalWriter
 from _pytest._io.saferepr import safeformat
 from _pytest._io.saferepr import saferepr
 from _pytest.compat import ATTRS_EQ_FIELD
+from _pytest.compat import final
 from _pytest.compat import get_real_func
 from _pytest.compat import overload
 from _pytest.compat import TYPE_CHECKING
+from _pytest.pathlib import Path
 
 if TYPE_CHECKING:
     from typing import Type
@@ -71,9 +73,8 @@ class Code:
 
     @property
     def path(self) -> Union[py.path.local, str]:
-        """Return a path object pointing to source code (or a str in case
-        of OSError / non-existing file).
-        """
+        """Return a path object pointing to source code, or an ``str`` in
+        case of ``OSError`` / non-existing file."""
         if not self.raw.co_filename:
             return ""
         try:
@@ -246,10 +247,20 @@ class TracebackEntry:
 
         Mostly for internal use.
         """
-        f = self.frame
-        tbh = f.f_locals.get(
-            "__tracebackhide__", f.f_globals.get("__tracebackhide__", False)
+        tbh = (
+            False
         )  # type: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]]
+        for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
+            # in normal cases, f_locals and f_globals are dictionaries
+            # however via `exec(...)` / `eval(...)` they can be other types
+            # (even incorrect types!).
+            # as such, we suppress all exceptions while accessing __tracebackhide__
+            try:
+                tbh = maybe_ns_dct["__tracebackhide__"]
+            except Exception:
+                pass
+            else:
+                break
         if tbh and callable(tbh):
             return tbh(None if self._excinfo is None else self._excinfo())
         return tbh
@@ -334,11 +345,11 @@ class Traceback(List[TracebackEntry]):
 
     @overload
     def __getitem__(self, key: int) -> TracebackEntry:
-        raise NotImplementedError()
+        ...
 
     @overload  # noqa: F811
     def __getitem__(self, key: slice) -> "Traceback":  # noqa: F811
-        raise NotImplementedError()
+        ...
 
     def __getitem__(  # noqa: F811
         self, key: Union[int, slice]
@@ -404,6 +415,7 @@ co_equal = compile(
 _E = TypeVar("_E", bound=BaseException, covariant=True)
 
 
+@final
 @attr.s(repr=False)
 class ExceptionInfo(Generic[_E]):
     """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
@@ -420,15 +432,16 @@ class ExceptionInfo(Generic[_E]):
         exc_info: Tuple["Type[_E]", "_E", TracebackType],
         exprinfo: Optional[str] = None,
     ) -> "ExceptionInfo[_E]":
-        """Returns an ExceptionInfo for an existing exc_info tuple.
+        """Return an ExceptionInfo for an existing exc_info tuple.
 
         .. warning::
 
             Experimental API
 
-        :param exprinfo: a text string helping to determine if we should
-                         strip ``AssertionError`` from the output, defaults
-                         to the exception message/``__str__()``
+        :param exprinfo:
+            A text string helping to determine if we should strip
+            ``AssertionError`` from the output. Defaults to the exception
+            message/``__str__()``.
         """
         _striptext = ""
         if exprinfo is None and isinstance(exc_info[1], AssertionError):
@@ -444,15 +457,16 @@ class ExceptionInfo(Generic[_E]):
     def from_current(
         cls, exprinfo: Optional[str] = None
     ) -> "ExceptionInfo[BaseException]":
-        """Returns an ExceptionInfo matching the current traceback.
+        """Return an ExceptionInfo matching the current traceback.
 
         .. warning::
 
             Experimental API
 
-        :param exprinfo: a text string helping to determine if we should
-                         strip ``AssertionError`` from the output, defaults
-                         to the exception message/``__str__()``
+        :param exprinfo:
+            A text string helping to determine if we should strip
+            ``AssertionError`` from the output. Defaults to the exception
+            message/``__str__()``.
         """
         tup = sys.exc_info()
         assert tup[0] is not None, "no current exception"
@@ -467,7 +481,7 @@ class ExceptionInfo(Generic[_E]):
         return cls(None)
 
     def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None:
-        """fill an unfilled ExceptionInfo created with for_later()"""
+        """Fill an unfilled ExceptionInfo created with ``for_later()``."""
         assert self._excinfo is None, "ExceptionInfo was already filled"
         self._excinfo = exc_info
 
@@ -568,7 +582,8 @@ class ExceptionInfo(Generic[_E]):
             Show locals per traceback entry.
             Ignored if ``style=="native"``.
 
-        :param str style: long|short|no|native|value traceback style
+        :param str style:
+            long|short|no|native|value traceback style.
 
         :param bool abspath:
             If paths should be changed to absolute or left unchanged.
@@ -583,7 +598,8 @@ class ExceptionInfo(Generic[_E]):
         :param bool truncate_locals:
             With ``showlocals==True``, make sure locals can be safely represented as strings.
 
-        :param bool chain: if chained exceptions in Python 3 should be shown.
+        :param bool chain:
+            If chained exceptions in Python 3 should be shown.
 
         .. versionchanged:: 3.9
 
@@ -610,7 +626,7 @@ class ExceptionInfo(Generic[_E]):
         )
         return fmt.repr_excinfo(self)
 
-    def match(self, regexp: "Union[str, Pattern]") -> "Literal[True]":
+    def match(self, regexp: "Union[str, Pattern[str]]") -> "Literal[True]":
         """Check whether the regular expression `regexp` matches the string
         representation of the exception using :func:`python:re.search`.
 
@@ -643,7 +659,7 @@ class FormattedExcinfo:
     astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
 
     def _getindent(self, source: "Source") -> int:
-        # figure out indent for given source
+        # Figure out indent for the given source.
         try:
             s = str(source.getstatement(len(source) - 1))
         except KeyboardInterrupt:
@@ -673,9 +689,9 @@ class FormattedExcinfo:
 
     def get_source(
         self,
-        source: "Source",
+        source: Optional["Source"],
         line_index: int = -1,
-        excinfo: Optional[ExceptionInfo] = None,
+        excinfo: Optional[ExceptionInfo[BaseException]] = None,
         short: bool = False,
     ) -> List[str]:
         """Return formatted and marked up source lines."""
@@ -700,11 +716,14 @@ class FormattedExcinfo:
         return lines
 
     def get_exconly(
-        self, excinfo: ExceptionInfo, indent: int = 4, markall: bool = False
+        self,
+        excinfo: ExceptionInfo[BaseException],
+        indent: int = 4,
+        markall: bool = False,
     ) -> List[str]:
         lines = []
         indentstr = " " * indent
-        # get the real exception information out
+        # Get the real exception information out.
         exlines = excinfo.exconly(tryshort=True).split("\n")
         failindent = self.fail_marker + indentstr[1:]
         for line in exlines:
@@ -730,8 +749,7 @@ class FormattedExcinfo:
                         str_repr = saferepr(value)
                     else:
                         str_repr = safeformat(value)
-                    # if len(str_repr) < 70 or not isinstance(value,
-                    #                            (list, tuple, dict)):
+                    # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
                     lines.append("{:<10} = {}".format(name, str_repr))
                     # else:
                     #    self._line("%-10s =\\" % (name,))
@@ -741,7 +759,9 @@ class FormattedExcinfo:
         return None
 
     def repr_traceback_entry(
-        self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None
+        self,
+        entry: TracebackEntry,
+        excinfo: Optional[ExceptionInfo[BaseException]] = None,
     ) -> "ReprEntry":
         lines = []  # type: List[str]
         style = entry._repr_style if entry._repr_style is not None else self.style
@@ -783,7 +803,7 @@ class FormattedExcinfo:
                 path = np
         return path
 
-    def repr_traceback(self, excinfo: ExceptionInfo) -> "ReprTraceback":
+    def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
         traceback = excinfo.traceback
         if self.tbfilter:
             traceback = traceback.filter()
@@ -809,16 +829,17 @@ class FormattedExcinfo:
     def _truncate_recursive_traceback(
         self, traceback: Traceback
     ) -> Tuple[Traceback, Optional[str]]:
-        """
-        Truncate the given recursive traceback trying to find the starting point
-        of the recursion.
+        """Truncate the given recursive traceback trying to find the starting
+        point of the recursion.
 
-        The detection is done by going through each traceback entry and finding the
-        point in which the locals of the frame are equal to the locals of a previous frame (see ``recursionindex()``.
+        The detection is done by going through each traceback entry and
+        finding the point in which the locals of the frame are equal to the
+        locals of a previous frame (see ``recursionindex()``).
 
-        Handle the situation where the recursion process might raise an exception (for example
-        comparing numpy arrays using equality raises a TypeError), in which case we do our best to
-        warn the user of the error and show a limited traceback.
+        Handle the situation where the recursion process might raise an
+        exception (for example comparing numpy arrays using equality raises a
+        TypeError), in which case we do our best to warn the user of the
+        error and show a limited traceback.
         """
         try:
             recursionindex = traceback.recursionindex()
@@ -847,12 +868,14 @@ class FormattedExcinfo:
 
         return traceback, extraline
 
-    def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr":
+    def repr_excinfo(
+        self, excinfo: ExceptionInfo[BaseException]
+    ) -> "ExceptionChainRepr":
         repr_chain = (
             []
         )  # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]]
-        e = excinfo.value
-        excinfo_ = excinfo  # type: Optional[ExceptionInfo]
+        e = excinfo.value  # type: Optional[BaseException]
+        excinfo_ = excinfo  # type: Optional[ExceptionInfo[BaseException]]
         descr = None
         seen = set()  # type: Set[int]
         while e is not None and id(e) not in seen:
@@ -863,8 +886,8 @@ class FormattedExcinfo:
                     excinfo_._getreprcrash() if self.style != "value" else None
                 )  # type: Optional[ReprFileLocation]
             else:
-                # fallback to native repr if the exception doesn't have a traceback:
-                # ExceptionInfo objects require a full traceback to work
+                # Fallback to native repr if the exception doesn't have a traceback:
+                # ExceptionInfo objects require a full traceback to work.
                 reprtraceback = ReprTracebackNative(
                     traceback.format_exception(type(e), e, None)
                 )
@@ -915,7 +938,7 @@ class TerminalRepr:
 # This class is abstract -- only subclasses are instantiated.
 @attr.s(**{ATTRS_EQ_FIELD: False})  # type: ignore
 class ExceptionRepr(TerminalRepr):
-    # Provided by in subclasses.
+    # Provided by subclasses.
     reprcrash = None  # type: Optional[ReprFileLocation]
     reprtraceback = None  # type: ReprTraceback
 
@@ -942,7 +965,7 @@ class ExceptionChainRepr(ExceptionRepr):
     def __attrs_post_init__(self) -> None:
         super().__attrs_post_init__()
         # reprcrash and reprtraceback of the outermost (the newest) exception
-        # in the chain
+        # in the chain.
         self.reprtraceback = self.chain[-1][0]
         self.reprcrash = self.chain[-1][1]
 
@@ -974,7 +997,7 @@ class ReprTraceback(TerminalRepr):
     entrysep = "_ "
 
     def toterminal(self, tw: TerminalWriter) -> None:
-        # the entries might have different styles
+        # The entries might have different styles.
         for i, entry in enumerate(self.reprentries):
             if entry.style == "long":
                 tw.line("")
@@ -1017,7 +1040,7 @@ class ReprEntry(TerminalRepr):
     style = attr.ib(type="_TracebackStyle")
 
     def _write_entry_lines(self, tw: TerminalWriter) -> None:
-        """Writes the source code portions of a list of traceback entries with syntax highlighting.
+        """Write the source code portions of a list of traceback entries with syntax highlighting.
 
         Usually entries are lines like these:
 
@@ -1095,8 +1118,8 @@ class ReprFileLocation(TerminalRepr):
     message = attr.ib(type=str)
 
     def toterminal(self, tw: TerminalWriter) -> None:
-        # filename and lineno output for each entry,
-        # using an output format that most editors understand
+        # Filename and lineno output for each entry, using an output format
+        # that most editors understand.
         msg = self.message
         i = msg.find("\n")
         if i != -1:
@@ -1171,17 +1194,17 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]:
     return code.path, code.firstlineno
 
 
-# relative paths that we use to filter traceback entries from appearing to the user;
-# see filter_traceback
+# Relative paths that we use to filter traceback entries from appearing to the user;
+# see filter_traceback.
 # note: if we need to add more paths than what we have now we should probably use a list
-# for better maintenance
+# for better maintenance.
 
-_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc"))
+_PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
 # pluggy is either a package or a single module depending on the version
-if _PLUGGY_DIR.basename == "__init__.py":
-    _PLUGGY_DIR = _PLUGGY_DIR.dirpath()
-_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath()
-_PY_DIR = py.path.local(py.__file__).dirpath()
+if _PLUGGY_DIR.name == "__init__.py":
+    _PLUGGY_DIR = _PLUGGY_DIR.parent
+_PYTEST_DIR = Path(_pytest.__file__).parent
+_PY_DIR = Path(py.__file__).parent
 
 
 def filter_traceback(entry: TracebackEntry) -> bool:
@@ -1193,15 +1216,23 @@ def filter_traceback(entry: TracebackEntry) -> bool:
     * internal traceback from pytest or its internal libraries, py and pluggy.
     """
     # entry.path might sometimes return a str object when the entry
-    # points to dynamically generated code
-    # see https://bitbucket.org/pytest-dev/py/issues/71
+    # points to dynamically generated code.
+    # See https://bitbucket.org/pytest-dev/py/issues/71.
     raw_filename = entry.frame.code.raw.co_filename
     is_generated = "<" in raw_filename and ">" in raw_filename
     if is_generated:
         return False
+
     # entry.path might point to a non-existing file, in which case it will
-    # also return a str object. see #1133
-    p = py.path.local(entry.path)
-    return (
-        not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR)
-    )
+    # also return a str object. See #1133.
+    p = Path(entry.path)
+
+    parents = p.parents
+    if _PLUGGY_DIR in parents:
+        return False
+    if _PYTEST_DIR in parents:
+        return False
+    if _PY_DIR in parents:
+        return False
+
+    return True
index 65560be2a5ecf58c2215fdb755544b50f55feea2..4ba18aa63613ae5e20b8d4f863175baf6694d7d6 100644 (file)
@@ -44,11 +44,11 @@ class Source:
 
     @overload
     def __getitem__(self, key: int) -> str:
-        raise NotImplementedError()
+        ...
 
     @overload  # noqa: F811
     def __getitem__(self, key: slice) -> "Source":  # noqa: F811
-        raise NotImplementedError()
+        ...
 
     def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]:  # noqa: F811
         if isinstance(key, int):
@@ -67,9 +67,7 @@ class Source:
         return len(self.lines)
 
     def strip(self) -> "Source":
-        """ return new source object with trailing
-            and leading blank lines removed.
-        """
+        """Return new Source object with trailing and leading blank lines removed."""
         start, end = 0, len(self)
         while start < end and not self.lines[start].strip():
             start += 1
@@ -80,31 +78,28 @@ class Source:
         return source
 
     def indent(self, indent: str = " " * 4) -> "Source":
-        """ return a copy of the source object with
-            all lines indented by the given indent-string.
-        """
+        """Return a copy of the source object with all lines indented by the
+        given indent-string."""
         newsource = Source()
         newsource.lines = [(indent + line) for line in self.lines]
         return newsource
 
     def getstatement(self, lineno: int) -> "Source":
-        """ return Source statement which contains the
-            given linenumber (counted from 0).
-        """
+        """Return Source statement which contains the given linenumber
+        (counted from 0)."""
         start, end = self.getstatementrange(lineno)
         return self[start:end]
 
     def getstatementrange(self, lineno: int) -> Tuple[int, int]:
-        """ return (start, end) tuple which spans the minimal
-            statement region which containing the given lineno.
-        """
+        """Return (start, end) tuple which spans the minimal statement region
+        which containing the given lineno."""
         if not (0 <= lineno < len(self)):
             raise IndexError("lineno out of range")
         ast, start, end = getstatementrange_ast(lineno, self)
         return start, end
 
     def deindent(self) -> "Source":
-        """return a new source object deindented."""
+        """Return a new Source object deindented."""
         newsource = Source()
         newsource.lines[:] = deindent(self.lines)
         return newsource
@@ -129,7 +124,7 @@ def findsource(obj) -> Tuple[Optional[Source], int]:
 
 
 def getrawcode(obj, trycall: bool = True):
-    """ return code object for given function. """
+    """Return code object for given function."""
     try:
         return obj.__code__
     except AttributeError:
@@ -148,8 +143,8 @@ def deindent(lines: Iterable[str]) -> List[str]:
 
 
 def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]:
-    # flatten all statements and except handlers into one lineno-list
-    # AST's line numbers start indexing at 1
+    # Flatten all statements and except handlers into one lineno-list.
+    # AST's line numbers start indexing at 1.
     values = []  # type: List[int]
     for x in ast.walk(node):
         if isinstance(x, (ast.stmt, ast.ExceptHandler)):
@@ -157,7 +152,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i
             for name in ("finalbody", "orelse"):
                 val = getattr(x, name, None)  # type: Optional[List[ast.stmt]]
                 if val:
-                    # treat the finally/orelse part as its own statement
+                    # Treat the finally/orelse part as its own statement.
                     values.append(val[0].lineno - 1 - 1)
     values.sort()
     insert_index = bisect_right(values, lineno)
@@ -178,13 +173,13 @@ def getstatementrange_ast(
     if astnode is None:
         content = str(source)
         # See #4260:
-        # don't produce duplicate warnings when compiling source to find ast
+        # Don't produce duplicate warnings when compiling source to find AST.
         with warnings.catch_warnings():
             warnings.simplefilter("ignore")
             astnode = ast.parse(content, "source", "exec")
 
     start, end = get_statement_startend2(lineno, astnode)
-    # we need to correct the end:
+    # We need to correct the end:
     # - ast-parsing strips comments
     # - there might be empty lines
     # - we might have lesser indented code blocks at the end
@@ -192,10 +187,10 @@ def getstatementrange_ast(
         end = len(source.lines)
 
     if end > start + 1:
-        # make sure we don't span differently indented code blocks
-        # by using the BlockFinder helper used which inspect.getsource() uses itself
+        # Make sure we don't span differently indented code blocks
+        # by using the BlockFinder helper used which inspect.getsource() uses itself.
         block_finder = inspect.BlockFinder()
-        # if we start with an indented line, put blockfinder to "started" mode
+        # If we start with an indented line, put blockfinder to "started" mode.
         block_finder.started = source.lines[start][0].isspace()
         it = ((x + "\n") for x in source.lines[start:end])
         try:
@@ -206,7 +201,7 @@ def getstatementrange_ast(
         except Exception:
             pass
 
-    # the end might still point to a comment or empty line, correct it
+    # The end might still point to a comment or empty line, correct it.
     while end:
         line = source.lines[end - 1].lstrip()
         if line.startswith("#") or not line:
index 823b8d71942f5e423a85d8dac56ae3cffd90c65e..9a4975f61ad24cde777ca45a3e51dab6006aac4b 100644 (file)
@@ -36,9 +36,8 @@ def _ellipsize(s: str, maxsize: int) -> str:
 
 
 class SafeRepr(reprlib.Repr):
-    """subclass of repr.Repr that limits the resulting size of repr()
-    and includes information on exceptions raised during the call.
-    """
+    """repr.Repr that limits the resulting size of repr() and includes
+    information on exceptions raised during the call."""
 
     def __init__(self, maxsize: int) -> None:
         super().__init__()
@@ -65,7 +64,8 @@ class SafeRepr(reprlib.Repr):
 
 
 def safeformat(obj: object) -> str:
-    """return a pretty printed string for the given object.
+    """Return a pretty printed string for the given object.
+
     Failing __repr__ functions of user instances will be represented
     with a short exception info.
     """
@@ -76,11 +76,14 @@ def safeformat(obj: object) -> str:
 
 
 def saferepr(obj: object, maxsize: int = 240) -> str:
-    """return a size-limited safe repr-string for the given object.
+    """Return a size-limited safe repr-string for the given object.
+
     Failing __repr__ functions of user instances will be represented
     with a short exception info and 'saferepr' generally takes
-    care to never raise exceptions itself.  This function is a wrapper
-    around the Repr/reprlib functionality of the standard 2.6 lib.
+    care to never raise exceptions itself.
+
+    This function is a wrapper around the Repr/reprlib functionality of the
+    standard 2.6 lib.
     """
     return SafeRepr(maxsize).repr(obj)
 
index 5ffc550db2894d477bc49cfc5a07f566bcaaa24c..a9404ebcc169927197ac50feff0a25406b38f2bb 100644 (file)
@@ -7,6 +7,7 @@ from typing import Sequence
 from typing import TextIO
 
 from .wcwidth import wcswidth
+from _pytest.compat import final
 
 
 # This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
@@ -36,6 +37,7 @@ def should_do_markup(file: TextIO) -> bool:
     )
 
 
+@final
 class TerminalWriter:
     _esctable = dict(
         black=30,
@@ -111,13 +113,13 @@ class TerminalWriter:
     ) -> None:
         if fullwidth is None:
             fullwidth = self.fullwidth
-        # the goal is to have the line be as long as possible
-        # under the condition that len(line) <= fullwidth
+        # The goal is to have the line be as long as possible
+        # under the condition that len(line) <= fullwidth.
         if sys.platform == "win32":
-            # if we print in the last column on windows we are on a
+            # If we print in the last column on windows we are on a
             # new line but there is no way to verify/neutralize this
-            # (we may not know the exact line width)
-            # so let's be defensive to avoid empty lines in the output
+            # (we may not know the exact line width).
+            # So let's be defensive to avoid empty lines in the output.
             fullwidth -= 1
         if title is not None:
             # we want 2 + 2*len(fill) + len(title) <= fullwidth
@@ -131,9 +133,9 @@ class TerminalWriter:
             # we want len(sepchar)*N <= fullwidth
             # i.e.    N <= fullwidth // len(sepchar)
             line = sepchar * (fullwidth // len(sepchar))
-        # in some situations there is room for an extra sepchar at the right,
+        # In some situations there is room for an extra sepchar at the right,
         # in particular if we consider that with a sepchar like "_ " the
-        # trailing space is not important at the end of the line
+        # trailing space is not important at the end of the line.
         if len(line) + len(sepchar.rstrip()) <= fullwidth:
             line += sepchar.rstrip()
 
index 64d2267e70af4b89015a02d186f653e0a9f37a9e..06057d0c4c1d5002fa4c2ab9b783ce3301030e24 100644 (file)
@@ -1,6 +1,4 @@
-"""
-support for presenting detailed information in failing assertions.
-"""
+"""Support for presenting detailed information in failing assertions."""
 import sys
 from typing import Any
 from typing import Generator
@@ -55,11 +53,11 @@ def register_assert_rewrite(*names: str) -> None:
     actually imported, usually in your __init__.py if you are a plugin
     using a package.
 
-    :raise TypeError: if the given module names are not strings.
+    :raises TypeError: If the given module names are not strings.
     """
     for name in names:
         if not isinstance(name, str):
-            msg = "expected module names as *args, got {0} instead"
+            msg = "expected module names as *args, got {0} instead"  # type: ignore[unreachable]
             raise TypeError(msg.format(repr(names)))
     for hook in sys.meta_path:
         if isinstance(hook, rewrite.AssertionRewritingHook):
@@ -105,9 +103,9 @@ def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
 
 
 def pytest_collection(session: "Session") -> None:
-    # this hook is only called when test modules are collected
+    # This hook is only called when test modules are collected
     # so for example not in the master process of pytest-xdist
-    # (which does not collect test modules)
+    # (which does not collect test modules).
     assertstate = session.config._store.get(assertstate_key, None)
     if assertstate:
         if assertstate.hook is not None:
@@ -116,18 +114,17 @@ def pytest_collection(session: "Session") -> None:
 
 @hookimpl(tryfirst=True, hookwrapper=True)
 def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
-    """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks
+    """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
 
-    The rewrite module will use util._reprcompare if
-    it exists to use custom reporting via the
-    pytest_assertrepr_compare hook.  This sets up this custom
+    The rewrite module will use util._reprcompare if it exists to use custom
+    reporting via the pytest_assertrepr_compare hook.  This sets up this custom
     comparison for the test.
     """
 
     ihook = item.ihook
 
     def callbinrepr(op, left: object, right: object) -> Optional[str]:
-        """Call the pytest_assertrepr_compare hook and prepare the result
+        """Call the pytest_assertrepr_compare hook and prepare the result.
 
         This uses the first result from the hook and then ensures the
         following:
index e77b1b0b861bbfe067ba9e0c9014d288b95eab19..5ff57824579e0bb3072fe57cdef2349a53ea0576 100644 (file)
@@ -1,4 +1,4 @@
-"""Rewrite assertion AST to produce nice error messages"""
+"""Rewrite assertion AST to produce nice error messages."""
 import ast
 import errno
 import functools
@@ -16,6 +16,7 @@ import types
 from typing import Callable
 from typing import Dict
 from typing import IO
+from typing import Iterable
 from typing import List
 from typing import Optional
 from typing import Sequence
@@ -170,7 +171,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
         exec(co, module.__dict__)
 
     def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool:
-        """This is a fast way to get out of rewriting modules.
+        """A fast way to get out of rewriting modules.
 
         Profiling has shown that the call to PathFinder.find_spec (inside of
         the find_spec from this class) is a major slowdown, so, this method
@@ -266,13 +267,11 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
 
     def _warn_already_imported(self, name: str) -> None:
         from _pytest.warning_types import PytestAssertRewriteWarning
-        from _pytest.warnings import _issue_warning_captured
 
-        _issue_warning_captured(
+        self.config.issue_config_time_warning(
             PytestAssertRewriteWarning(
                 "Module already imported so cannot be rewritten: %s" % name
             ),
-            self.config.hook,
             stacklevel=5,
         )
 
@@ -350,7 +349,7 @@ else:
 
 
 def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]:
-    """read and rewrite *fn* and return the code object."""
+    """Read and rewrite *fn* and return the code object."""
     fn_ = fspath(fn)
     stat = os.stat(fn_)
     with open(fn_, "rb") as f:
@@ -411,7 +410,7 @@ def rewrite_asserts(
 
 
 def _saferepr(obj: object) -> str:
-    """Get a safe repr of an object for assertion error messages.
+    r"""Get a safe repr of an object for assertion error messages.
 
     The assertion formatting (util.format_explanation()) requires
     newlines to be escaped since they are a special character for it.
@@ -419,18 +418,16 @@ def _saferepr(obj: object) -> str:
     custom repr it is possible to contain one of the special escape
     sequences, especially '\n{' and '\n}' are likely to be present in
     JSON reprs.
-
     """
     return saferepr(obj).replace("\n", "\\n")
 
 
 def _format_assertmsg(obj: object) -> str:
-    """Format the custom assertion message given.
+    r"""Format the custom assertion message given.
 
     For strings this simply replaces newlines with '\n~' so that
     util.format_explanation() will preserve them instead of escaping
     newlines.  For other objects saferepr() is used first.
-
     """
     # reprlib appears to have a bug which means that if a string
     # contains a newline it gets escaped, however if an object has a
@@ -457,12 +454,9 @@ def _should_repr_global_name(obj: object) -> bool:
         return True
 
 
-def _format_boolop(explanations, is_or: bool):
+def _format_boolop(explanations: Iterable[str], is_or: bool) -> str:
     explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
-    if isinstance(explanation, str):
-        return explanation.replace("%", "%%")
-    else:
-        return explanation.replace(b"%", b"%%")
+    return explanation.replace("%", "%%")
 
 
 def _call_reprcompare(
@@ -491,8 +485,8 @@ def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None:
 
 
 def _check_if_assertion_pass_impl() -> bool:
-    """Checks if any plugins implement the pytest_assertion_pass hook
-    in order not to generate explanation unecessarily (might be expensive)"""
+    """Check if any plugins implement the pytest_assertion_pass hook
+    in order not to generate explanation unecessarily (might be expensive)."""
     return True if util._assertion_pass else False
 
 
@@ -541,7 +535,7 @@ def set_location(node, lineno, col_offset):
 
 
 def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
-    """Returns a mapping from {lineno: "assertion test expression"}"""
+    """Return a mapping from {lineno: "assertion test expression"}."""
     ret = {}  # type: Dict[int, str]
 
     depth = 0
@@ -645,7 +639,6 @@ class AssertionRewriter(ast.NodeVisitor):
 
     This state is reset on every new assert statement visited and used
     by the other visitors.
-
     """
 
     def __init__(
@@ -694,13 +687,18 @@ class AssertionRewriter(ast.NodeVisitor):
                     return
                 expect_docstring = False
             elif (
-                not isinstance(item, ast.ImportFrom)
-                or item.level > 0
-                or item.module != "__future__"
+                isinstance(item, ast.ImportFrom)
+                and item.level == 0
+                and item.module == "__future__"
             ):
-                lineno = item.lineno
+                pass
+            else:
                 break
             pos += 1
+        # Special case: for a decorated function, set the lineno to that of the
+        # first decorator, not the `def`. Issue #4984.
+        if isinstance(item, ast.FunctionDef) and item.decorator_list:
+            lineno = item.decorator_list[0].lineno
         else:
             lineno = item.lineno
         imports = [
@@ -713,7 +711,7 @@ class AssertionRewriter(ast.NodeVisitor):
             node = nodes.pop()
             for name, field in ast.iter_fields(node):
                 if isinstance(field, list):
-                    new = []  # type: List
+                    new = []  # type: List[ast.AST]
                     for i, child in enumerate(field):
                         if isinstance(child, ast.Assert):
                             # Transform assert.
@@ -770,7 +768,6 @@ class AssertionRewriter(ast.NodeVisitor):
         current formatting context, e.g. ``%(py0)s``.  The placeholder
         and expr are placed in the current format context so that it
         can be used on the next call to .pop_format_context().
-
         """
         specifier = "py" + str(next(self.variable_counter))
         self.explanation_specifiers[specifier] = expr
@@ -785,7 +782,6 @@ class AssertionRewriter(ast.NodeVisitor):
         .explanation_param().  Finally .pop_format_context() is used
         to format a string of %-formatted values as added by
         .explanation_param().
-
         """
         self.explanation_specifiers = {}  # type: Dict[str, ast.expr]
         self.stack.append(self.explanation_specifiers)
@@ -797,7 +793,6 @@ class AssertionRewriter(ast.NodeVisitor):
         the %-placeholders created by .explanation_param().  This will
         add the required code to format said string to .expl_stmts and
         return the ast.Name instance of the formatted string.
-
         """
         current = self.stack.pop()
         if self.stack:
@@ -824,7 +819,6 @@ class AssertionRewriter(ast.NodeVisitor):
         intermediate values and replace it with an if statement which
         raises an assertion error with a detailed explanation in case
         the expression is false.
-
         """
         if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
             from _pytest.warning_types import PytestAssertRewriteWarning
@@ -994,9 +988,6 @@ class AssertionRewriter(ast.NodeVisitor):
         return res, explanation
 
     def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]:
-        """
-        visit `ast.Call` nodes
-        """
         new_func, func_expl = self.visit(call.func)
         arg_expls = []
         new_args = []
@@ -1021,7 +1012,7 @@ class AssertionRewriter(ast.NodeVisitor):
         return res, outer_expl
 
     def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]:
-        # From Python 3.5, a Starred node can appear in a function call
+        # From Python 3.5, a Starred node can appear in a function call.
         res, expl = self.visit(starred.value)
         new_starred = ast.Starred(res, starred.ctx)
         return new_starred, "*" + expl
@@ -1076,8 +1067,10 @@ class AssertionRewriter(ast.NodeVisitor):
 
 
 def try_makedirs(cache_dir: Path) -> bool:
-    """Attempts to create the given directory and sub-directories exist, returns True if
-    successful or it already exists"""
+    """Attempt to create the given directory and sub-directories exist.
+
+    Returns True if successful or if it already exists.
+    """
     try:
         os.makedirs(fspath(cache_dir), exist_ok=True)
     except (FileNotFoundError, NotADirectoryError, FileExistsError):
@@ -1096,7 +1089,7 @@ def try_makedirs(cache_dir: Path) -> bool:
 
 
 def get_cache_dir(file_path: Path) -> Path:
-    """Returns the cache directory to write .pyc files for the given .py file path"""
+    """Return the cache directory to write .pyc files for the given .py file path."""
     if sys.version_info >= (3, 8) and sys.pycache_prefix:
         # given:
         #   prefix = '/tmp/pycs'
index fb2bf9c8e3572326231dd61902c4d711805d9325..c572cc7446128f235f634568437289388b41bfab 100644 (file)
@@ -1,5 +1,4 @@
-"""
-Utilities for truncating assertion output.
+"""Utilities for truncating assertion output.
 
 Current default behaviour is to truncate assertion explanations at
 ~8 terminal lines, unless running in "-vv" mode or running on CI.
@@ -19,18 +18,14 @@ USAGE_MSG = "use '-vv' to show"
 def truncate_if_required(
     explanation: List[str], item: Item, max_length: Optional[int] = None
 ) -> List[str]:
-    """
-    Truncate this assertion explanation if the given test item is eligible.
-    """
+    """Truncate this assertion explanation if the given test item is eligible."""
     if _should_truncate_item(item):
         return _truncate_explanation(explanation)
     return explanation
 
 
 def _should_truncate_item(item: Item) -> bool:
-    """
-    Whether or not this test item is eligible for truncation.
-    """
+    """Whether or not this test item is eligible for truncation."""
     verbose = item.config.option.verbose
     return verbose < 2 and not _running_on_ci()
 
@@ -46,8 +41,7 @@ def _truncate_explanation(
     max_lines: Optional[int] = None,
     max_chars: Optional[int] = None,
 ) -> List[str]:
-    """
-    Truncate given list of strings that makes up the assertion explanation.
+    """Truncate given list of strings that makes up the assertion explanation.
 
     Truncates to either 8 lines, or 640 characters - whichever the input reaches
     first. The remaining lines will be replaced by a usage message.
index 554aec77fa99130bb89426769a5f82601d1bd085..e80e476c84a6d7a21443c274f81896825e5180fc 100644 (file)
@@ -1,4 +1,4 @@
-"""Utilities for assertion debugging"""
+"""Utilities for assertion debugging."""
 import collections.abc
 import pprint
 from typing import AbstractSet
@@ -30,7 +30,7 @@ _assertion_pass = None  # type: Optional[Callable[[int, str, str], None]]
 
 
 def format_explanation(explanation: str) -> str:
-    """This formats an explanation
+    r"""Format an explanation.
 
     Normally all embedded newlines are escaped, however there are
     three exceptions: \n{, \n} and \n~.  The first two are intended
@@ -45,7 +45,7 @@ def format_explanation(explanation: str) -> str:
 
 
 def _split_explanation(explanation: str) -> List[str]:
-    """Return a list of individual lines in the explanation
+    r"""Return a list of individual lines in the explanation.
 
     This will return a list of lines split on '\n{', '\n}' and '\n~'.
     Any other newlines will be escaped and appear in the line as the
@@ -62,11 +62,11 @@ def _split_explanation(explanation: str) -> List[str]:
 
 
 def _format_lines(lines: Sequence[str]) -> List[str]:
-    """Format the individual lines
+    """Format the individual lines.
 
-    This will replace the '{', '}' and '~' characters of our mini
-    formatting language with the proper 'where ...', 'and ...' and ' +
-    ...' text, taking care of indentation along the way.
+    This will replace the '{', '}' and '~' characters of our mini formatting
+    language with the proper 'where ...', 'and ...' and ' + ...' text, taking
+    care of indentation along the way.
 
     Return a list of formatted lines.
     """
@@ -129,7 +129,7 @@ def isiterable(obj: Any) -> bool:
 
 
 def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
-    """Return specialised explanations for some operators/operands"""
+    """Return specialised explanations for some operators/operands."""
     verbose = config.getoption("verbose")
     if verbose > 1:
         left_repr = safeformat(left)
index de7ee914980ee6de74677adc393551eabc49b5d7..b04305ed9d256f848d8a449d7e05f38d112a028b 100755 (executable)
@@ -1,9 +1,6 @@
-"""
-merged implementation of the cache provider
-
-the name cache was not chosen to ensure pluggy automatically
-ignores the external pytest-cache
-"""
+"""Implementation of the cache provider."""
+# This plugin was not named "cache" to avoid conflicts with the external
+# pytest-cache version.
 import json
 import os
 from typing import Dict
@@ -24,6 +21,7 @@ from .pathlib import rm_rf
 from .reports import CollectReport
 from _pytest import nodes
 from _pytest._io import TerminalWriter
+from _pytest.compat import final
 from _pytest.compat import order_preserving_dict
 from _pytest.config import Config
 from _pytest.config import ExitCode
@@ -53,6 +51,7 @@ Signature: 8a477f597d28d172789f06886806bc55
 """
 
 
+@final
 @attr.s
 class Cache:
     _cachedir = attr.ib(type=Path, repr=False)
@@ -73,7 +72,7 @@ class Cache:
 
     @classmethod
     def clear_cache(cls, cachedir: Path) -> None:
-        """Clears the sub-directories used to hold cached directories and values."""
+        """Clear the sub-directories used to hold cached directories and values."""
         for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
             d = cachedir / prefix
             if d.is_dir():
@@ -81,7 +80,7 @@ class Cache:
 
     @staticmethod
     def cache_dir_from_config(config: Config) -> Path:
-        return resolve_from_str(config.getini("cache_dir"), config.rootdir)
+        return resolve_from_str(config.getini("cache_dir"), config.rootpath)
 
     def warn(self, fmt: str, **args: object) -> None:
         import warnings
@@ -94,14 +93,16 @@ class Cache:
         )
 
     def makedir(self, name: str) -> py.path.local:
-        """ return a directory path object with the given name.  If the
-        directory does not yet exist, it will be created.  You can use it
-        to manage files likes e. g. store/retrieve database
-        dumps across test sessions.
-
-        :param name: must be a string not containing a ``/`` separator.
-             Make sure the name contains your plugin or application
-             identifiers to prevent clashes with other cache users.
+        """Return a directory path object with the given name.
+
+        If the directory does not yet exist, it will be created. You can use
+        it to manage files to e.g. store/retrieve database dumps across test
+        sessions.
+
+        :param name:
+            Must be a string not containing a ``/`` separator.
+            Make sure the name contains your plugin or application
+            identifiers to prevent clashes with other cache users.
         """
         path = Path(name)
         if len(path.parts) > 1:
@@ -114,15 +115,16 @@ class Cache:
         return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
 
     def get(self, key: str, default):
-        """ return cached value for the given key.  If no value
-        was yet cached or the value cannot be read, the specified
-        default is returned.
+        """Return the cached value for the given key.
 
-        :param key: must be a ``/`` separated value. Usually the first
-             name is the name of your plugin or your application.
-        :param default: must be provided in case of a cache-miss or
-             invalid cache values.
+        If no value was yet cached or the value cannot be read, the specified
+        default is returned.
 
+        :param key:
+            Must be a ``/`` separated value. Usually the first
+            name is the name of your plugin or your application.
+        :param default:
+            The value to return in case of a cache-miss or invalid cache value.
         """
         path = self._getvaluepath(key)
         try:
@@ -132,13 +134,14 @@ class Cache:
             return default
 
     def set(self, key: str, value: object) -> None:
-        """ save value for the given key.
-
-        :param key: must be a ``/`` separated value. Usually the first
-             name is the name of your plugin or your application.
-        :param value: must be of any combination of basic
-               python types, including nested types
-               like e. g. lists of dictionaries.
+        """Save value for the given key.
+
+        :param key:
+            Must be a ``/`` separated value. Usually the first
+            name is the name of your plugin or your application.
+        :param value:
+            Must be of any combination of basic python types,
+            including nested types like lists of dictionaries.
         """
         path = self._getvaluepath(key)
         try:
@@ -180,7 +183,7 @@ class LFPluginCollWrapper:
         self._collected_at_least_one_failure = False
 
     @pytest.hookimpl(hookwrapper=True)
-    def pytest_make_collect_report(self, collector: nodes.Collector) -> Generator:
+    def pytest_make_collect_report(self, collector: nodes.Collector):
         if isinstance(collector, Session):
             out = yield
             res = out.get_result()  # type: CollectReport
@@ -241,7 +244,7 @@ class LFPluginCollSkipfiles:
 
 
 class LFPlugin:
-    """ Plugin which implements the --lf (run last-failing) option """
+    """Plugin which implements the --lf (run last-failing) option."""
 
     def __init__(self, config: Config) -> None:
         self.config = config
@@ -262,8 +265,8 @@ class LFPlugin:
             )
 
     def get_last_failed_paths(self) -> Set[Path]:
-        """Returns a set with all Paths()s of the previously failed nodeids."""
-        rootpath = Path(str(self.config.rootdir))
+        """Return a set with all Paths()s of the previously failed nodeids."""
+        rootpath = self.config.rootpath
         result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
         return {x for x in result if x.exists()}
 
@@ -351,7 +354,7 @@ class LFPlugin:
 
 
 class NFPlugin:
-    """ Plugin which implements the --nf (run new-first) option """
+    """Plugin which implements the --nf (run new-first) option."""
 
     def __init__(self, config: Config) -> None:
         self.config = config
@@ -471,13 +474,12 @@ def pytest_configure(config: Config) -> None:
 
 @pytest.fixture
 def cache(request: FixtureRequest) -> Cache:
-    """
-    Return a cache object that can persist state between testing sessions.
+    """Return a cache object that can persist state between testing sessions.
 
     cache.get(key, default)
     cache.set(key, value)
 
-    Keys must be a ``/`` separated value, where the first part is usually the
+    Keys must be ``/`` separated strings, where the first part is usually the
     name of your plugin or application to avoid clashes with other cache users.
 
     Values can be any object handled by the json stdlib module.
@@ -495,7 +497,7 @@ def pytest_report_header(config: Config) -> Optional[str]:
         # starting with .., ../.. if sensible
 
         try:
-            displaypath = cachedir.relative_to(str(config.rootdir))
+            displaypath = cachedir.relative_to(config.rootpath)
         except ValueError:
             displaypath = cachedir
         return "cachedir: {}".format(displaypath)
index a20d14fe7e330635e684a6e0c3a900e3e28f2cea..2d2b392aba8f509665ada48a3c2ded0dea609c70 100644 (file)
@@ -1,21 +1,23 @@
-"""
-per-test stdout/stderr capturing mechanism.
-
-"""
-import collections
+"""Per-test stdout/stderr capturing mechanism."""
 import contextlib
+import functools
 import io
 import os
 import sys
 from io import UnsupportedOperation
 from tempfile import TemporaryFile
+from typing import Any
+from typing import AnyStr
 from typing import Generator
+from typing import Generic
+from typing import Iterator
 from typing import Optional
 from typing import TextIO
 from typing import Tuple
 from typing import Union
 
 import pytest
+from _pytest.compat import final
 from _pytest.compat import TYPE_CHECKING
 from _pytest.config import Config
 from _pytest.config.argparsing import Parser
@@ -49,8 +51,7 @@ def pytest_addoption(parser: Parser) -> None:
 
 
 def _colorama_workaround() -> None:
-    """
-    Ensure colorama is imported so that it attaches to the correct stdio
+    """Ensure colorama is imported so that it attaches to the correct stdio
     handles on Windows.
 
     colorama uses the terminal on import time. So if something does the
@@ -65,8 +66,7 @@ def _colorama_workaround() -> None:
 
 
 def _readline_workaround() -> None:
-    """
-    Ensure readline is imported so that it attaches to the correct stdio
+    """Ensure readline is imported so that it attaches to the correct stdio
     handles on Windows.
 
     Pdb uses readline support where available--when not running from the Python
@@ -80,7 +80,7 @@ def _readline_workaround() -> None:
     workaround ensures that readline is imported before I/O capture is setup so
     that it can attach to the actual stdin/out for the console.
 
-    See https://github.com/pytest-dev/pytest/pull/1281
+    See https://github.com/pytest-dev/pytest/pull/1281.
     """
     if sys.platform.startswith("win32"):
         try:
@@ -90,8 +90,9 @@ def _readline_workaround() -> None:
 
 
 def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
-    """
-    Python 3.6 implemented unicode console handling for Windows. This works
+    """Workaround for Windows Unicode console handling on Python>=3.6.
+
+    Python 3.6 implemented Unicode console handling for Windows. This works
     by reading/writing to the raw console handle using
     ``{Read,Write}ConsoleW``.
 
@@ -106,10 +107,11 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
     also means a different handle by replicating the logic in
     "Py_lifecycle.c:initstdio/create_stdio".
 
-    :param stream: in practice ``sys.stdout`` or ``sys.stderr``, but given
+    :param stream:
+        In practice ``sys.stdout`` or ``sys.stderr``, but given
         here as parameter for unittesting purposes.
 
-    See https://github.com/pytest-dev/py/issues/103
+    See https://github.com/pytest-dev/py/issues/103.
     """
     if (
         not sys.platform.startswith("win32")
@@ -118,8 +120,8 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
     ):
         return
 
-    # bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666)
-    if not hasattr(stream, "buffer"):
+    # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
+    if not hasattr(stream, "buffer"):  # type: ignore[unreachable]
         return
 
     buffered = hasattr(stream.buffer, "raw")
@@ -158,10 +160,10 @@ def pytest_load_initial_conftests(early_config: Config):
     capman = CaptureManager(ns.capture)
     pluginmanager.register(capman, "capturemanager")
 
-    # make sure that capturemanager is properly reset at final shutdown
+    # Make sure that capturemanager is properly reset at final shutdown.
     early_config.add_cleanup(capman.stop_global_capturing)
 
-    # finally trigger conftest loading but while capturing (issue93)
+    # Finally trigger conftest loading but while capturing (issue #93).
     capman.start_global_capturing()
     outcome = yield
     capman.suspend_global_capture()
@@ -347,9 +349,9 @@ class SysCapture(SysCaptureBinary):
 
 
 class FDCaptureBinary:
-    """Capture IO to/from a given os-level filedescriptor.
+    """Capture IO to/from a given OS-level file descriptor.
 
-    snap() produces `bytes`
+    snap() produces `bytes`.
     """
 
     EMPTY_BUFFER = b""
@@ -415,7 +417,7 @@ class FDCaptureBinary:
         )
 
     def start(self) -> None:
-        """ Start capturing on targetfd using memorized tmpfile. """
+        """Start capturing on targetfd using memorized tmpfile."""
         self._assert_state("start", ("initialized",))
         os.dup2(self.tmpfile.fileno(), self.targetfd)
         self.syscapture.start()
@@ -430,8 +432,8 @@ class FDCaptureBinary:
         return res
 
     def done(self) -> None:
-        """ stop capturing, restore streams, return original capture file,
-        seeked to position zero. """
+        """Stop capturing, restore streams, return original capture file,
+        seeked to position zero."""
         self._assert_state("done", ("initialized", "started", "suspended", "done"))
         if self._state == "done":
             return
@@ -462,15 +464,15 @@ class FDCaptureBinary:
         self._state = "started"
 
     def writeorg(self, data):
-        """ write to original file descriptor. """
+        """Write to original file descriptor."""
         self._assert_state("writeorg", ("started", "suspended"))
         os.write(self.targetfd_save, data)
 
 
 class FDCapture(FDCaptureBinary):
-    """Capture IO to/from a given os-level filedescriptor.
+    """Capture IO to/from a given OS-level file descriptor.
 
-    snap() produces text
+    snap() produces text.
     """
 
     # Ignore type because it doesn't match the type in the superclass (bytes).
@@ -485,16 +487,71 @@ class FDCapture(FDCaptureBinary):
         return res
 
     def writeorg(self, data):
-        """ write to original file descriptor. """
+        """Write to original file descriptor."""
         super().writeorg(data.encode("utf-8"))  # XXX use encoding of original stream
 
 
 # MultiCapture
 
-CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"])
 
+# This class was a namedtuple, but due to mypy limitation[0] it could not be
+# made generic, so was replaced by a regular class which tries to emulate the
+# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
+# make it a namedtuple again.
+# [0]: https://github.com/python/mypy/issues/685
+@final
+@functools.total_ordering
+class CaptureResult(Generic[AnyStr]):
+    """The result of :method:`CaptureFixture.readouterr`."""
+
+    # Can't use slots in Python<3.5.3 due to https://bugs.python.org/issue31272
+    if sys.version_info >= (3, 5, 3):
+        __slots__ = ("out", "err")
+
+    def __init__(self, out: AnyStr, err: AnyStr) -> None:
+        self.out = out  # type: AnyStr
+        self.err = err  # type: AnyStr
+
+    def __len__(self) -> int:
+        return 2
+
+    def __iter__(self) -> Iterator[AnyStr]:
+        return iter((self.out, self.err))
+
+    def __getitem__(self, item: int) -> AnyStr:
+        return tuple(self)[item]
+
+    def _replace(
+        self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None
+    ) -> "CaptureResult[AnyStr]":
+        return CaptureResult(
+            out=self.out if out is None else out, err=self.err if err is None else err
+        )
+
+    def count(self, value: AnyStr) -> int:
+        return tuple(self).count(value)
+
+    def index(self, value) -> int:
+        return tuple(self).index(value)
 
-class MultiCapture:
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, (CaptureResult, tuple)):
+            return NotImplemented
+        return tuple(self) == tuple(other)
+
+    def __hash__(self) -> int:
+        return hash(tuple(self))
+
+    def __lt__(self, other: object) -> bool:
+        if not isinstance(other, (CaptureResult, tuple)):
+            return NotImplemented
+        return tuple(self) < tuple(other)
+
+    def __repr__(self) -> str:
+        return "CaptureResult(out={!r}, err={!r})".format(self.out, self.err)
+
+
+class MultiCapture(Generic[AnyStr]):
     _state = None
     _in_suspended = False
 
@@ -517,8 +574,8 @@ class MultiCapture:
         if self.err:
             self.err.start()
 
-    def pop_outerr_to_orig(self):
-        """ pop current snapshot out/err capture and flush to orig streams. """
+    def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]:
+        """Pop current snapshot out/err capture and flush to orig streams."""
         out, err = self.readouterr()
         if out:
             self.out.writeorg(out)
@@ -547,7 +604,7 @@ class MultiCapture:
             self._in_suspended = False
 
     def stop_capturing(self) -> None:
-        """ stop capturing and reset capturing streams """
+        """Stop capturing and reset capturing streams."""
         if self._state == "stopped":
             raise ValueError("was already stopped")
         self._state = "stopped"
@@ -562,7 +619,7 @@ class MultiCapture:
         """Whether actively capturing -- not suspended or stopped."""
         return self._state == "started"
 
-    def readouterr(self) -> CaptureResult:
+    def readouterr(self) -> CaptureResult[AnyStr]:
         if self.out:
             out = self.out.snap()
         else:
@@ -574,7 +631,7 @@ class MultiCapture:
         return CaptureResult(out, err)
 
 
-def _get_multicapture(method: "_CaptureMethod") -> MultiCapture:
+def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
     if method == "fd":
         return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
     elif method == "sys":
@@ -592,22 +649,28 @@ def _get_multicapture(method: "_CaptureMethod") -> MultiCapture:
 
 
 class CaptureManager:
-    """
-    Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each
-    test phase (setup, call, teardown). After each of those points, the captured output is obtained and
-    attached to the collection/runtest report.
+    """The capture plugin.
+
+    Manages that the appropriate capture method is enabled/disabled during
+    collection and each test phase (setup, call, teardown). After each of
+    those points, the captured output is obtained and attached to the
+    collection/runtest report.
 
     There are two levels of capture:
-    * global: which is enabled by default and can be suppressed by the ``-s`` option. This is always enabled/disabled
-      during collection and each test phase.
-    * fixture: when a test function or one of its fixture depend on the ``capsys`` or ``capfd`` fixtures. In this
-      case special handling is needed to ensure the fixtures take precedence over the global capture.
+
+    * global: enabled by default and can be suppressed by the ``-s``
+      option. This is always enabled/disabled during collection and each test
+      phase.
+
+    * fixture: when a test function or one of its fixture depend on the
+      ``capsys`` or ``capfd`` fixtures. In this case special handling is
+      needed to ensure the fixtures take precedence over the global capture.
     """
 
     def __init__(self, method: "_CaptureMethod") -> None:
         self._method = method
-        self._global_capturing = None  # type: Optional[MultiCapture]
-        self._capture_fixture = None  # type: Optional[CaptureFixture]
+        self._global_capturing = None  # type: Optional[MultiCapture[str]]
+        self._capture_fixture = None  # type: Optional[CaptureFixture[Any]]
 
     def __repr__(self) -> str:
         return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
@@ -656,13 +719,13 @@ class CaptureManager:
         self.resume_global_capture()
         self.resume_fixture()
 
-    def read_global_capture(self):
+    def read_global_capture(self) -> CaptureResult[str]:
         assert self._global_capturing is not None
         return self._global_capturing.readouterr()
 
     # Fixture Control
 
-    def set_fixture(self, capture_fixture: "CaptureFixture") -> None:
+    def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None:
         if self._capture_fixture:
             current_fixture = self._capture_fixture.request.fixturename
             requested_fixture = capture_fixture.request.fixturename
@@ -677,14 +740,13 @@ class CaptureManager:
         self._capture_fixture = None
 
     def activate_fixture(self) -> None:
-        """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
-        the global capture.
-        """
+        """If the current item is using ``capsys`` or ``capfd``, activate
+        them so they take precedence over the global capture."""
         if self._capture_fixture:
             self._capture_fixture._start()
 
     def deactivate_fixture(self) -> None:
-        """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
+        """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any."""
         if self._capture_fixture:
             self._capture_fixture.close()
 
@@ -770,16 +832,14 @@ class CaptureManager:
         self.stop_global_capturing()
 
 
-class CaptureFixture:
-    """
-    Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary`
-    fixtures.
-    """
+class CaptureFixture(Generic[AnyStr]):
+    """Object returned by the :py:func:`capsys`, :py:func:`capsysbinary`,
+    :py:func:`capfd` and :py:func:`capfdbinary` fixtures."""
 
     def __init__(self, captureclass, request: SubRequest) -> None:
         self.captureclass = captureclass
         self.request = request
-        self._capture = None  # type: Optional[MultiCapture]
+        self._capture = None  # type: Optional[MultiCapture[AnyStr]]
         self._captured_out = self.captureclass.EMPTY_BUFFER
         self._captured_err = self.captureclass.EMPTY_BUFFER
 
@@ -798,10 +858,13 @@ class CaptureFixture:
             self._capture.stop_capturing()
             self._capture = None
 
-    def readouterr(self):
-        """Read and return the captured output so far, resetting the internal buffer.
+    def readouterr(self) -> CaptureResult[AnyStr]:
+        """Read and return the captured output so far, resetting the internal
+        buffer.
 
-        :return: captured content as a namedtuple with ``out`` and ``err`` string attributes
+        :returns:
+            The captured content as a namedtuple with ``out`` and ``err``
+            string attributes.
         """
         captured_out, captured_err = self._captured_out, self._captured_err
         if self._capture is not None:
@@ -813,12 +876,12 @@ class CaptureFixture:
         return CaptureResult(captured_out, captured_err)
 
     def _suspend(self) -> None:
-        """Suspends this fixture's own capturing temporarily."""
+        """Suspend this fixture's own capturing temporarily."""
         if self._capture is not None:
             self._capture.suspend_capturing()
 
     def _resume(self) -> None:
-        """Resumes this fixture's own capturing temporarily."""
+        """Resume this fixture's own capturing temporarily."""
         if self._capture is not None:
             self._capture.resume_capturing()
 
@@ -830,7 +893,7 @@ class CaptureFixture:
 
     @contextlib.contextmanager
     def disabled(self) -> Generator[None, None, None]:
-        """Temporarily disables capture while inside the 'with' block."""
+        """Temporarily disable capturing while inside the ``with`` block."""
         capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
         with capmanager.global_and_fixture_disabled():
             yield
@@ -840,7 +903,7 @@ class CaptureFixture:
 
 
 @pytest.fixture
-def capsys(request: SubRequest) -> Generator[CaptureFixture, None, None]:
+def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
     """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
 
     The captured output is made available via ``capsys.readouterr()`` method
@@ -848,7 +911,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture, None, None]:
     ``out`` and ``err`` will be ``text`` objects.
     """
     capman = request.config.pluginmanager.getplugin("capturemanager")
-    capture_fixture = CaptureFixture(SysCapture, request)
+    capture_fixture = CaptureFixture[str](SysCapture, request)
     capman.set_fixture(capture_fixture)
     capture_fixture._start()
     yield capture_fixture
@@ -857,7 +920,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture, None, None]:
 
 
 @pytest.fixture
-def capsysbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]:
+def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
     """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
 
     The captured output is made available via ``capsysbinary.readouterr()``
@@ -865,7 +928,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]:
     ``out`` and ``err`` will be ``bytes`` objects.
     """
     capman = request.config.pluginmanager.getplugin("capturemanager")
-    capture_fixture = CaptureFixture(SysCaptureBinary, request)
+    capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request)
     capman.set_fixture(capture_fixture)
     capture_fixture._start()
     yield capture_fixture
@@ -874,7 +937,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]:
 
 
 @pytest.fixture
-def capfd(request: SubRequest) -> Generator[CaptureFixture, None, None]:
+def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
     """Enable text capturing of writes to file descriptors ``1`` and ``2``.
 
     The captured output is made available via ``capfd.readouterr()`` method
@@ -882,7 +945,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture, None, None]:
     ``out`` and ``err`` will be ``text`` objects.
     """
     capman = request.config.pluginmanager.getplugin("capturemanager")
-    capture_fixture = CaptureFixture(FDCapture, request)
+    capture_fixture = CaptureFixture[str](FDCapture, request)
     capman.set_fixture(capture_fixture)
     capture_fixture._start()
     yield capture_fixture
@@ -891,7 +954,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture, None, None]:
 
 
 @pytest.fixture
-def capfdbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]:
+def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
     """Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
 
     The captured output is made available via ``capfd.readouterr()`` method
@@ -899,7 +962,7 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]:
     ``out`` and ``err`` will be ``byte`` objects.
     """
     capman = request.config.pluginmanager.getplugin("capturemanager")
-    capture_fixture = CaptureFixture(FDCaptureBinary, request)
+    capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request)
     capman.set_fixture(capture_fixture)
     capture_fixture._start()
     yield capture_fixture
index cd7dca7197afca98b2e2378cf9e38f604e990a80..7eab2ea0c85665478eae2c2109105b12ed7dba9b 100644 (file)
@@ -1,6 +1,4 @@
-"""
-python version compatibility code
-"""
+"""Python version compatibility code."""
 import enum
 import functools
 import inspect
@@ -14,15 +12,13 @@ from typing import Any
 from typing import Callable
 from typing import Generic
 from typing import Optional
-from typing import overload
+from typing import overload as overload
 from typing import Tuple
 from typing import TypeVar
 from typing import Union
 
 import attr
-import py
 
-from _pytest._io.saferepr import saferepr
 from _pytest.outcomes import fail
 from _pytest.outcomes import TEST_OUTCOME
 
@@ -73,8 +69,7 @@ if sys.version_info < (3, 6):
 
     def fspath(p):
         """os.fspath replacement, useful to point out when we should replace it by the
-        real function once we drop py35.
-        """
+        real function once we drop py35."""
         return str(p)
 
 
@@ -88,8 +83,7 @@ def is_generator(func: object) -> bool:
 
 
 def iscoroutinefunction(func: object) -> bool:
-    """
-    Return True if func is a coroutine function (a function defined with async
+    """Return True if func is a coroutine function (a function defined with async
     def syntax, and doesn't contain yield), or a function decorated with
     @asyncio.coroutine.
 
@@ -101,25 +95,31 @@ def iscoroutinefunction(func: object) -> bool:
 
 
 def is_async_function(func: object) -> bool:
-    """Return True if the given function seems to be an async function or async generator"""
+    """Return True if the given function seems to be an async function or
+    an async generator."""
     return iscoroutinefunction(func) or (
         sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func)
     )
 
 
-def getlocation(function, curdir=None) -> str:
+def getlocation(function, curdir: Optional[str] = None) -> str:
+    from _pytest.pathlib import Path
+
     function = get_real_func(function)
-    fn = py.path.local(inspect.getfile(function))
+    fn = Path(inspect.getfile(function))
     lineno = function.__code__.co_firstlineno
     if curdir is not None:
-        relfn = fn.relto(curdir)
-        if relfn:
+        try:
+            relfn = fn.relative_to(curdir)
+        except ValueError:
+            pass
+        else:
             return "%s:%d" % (relfn, lineno + 1)
     return "%s:%d" % (fn, lineno + 1)
 
 
 def num_mock_patch_args(function) -> int:
-    """ return number of arguments used up by mock arguments (if any) """
+    """Return number of arguments used up by mock arguments (if any)."""
     patchings = getattr(function, "patchings", None)
     if not patchings:
         return 0
@@ -144,13 +144,13 @@ def getfuncargnames(
     is_method: bool = False,
     cls: Optional[type] = None
 ) -> Tuple[str, ...]:
-    """Returns the names of a function's mandatory arguments.
+    """Return the names of a function's mandatory arguments.
 
-    This should return the names of all function arguments that:
-        * Aren't bound to an instance or type as in instance or class methods.
-        * Don't have default values.
-        * Aren't bound with functools.partial.
-        * Aren't replaced with mocks.
+    Should return the names of all function arguments that:
+    * Aren't bound to an instance or type as in instance or class methods.
+    * Don't have default values.
+    * Aren't bound with functools.partial.
+    * Aren't replaced with mocks.
 
     The is_method and cls arguments indicate that the function should
     be treated as a bound method even though it's not unless, only in
@@ -179,8 +179,9 @@ def getfuncargnames(
         p.name
         for p in parameters.values()
         if (
-            p.kind is Parameter.POSITIONAL_OR_KEYWORD
-            or p.kind is Parameter.KEYWORD_ONLY
+            # TODO: Remove type ignore after https://github.com/python/typeshed/pull/4383
+            p.kind is Parameter.POSITIONAL_OR_KEYWORD  # type: ignore[unreachable]
+            or p.kind is Parameter.KEYWORD_ONLY  # type: ignore[unreachable]
         )
         and p.default is Parameter.empty
     )
@@ -208,12 +209,13 @@ if sys.version_info < (3, 7):
 
 
 else:
-    from contextlib import nullcontext  # noqa
+    from contextlib import nullcontext as nullcontext  # noqa: F401
 
 
 def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
-    # Note: this code intentionally mirrors the code at the beginning of getfuncargnames,
-    # to get the arguments which were excluded from its result because they had default values
+    # Note: this code intentionally mirrors the code at the beginning of
+    # getfuncargnames, to get the arguments which were excluded from its result
+    # because they had default values.
     return tuple(
         p.name
         for p in signature(function).parameters.values()
@@ -242,22 +244,21 @@ def _bytes_to_ascii(val: bytes) -> str:
 
 
 def ascii_escaped(val: Union[bytes, str]) -> str:
-    """If val is pure ascii, returns it as a str().  Otherwise, escapes
+    r"""If val is pure ASCII, return it as an str, otherwise, escape
     bytes objects into a sequence of escaped bytes:
 
-    b'\xc3\xb4\xc5\xd6' -> '\\xc3\\xb4\\xc5\\xd6'
+    b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
 
     and escapes unicode objects into a sequence of escaped unicode
     ids, e.g.:
 
-    '4\\nV\\U00043efa\\x0eMXWB\\x1e\\u3028\\u15fd\\xcd\\U0007d944'
+    r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
 
-    note:
-       the obvious "v.decode('unicode-escape')" will return
-       valid utf-8 unicode if it finds them in bytes, but we
+    Note:
+       The obvious "v.decode('unicode-escape')" will return
+       valid UTF-8 unicode if it finds them in bytes, but we
        want to return escaped bytes for any byte, even if they match
-       a utf-8 string.
-
+       a UTF-8 string.
     """
     if isinstance(val, bytes):
         ret = _bytes_to_ascii(val)
@@ -270,18 +271,17 @@ def ascii_escaped(val: Union[bytes, str]) -> str:
 class _PytestWrapper:
     """Dummy wrapper around a function object for internal use only.
 
-    Used to correctly unwrap the underlying function object
-    when we are creating fixtures, because we wrap the function object ourselves with a decorator
-    to issue warnings when the fixture function is called directly.
+    Used to correctly unwrap the underlying function object when we are
+    creating fixtures, because we wrap the function object ourselves with a
+    decorator to issue warnings when the fixture function is called directly.
     """
 
     obj = attr.ib()
 
 
 def get_real_func(obj):
-    """ gets the real function object of the (possibly) wrapped object by
-    functools.wraps or functools.partial.
-    """
+    """Get the real function object of the (possibly) wrapped object by
+    functools.wraps or functools.partial."""
     start_obj = obj
     for i in range(100):
         # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function
@@ -296,6 +296,8 @@ def get_real_func(obj):
             break
         obj = new_obj
     else:
+        from _pytest._io.saferepr import saferepr
+
         raise ValueError(
             ("could not find real function of {start}\nstopped at {current}").format(
                 start=saferepr(start_obj), current=saferepr(obj)
@@ -307,10 +309,9 @@ def get_real_func(obj):
 
 
 def get_real_method(obj, holder):
-    """
-    Attempts to obtain the real function object that might be wrapping ``obj``, while at the same time
-    returning a bound method to ``holder`` if the original object was a bound method.
-    """
+    """Attempt to obtain the real function object that might be wrapping
+    ``obj``, while at the same time returning a bound method to ``holder`` if
+    the original object was a bound method."""
     try:
         is_method = hasattr(obj, "__func__")
         obj = get_real_func(obj)
@@ -329,12 +330,13 @@ def getimfunc(func):
 
 
 def safe_getattr(object: Any, name: str, default: Any) -> Any:
-    """ Like getattr but return default upon any Exception or any OutcomeException.
+    """Like getattr but return default upon any Exception or any OutcomeException.
 
     Attribute access can potentially fail for 'evil' Python objects.
     See issue #214.
-    It catches OutcomeException because of #2490 (issue #580), new outcomes are derived from BaseException
-    instead of Exception (for more details check #2707)
+    It catches OutcomeException because of #2490 (issue #580), new outcomes
+    are derived from BaseException instead of Exception (for more details
+    check #2707).
     """
     try:
         return getattr(object, name, default)
@@ -356,6 +358,19 @@ if sys.version_info < (3, 5, 2):
         return f
 
 
+if TYPE_CHECKING:
+    if sys.version_info >= (3, 8):
+        from typing import final as final
+    else:
+        from typing_extensions import final as final
+elif sys.version_info >= (3, 8):
+    from typing import final as final
+else:
+
+    def final(f):  # noqa: F811
+        return f
+
+
 if getattr(attr, "__version_info__", ()) >= (19, 2):
     ATTRS_EQ_FIELD = "eq"
 else:
@@ -363,7 +378,7 @@ else:
 
 
 if sys.version_info >= (3, 8):
-    from functools import cached_property
+    from functools import cached_property as cached_property
 else:
 
     class cached_property(Generic[_S, _T]):
@@ -377,13 +392,13 @@ else:
         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:
@@ -427,7 +442,7 @@ else:
 #
 # With `assert_never` we can do better:
 #
-#     // throw new Error('unreachable');
+#     // raise Exception('unreachable')
 #     return assert_never(x)
 #
 # Now, if we forget to handle the new variant, the type-checker will emit a
index f4e0d5d0fab23444043a8703e55af3495c2169de..f89ed37027b4436d8b58aa0a81d26fefda76e4a6 100644 (file)
@@ -1,4 +1,4 @@
-""" command line options, ini-file and conftest.py processing. """
+"""Command line options, ini-file and conftest.py processing."""
 import argparse
 import collections.abc
 import contextlib
@@ -6,6 +6,7 @@ import copy
 import enum
 import inspect
 import os
+import re
 import shlex
 import sys
 import types
@@ -15,6 +16,7 @@ from types import TracebackType
 from typing import Any
 from typing import Callable
 from typing import Dict
+from typing import Generator
 from typing import IO
 from typing import Iterable
 from typing import Iterator
@@ -34,17 +36,19 @@ from pluggy import PluginManager
 
 import _pytest._code
 import _pytest.deprecated
-import _pytest.hookspec  # the extension point definitions
-from .exceptions import PrintHelp
-from .exceptions import UsageError
+import _pytest.hookspec
+from .exceptions import PrintHelp as PrintHelp
+from .exceptions import UsageError as UsageError
 from .findpaths import determine_setup
 from _pytest._code import ExceptionInfo
 from _pytest._code import filter_traceback
 from _pytest._io import TerminalWriter
+from _pytest.compat import final
 from _pytest.compat import importlib_metadata
 from _pytest.compat import TYPE_CHECKING
 from _pytest.outcomes import fail
 from _pytest.outcomes import Skipped
+from _pytest.pathlib import bestrelpath
 from _pytest.pathlib import import_path
 from _pytest.pathlib import ImportMode
 from _pytest.pathlib import Path
@@ -61,35 +65,38 @@ if TYPE_CHECKING:
 
 _PluggyPlugin = object
 """A type to represent plugin objects.
+
 Plugins can be any namespace, so we can't narrow it down much, but we use an
 alias to make the intent clear.
-Ideally this type would be provided by pluggy itself."""
+
+Ideally this type would be provided by pluggy itself.
+"""
 
 
 hookimpl = HookimplMarker("pytest")
 hookspec = HookspecMarker("pytest")
 
 
+@final
 class ExitCode(enum.IntEnum):
-    """
-    .. versionadded:: 5.0
-
-    Encodes the valid exit codes by pytest.
+    """Encodes the valid exit codes by pytest.
 
     Currently users and plugins may supply other exit codes as well.
+
+    .. versionadded:: 5.0
     """
 
-    #: tests passed
+    #: Tests passed.
     OK = 0
-    #: tests failed
+    #: Tests failed.
     TESTS_FAILED = 1
-    #: pytest was interrupted
+    #: pytest was interrupted.
     INTERRUPTED = 2
-    #: an internal error got in the way
+    #: An internal error got in the way.
     INTERNAL_ERROR = 3
-    #: pytest was misused
+    #: pytest was misused.
     USAGE_ERROR = 4
-    #: pytest couldn't find tests
+    #: pytest couldn't find tests.
     NO_TESTS_COLLECTED = 5
 
 
@@ -112,7 +119,7 @@ class ConftestImportFailure(Exception):
 def filter_traceback_for_conftest_import_failure(
     entry: _pytest._code.TracebackEntry,
 ) -> bool:
-    """filters tracebacks entries which point to pytest internals or importlib.
+    """Filter tracebacks entries which point to pytest internals or importlib.
 
     Make a special case for importlib because we use it to import test modules and conftest files
     in _pytest.pathlib.import_path.
@@ -121,15 +128,15 @@ def filter_traceback_for_conftest_import_failure(
 
 
 def main(
-    args: Optional[List[str]] = None,
+    args: Optional[Union[List[str], py.path.local]] = None,
     plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
 ) -> Union[int, ExitCode]:
-    """ return exit code, after performing an in-process test run.
+    """Perform an in-process test run.
 
-    :arg args: list of command line arguments.
+    :param args: List of command line arguments.
+    :param plugins: List of plugin objects to be auto-registered during initialization.
 
-    :arg plugins: list of plugin objects to be auto-registered during
-                  initialization.
+    :returns: An exit code.
     """
     try:
         try:
@@ -171,7 +178,7 @@ def main(
 
 
 def console_main() -> int:
-    """pytest's CLI entry point.
+    """The CLI entry point of pytest.
 
     This function is not meant for programmable use; use `main()` instead.
     """
@@ -193,10 +200,10 @@ class cmdline:  # compatibility namespace
 
 
 def filename_arg(path: str, optname: str) -> str:
-    """ Argparse type validator for filename arguments.
+    """Argparse type validator for filename arguments.
 
-    :path: path of filename
-    :optname: name of the option
+    :path: Path of filename.
+    :optname: Name of the option.
     """
     if os.path.isdir(path):
         raise UsageError("{} must be a filename, given: {}".format(optname, path))
@@ -206,8 +213,8 @@ def filename_arg(path: str, optname: str) -> str:
 def directory_arg(path: str, optname: str) -> str:
     """Argparse type validator for directory arguments.
 
-    :path: path of directory
-    :optname: name of the option
+    :path: Path of directory.
+    :optname: Name of the option.
     """
     if not os.path.isdir(path):
         raise UsageError("{} must be a directory, given: {}".format(optname, path))
@@ -237,7 +244,6 @@ default_plugins = essential_plugins + (
     "nose",
     "assertion",
     "junitxml",
-    "resultlog",
     "doctest",
     "cacheprovider",
     "freeze_support",
@@ -278,8 +284,7 @@ def get_config(
 
 
 def get_plugin_manager() -> "PytestPluginManager":
-    """
-    Obtain a new instance of the
+    """Obtain a new instance of the
     :py:class:`_pytest.config.PytestPluginManager`, with default plugins
     already loaded.
 
@@ -319,14 +324,14 @@ def _prepareconfig(
         raise
 
 
+@final
 class PytestPluginManager(PluginManager):
-    """
-    Overwrites :py:class:`pluggy.PluginManager <pluggy.PluginManager>` to add pytest-specific
-    functionality:
+    """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
+    additional pytest-specific functionality:
 
-    * loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
-      ``pytest_plugins`` global variables found in plugins being loaded;
-    * ``conftest.py`` loading during start-up;
+    * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
+      ``pytest_plugins`` global variables found in plugins being loaded.
+    * ``conftest.py`` loading during start-up.
     """
 
     def __init__(self) -> None:
@@ -343,6 +348,13 @@ class PytestPluginManager(PluginManager):
         self._noconftest = False
         self._duplicatepaths = set()  # type: Set[py.path.local]
 
+        # plugins that were explicitly skipped with pytest.skip
+        # list of (module name, skip reason)
+        # previously we would issue a warning when a plugin was skipped, but
+        # since we refactored warnings as first citizens of Config, they are
+        # just stored here to be used later.
+        self.skipped_plugins = []  # type: List[Tuple[str, str]]
+
         self.add_hookspecs(_pytest.hookspec)
         self.register(self)
         if os.environ.get("PYTEST_DEBUG"):
@@ -359,27 +371,27 @@ class PytestPluginManager(PluginManager):
 
         # Config._consider_importhook will set a real object if required.
         self.rewrite_hook = _pytest.assertion.DummyRewriteHook()
-        # Used to know when we are importing conftests after the pytest_configure stage
+        # Used to know when we are importing conftests after the pytest_configure stage.
         self._configured = False
 
     def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
-        # pytest hooks are always prefixed with pytest_
+        # pytest hooks are always prefixed with "pytest_",
         # so we avoid accessing possibly non-readable attributes
-        # (see issue #1073)
+        # (see issue #1073).
         if not name.startswith("pytest_"):
             return
-        # ignore names which can not be hooks
+        # Ignore names which can not be hooks.
         if name == "pytest_plugins":
             return
 
         method = getattr(plugin, name)
         opts = super().parse_hookimpl_opts(plugin, name)
 
-        # consider only actual functions for hooks (#3775)
+        # Consider only actual functions for hooks (#3775).
         if not inspect.isroutine(method):
             return
 
-        # collect unmarked hooks as long as they have the `pytest_' prefix
+        # Collect unmarked hooks as long as they have the `pytest_' prefix.
         if opts is None and name.startswith("pytest_"):
             opts = {}
         if opts is not None:
@@ -432,17 +444,18 @@ class PytestPluginManager(PluginManager):
         return ret
 
     def getplugin(self, name: str):
-        # support deprecated naming because plugins (xdist e.g.) use it
+        # Support deprecated naming because plugins (xdist e.g.) use it.
         plugin = self.get_plugin(name)  # type: Optional[_PluggyPlugin]
         return plugin
 
     def hasplugin(self, name: str) -> bool:
-        """Return True if the plugin with the given name is registered."""
+        """Return whether a plugin with the given name is registered."""
         return bool(self.get_plugin(name))
 
     def pytest_configure(self, config: "Config") -> None:
+        """:meta private:"""
         # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
-        # we should remove tryfirst/trylast as markers
+        # we should remove tryfirst/trylast as markers.
         config.addinivalue_line(
             "markers",
             "tryfirst: mark a hook implementation function such that the "
@@ -456,15 +469,15 @@ class PytestPluginManager(PluginManager):
         self._configured = True
 
     #
-    # internal API for local conftest plugin handling
+    # Internal API for local conftest plugin handling.
     #
     def _set_initial_conftests(self, namespace: argparse.Namespace) -> None:
-        """ load initial conftest files given a preparsed "namespace".
-            As conftest files may add their own command line options
-            which have arguments ('--my-opt somepath') we might get some
-            false positives.  All builtin and 3rd party plugins will have
-            been loaded, however, so common options will not confuse our logic
-            here.
+        """Load initial conftest files given a preparsed "namespace".
+
+        As conftest files may add their own command line options which have
+        arguments ('--my-opt somepath') we might get some false positives.
+        All builtin and 3rd party plugins will have been loaded, however, so
+        common options will not confuse our logic here.
         """
         current = py.path.local()
         self._confcutdir = (
@@ -511,9 +524,9 @@ class PytestPluginManager(PluginManager):
         else:
             directory = path
 
-        # XXX these days we may rather want to use config.rootdir
+        # XXX these days we may rather want to use config.rootpath
         # and allow users to opt into looking into the rootdir parent
-        # directories instead of requiring to specify confcutdir
+        # directories instead of requiring to specify confcutdir.
         clist = []
         for parent in directory.parts():
             if self._confcutdir and self._confcutdir.relto(parent):
@@ -539,8 +552,8 @@ class PytestPluginManager(PluginManager):
     def _importconftest(
         self, conftestpath: py.path.local, importmode: Union[str, ImportMode],
     ) -> types.ModuleType:
-        # Use a resolved Path object as key to avoid loading the same conftest twice
-        # with build systems that create build directories containing
+        # Use a resolved Path object as key to avoid loading the same conftest
+        # twice with build systems that create build directories containing
         # symlinks to actual files.
         # Using Path().resolve() is better than py.path.realpath because
         # it resolves to the correct path/drive in case-insensitive file systems (#5792)
@@ -627,7 +640,7 @@ class PytestPluginManager(PluginManager):
             if name in essential_plugins:
                 raise UsageError("plugin %s cannot be disabled" % name)
 
-            # PR #4304 : remove stepwise if cacheprovider is blocked
+            # PR #4304: remove stepwise if cacheprovider is blocked.
             if name == "cacheprovider":
                 self.set_blocked("stepwise")
                 self.set_blocked("pytest_stepwise")
@@ -663,11 +676,12 @@ class PytestPluginManager(PluginManager):
             self.import_plugin(import_spec)
 
     def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
+        """Import a plugin with ``modname``.
+
+        If ``consider_entry_points`` is True, entry point names are also
+        considered to find a plugin.
         """
-        Imports a plugin with ``modname``. If ``consider_entry_points`` is True, entry point
-        names are also considered to find a plugin.
-        """
-        # most often modname refers to builtin modules, e.g. "pytester",
+        # Most often modname refers to builtin modules, e.g. "pytester",
         # "terminal" or "capture".  Those plugins are registered under their
         # basename for historic purposes but must be imported with the
         # _pytest prefix.
@@ -693,13 +707,7 @@ class PytestPluginManager(PluginManager):
             ).with_traceback(e.__traceback__) from e
 
         except Skipped as e:
-            from _pytest.warnings import _issue_warning_captured
-
-            _issue_warning_captured(
-                PytestConfigWarning("skipped plugin {!r}: {}".format(modname, e.msg)),
-                self.hook,
-                stacklevel=2,
-            )
+            self.skipped_plugins.append((modname, e.msg or ""))
         else:
             mod = sys.modules[importspec]
             self.register(mod, modname)
@@ -743,10 +751,11 @@ notset = Notset()
 
 
 def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
-    """
-    Given an iterable of file names in a source distribution, return the "names" that should
-    be marked for assertion rewrite (for example the package "pytest_mock/__init__.py" should
-    be added as "pytest_mock" in the assertion rewrite mechanism.
+    """Given an iterable of file names in a source distribution, return the "names" that should
+    be marked for assertion rewrite.
+
+    For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in
+    the assertion rewrite mechanism.
 
     This function has to deal with dist-info based distributions and egg based distributions
     (which are still very much in use for "editable" installs).
@@ -790,11 +799,11 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
             yield package_name
 
     if not seen_some:
-        # at this point we did not find any packages or modules suitable for assertion
+        # At this point we did not find any packages or modules suitable for assertion
         # rewriting, so we try again by stripping the first path component (to account for
-        # "src" based source trees for example)
-        # this approach lets us have the common case continue to be fast, as egg-distributions
-        # are rarer
+        # "src" based source trees for example).
+        # This approach lets us have the common case continue to be fast, as egg-distributions
+        # are rarer.
         new_package_files = []
         for fn in package_files:
             parts = fn.split("/")
@@ -809,20 +818,21 @@ def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
     return tuple(args)
 
 
+@final
 class Config:
-    """
-    Access to configuration values, pluginmanager and plugin hooks.
+    """Access to configuration values, pluginmanager and plugin hooks.
 
     :param PytestPluginManager pluginmanager:
 
     :param InvocationParams invocation_params:
-        Object containing the parameters regarding the ``pytest.main``
+        Object containing parameters regarding the :func:`pytest.main`
         invocation.
     """
 
+    @final
     @attr.s(frozen=True)
     class InvocationParams:
-        """Holds parameters passed during ``pytest.main()``
+        """Holds parameters passed during :func:`pytest.main`.
 
         The object attributes are read-only.
 
@@ -837,11 +847,20 @@ class Config:
         """
 
         args = attr.ib(type=Tuple[str, ...], converter=_args_converter)
-        """tuple of command-line arguments as passed to ``pytest.main()``."""
+        """The command-line arguments as passed to :func:`pytest.main`.
+
+        :type: Tuple[str, ...]
+        """
         plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]])
-        """list of extra plugins, might be `None`."""
+        """Extra plugins, might be `None`.
+
+        :type: Optional[Sequence[Union[str, plugin]]]
+        """
         dir = attr.ib(type=Path)
-        """directory where ``pytest.main()`` was invoked from."""
+        """The directory from which :func:`pytest.main` was invoked.
+
+        :type: pathlib.Path
+        """
 
     def __init__(
         self,
@@ -857,11 +876,16 @@ class Config:
             )
 
         self.option = argparse.Namespace()
-        """access to command line option as attributes.
+        """Access to command line option as attributes.
 
-          :type: argparse.Namespace"""
+        :type: argparse.Namespace
+        """
 
         self.invocation_params = invocation_params
+        """The parameters with which pytest was invoked.
+
+        :type: InvocationParams
+        """
 
         _a = FILE_OR_DIR
         self._parser = Parser(
@@ -869,9 +893,10 @@ class Config:
             processopt=self._processopt,
         )
         self.pluginmanager = pluginmanager
-        """the plugin manager handles plugin registration and hook invocation.
+        """The plugin manager handles plugin registration and hook invocation.
 
-          :type: PytestPluginManager"""
+        :type: PytestPluginManager
+        """
 
         self.trace = self.pluginmanager.trace.root.get("config")
         self.hook = self.pluginmanager.hook
@@ -895,11 +920,57 @@ class Config:
 
     @property
     def invocation_dir(self) -> py.path.local:
-        """Backward compatibility"""
+        """The directory from which pytest was invoked.
+
+        Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`,
+        which is a :class:`pathlib.Path`.
+
+        :type: py.path.local
+        """
         return py.path.local(str(self.invocation_params.dir))
 
+    @property
+    def rootpath(self) -> Path:
+        """The path to the :ref:`rootdir <rootdir>`.
+
+        :type: pathlib.Path
+
+        .. versionadded:: 6.1
+        """
+        return self._rootpath
+
+    @property
+    def rootdir(self) -> py.path.local:
+        """The path to the :ref:`rootdir <rootdir>`.
+
+        Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.
+
+        :type: py.path.local
+        """
+        return py.path.local(str(self.rootpath))
+
+    @property
+    def inipath(self) -> Optional[Path]:
+        """The path to the :ref:`configfile <configfiles>`.
+
+        :type: Optional[pathlib.Path]
+
+        .. versionadded:: 6.1
+        """
+        return self._inipath
+
+    @property
+    def inifile(self) -> Optional[py.path.local]:
+        """The path to the :ref:`configfile <configfiles>`.
+
+        Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
+
+        :type: Optional[py.path.local]
+        """
+        return py.path.local(str(self.inipath)) if self.inipath else None
+
     def add_cleanup(self, func: Callable[[], None]) -> None:
-        """ Add a function to be called when the config object gets out of
+        """Add a function to be called when the config object gets out of
         use (usually coninciding with pytest_unconfigure)."""
         self._cleanup.append(func)
 
@@ -970,15 +1041,15 @@ class Config:
                 sys.stderr.flush()
 
     def cwd_relative_nodeid(self, nodeid: str) -> str:
-        # nodeid's are relative to the rootpath, compute relative to cwd
-        if self.invocation_dir != self.rootdir:
-            fullpath = self.rootdir.join(nodeid)
-            nodeid = self.invocation_dir.bestrelpath(fullpath)
+        # nodeid's are relative to the rootpath, compute relative to cwd.
+        if self.invocation_params.dir != self.rootpath:
+            fullpath = self.rootpath / nodeid
+            nodeid = bestrelpath(self.invocation_params.dir, fullpath)
         return nodeid
 
     @classmethod
     def fromdictargs(cls, option_dict, args) -> "Config":
-        """ constructor usable for subprocesses. """
+        """Constructor usable for subprocesses."""
         config = get_config(args)
         config.option.__dict__.update(option_dict)
         config.parse(args, addopts=False)
@@ -1002,14 +1073,17 @@ class Config:
         ns, unknown_args = self._parser.parse_known_and_unknown_args(
             args, namespace=copy.copy(self.option)
         )
-        self.rootdir, self.inifile, self.inicfg = determine_setup(
+        rootpath, inipath, inicfg = determine_setup(
             ns.inifilename,
             ns.file_or_dir + unknown_args,
             rootdir_cmd_arg=ns.rootdir or None,
             config=self,
         )
-        self._parser.extra_info["rootdir"] = self.rootdir
-        self._parser.extra_info["inifile"] = self.inifile
+        self._rootpath = rootpath
+        self._inipath = inipath
+        self.inicfg = inicfg
+        self._parser.extra_info["rootdir"] = str(self.rootpath)
+        self._parser.extra_info["inifile"] = str(self.inipath)
         self._parser.addini("addopts", "extra command line options", "args")
         self._parser.addini("minversion", "minimally required pytest version")
         self._parser.addini(
@@ -1041,11 +1115,9 @@ class Config:
         self._warn_about_missing_assertion(mode)
 
     def _mark_plugins_for_rewrite(self, hook) -> None:
-        """
-        Given an importhook, mark for rewrite any top-level
+        """Given an importhook, mark for rewrite any top-level
         modules or packages in the distribution package for
-        all pytest plugins.
-        """
+        all pytest plugins."""
         self.pluginmanager.rewrite_hook = hook
 
         if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
@@ -1088,6 +1160,9 @@ class Config:
                 self._validate_args(self.getini("addopts"), "via addopts config") + args
             )
 
+        self.known_args_namespace = self._parser.parse_known_args(
+            args, namespace=copy.copy(self.option)
+        )
         self._checkversion()
         self._consider_importhook(args)
         self.pluginmanager.consider_preparse(args, exclude_only=False)
@@ -1096,33 +1171,40 @@ class Config:
             # plugins are going to be loaded.
             self.pluginmanager.load_setuptools_entrypoints("pytest11")
         self.pluginmanager.consider_env()
-        self.known_args_namespace = ns = self._parser.parse_known_args(
-            args, namespace=copy.copy(self.option)
+
+        self.known_args_namespace = self._parser.parse_known_args(
+            args, namespace=copy.copy(self.known_args_namespace)
         )
+
         self._validate_plugins()
-        if self.known_args_namespace.confcutdir is None and self.inifile:
-            confcutdir = py.path.local(self.inifile).dirname
+        self._warn_about_skipped_plugins()
+
+        if self.known_args_namespace.confcutdir is None and self.inipath is not None:
+            confcutdir = str(self.inipath.parent)
             self.known_args_namespace.confcutdir = confcutdir
         try:
             self.hook.pytest_load_initial_conftests(
                 early_config=self, args=args, parser=self._parser
             )
         except ConftestImportFailure as e:
-            if ns.help or ns.version:
+            if self.known_args_namespace.help or self.known_args_namespace.version:
                 # we don't want to prevent --help/--version to work
                 # so just let is pass and print a warning at the end
-                from _pytest.warnings import _issue_warning_captured
-
-                _issue_warning_captured(
+                self.issue_config_time_warning(
                     PytestConfigWarning(
                         "could not load initial conftests: {}".format(e.path)
                     ),
-                    self.hook,
                     stacklevel=2,
                 )
             else:
                 raise
-        self._validate_keys()
+
+    @hookimpl(hookwrapper=True)
+    def pytest_collection(self) -> Generator[None, None, None]:
+        """Validate invalid ini keys after collection is done so we take in account
+        options added by late-loading conftest files."""
+        yield
+        self._validate_config_options()
 
     def _checkversion(self) -> None:
         import pytest
@@ -1134,18 +1216,18 @@ class Config:
 
             if not isinstance(minver, str):
                 raise pytest.UsageError(
-                    "%s: 'minversion' must be a single value" % self.inifile
+                    "%s: 'minversion' must be a single value" % self.inipath
                 )
 
             if Version(minver) > Version(pytest.__version__):
                 raise pytest.UsageError(
                     "%s: 'minversion' requires pytest-%s, actual pytest-%s'"
-                    % (self.inifile, minver, pytest.__version__,)
+                    % (self.inipath, minver, pytest.__version__,)
                 )
 
-    def _validate_keys(self) -> None:
+    def _validate_config_options(self) -> None:
         for key in sorted(self._get_unknown_ini_keys()):
-            self._warn_or_fail_if_strict("Unknown config ini key: {}\n".format(key))
+            self._warn_or_fail_if_strict("Unknown config option: {}\n".format(key))
 
     def _validate_plugins(self) -> None:
         required_plugins = sorted(self.getini("required_plugins"))
@@ -1161,7 +1243,6 @@ class Config:
 
         missing_plugins = []
         for required_plugin in required_plugins:
-            spec = None
             try:
                 spec = Requirement(required_plugin)
             except InvalidRequirement:
@@ -1174,27 +1255,22 @@ class Config:
                 missing_plugins.append(required_plugin)
 
         if missing_plugins:
-            fail(
+            raise UsageError(
                 "Missing required plugins: {}".format(", ".join(missing_plugins)),
-                pytrace=False,
             )
 
     def _warn_or_fail_if_strict(self, message: str) -> None:
         if self.known_args_namespace.strict_config:
-            fail(message, pytrace=False)
+            raise UsageError(message)
 
-        from _pytest.warnings import _issue_warning_captured
-
-        _issue_warning_captured(
-            PytestConfigWarning(message), self.hook, stacklevel=3,
-        )
+        self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
 
     def _get_unknown_ini_keys(self) -> List[str]:
         parser_inicfg = self._parser._inidict
         return [name for name in self.inicfg if name not in parser_inicfg]
 
     def parse(self, args: List[str], addopts: bool = True) -> None:
-        # parse given cmdline arguments into this config object.
+        # Parse given cmdline arguments into this config object.
         assert not hasattr(
             self, "args"
         ), "can only parse cmdline args at most once per Config object"
@@ -1210,27 +1286,72 @@ class Config:
                 args, self.option, namespace=self.option
             )
             if not args:
-                if self.invocation_dir == self.rootdir:
+                if self.invocation_params.dir == self.rootpath:
                     args = self.getini("testpaths")
                 if not args:
-                    args = [str(self.invocation_dir)]
+                    args = [str(self.invocation_params.dir)]
             self.args = args
         except PrintHelp:
             pass
 
+    def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:
+        """Issue and handle a warning during the "configure" stage.
+
+        During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
+        function because it is not possible to have hookwrappers around ``pytest_configure``.
+
+        This function is mainly intended for plugins that need to issue warnings during
+        ``pytest_configure`` (or similar stages).
+
+        :param warning: The warning instance.
+        :param stacklevel: stacklevel forwarded to warnings.warn.
+        """
+        if self.pluginmanager.is_blocked("warnings"):
+            return
+
+        cmdline_filters = self.known_args_namespace.pythonwarnings or []
+        config_filters = self.getini("filterwarnings")
+
+        with warnings.catch_warnings(record=True) as records:
+            warnings.simplefilter("always", type(warning))
+            apply_warning_filters(config_filters, cmdline_filters)
+            warnings.warn(warning, stacklevel=stacklevel)
+
+        if records:
+            frame = sys._getframe(stacklevel - 1)
+            location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
+            self.hook.pytest_warning_captured.call_historic(
+                kwargs=dict(
+                    warning_message=records[0],
+                    when="config",
+                    item=None,
+                    location=location,
+                )
+            )
+            self.hook.pytest_warning_recorded.call_historic(
+                kwargs=dict(
+                    warning_message=records[0],
+                    when="config",
+                    nodeid="",
+                    location=location,
+                )
+            )
+
     def addinivalue_line(self, name: str, line: str) -> None:
-        """ add a line to an ini-file option. The option must have been
-        declared but might not yet be set in which case the line becomes the
-        the first line in its value. """
+        """Add a line to an ini-file option. The option must have been
+        declared but might not yet be set in which case the line becomes
+        the first line in its value."""
         x = self.getini(name)
         assert isinstance(x, list)
         x.append(line)  # modifies the cached list inline
 
     def getini(self, name: str):
-        """ return configuration value from an :ref:`ini file <configfiles>`. If the
-        specified name hasn't been registered through a prior
+        """Return configuration value from an :ref:`ini file <configfiles>`.
+
+        If the specified name hasn't been registered through a prior
         :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>`
-        call (usually from a plugin), a ValueError is raised. """
+        call (usually from a plugin), a ValueError is raised.
+        """
         try:
             return self._inicache[name]
         except KeyError:
@@ -1254,26 +1375,27 @@ class Config:
                 return []
         else:
             value = override_value
-        # coerce the values based on types
-        # note: some coercions are only required if we are reading from .ini files, because
+        # Coerce the values based on types.
+        #
+        # Note: some coercions are only required if we are reading from .ini files, because
         # the file format doesn't contain type information, but when reading from toml we will
         # get either str or list of str values (see _parse_ini_config_from_pyproject_toml).
-        # for example:
+        # For example:
         #
         #   ini:
         #     a_line_list = "tests acceptance"
-        #   in this case, we need to split the string to obtain a list of strings
+        #   in this case, we need to split the string to obtain a list of strings.
         #
         #   toml:
         #     a_line_list = ["tests", "acceptance"]
-        #   in this case, we already have a list ready to use
+        #   in this case, we already have a list ready to use.
         #
         if type == "pathlist":
             # TODO: This assert is probably not valid in all cases.
-            assert self.inifile is not None
-            dp = py.path.local(self.inifile).dirpath()
+            assert self.inipath is not None
+            dp = self.inipath.parent
             input_values = shlex.split(value) if isinstance(value, str) else value
-            return [dp.join(x, abs=True) for x in input_values]
+            return [py.path.local(str(dp / x)) for x in input_values]
         elif type == "args":
             return shlex.split(value) if isinstance(value, str) else value
         elif type == "linelist":
@@ -1300,16 +1422,16 @@ class Config:
         values = []  # type: List[py.path.local]
         for relroot in relroots:
             if not isinstance(relroot, py.path.local):
-                relroot = relroot.replace("/", py.path.local.sep)
+                relroot = relroot.replace("/", os.sep)
                 relroot = modpath.join(relroot, abs=True)
             values.append(relroot)
         return values
 
     def _get_override_ini_value(self, name: str) -> Optional[str]:
         value = None
-        # override_ini is a list of "ini=value" options
-        # always use the last item if multiple values are set for same ini-name,
-        # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2
+        # override_ini is a list of "ini=value" options.
+        # Always use the last item if multiple values are set for same ini-name,
+        # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
         for ini_config in self._override_ini:
             try:
                 key, user_ini_value = ini_config.split("=", 1)
@@ -1325,12 +1447,12 @@ class Config:
         return value
 
     def getoption(self, name: str, default=notset, skip: bool = False):
-        """ return command line option value.
+        """Return command line option value.
 
-        :arg name: name of the option.  You may also specify
+        :param name: Name of the option.  You may also specify
             the literal ``--OPT`` option instead of the "dest" option name.
-        :arg default: default value if no option of that name exists.
-        :arg skip: if True raise pytest.skip if option does not exists
+        :param default: Default value if no option of that name exists.
+        :param skip: If True, raise pytest.skip if option does not exists
             or has a None value.
         """
         name = self._opt2dest.get(name, name)
@@ -1349,17 +1471,15 @@ class Config:
             raise ValueError("no option named {!r}".format(name)) from e
 
     def getvalue(self, name: str, path=None):
-        """ (deprecated, use getoption()) """
+        """Deprecated, use getoption() instead."""
         return self.getoption(name)
 
     def getvalueorskip(self, name: str, path=None):
-        """ (deprecated, use getoption(skip=True)) """
+        """Deprecated, use getoption(skip=True) instead."""
         return self.getoption(name, skip=True)
 
     def _warn_about_missing_assertion(self, mode: str) -> None:
         if not _assertion_supported():
-            from _pytest.warnings import _issue_warning_captured
-
             if mode == "plain":
                 warning_text = (
                     "ASSERTIONS ARE NOT EXECUTED"
@@ -1374,8 +1494,15 @@ class Config:
                     "by the underlying Python interpreter "
                     "(are you using python -O?)\n"
                 )
-            _issue_warning_captured(
-                PytestConfigWarning(warning_text), self.hook, stacklevel=3,
+            self.issue_config_time_warning(
+                PytestConfigWarning(warning_text), stacklevel=3,
+            )
+
+    def _warn_about_skipped_plugins(self) -> None:
+        for module_name, msg in self.pluginmanager.skipped_plugins:
+            self.issue_config_time_warning(
+                PytestConfigWarning("skipped plugin {!r}: {}".format(module_name, msg)),
+                stacklevel=2,
             )
 
 
@@ -1385,17 +1512,20 @@ def _assertion_supported() -> bool:
     except AssertionError:
         return True
     else:
-        return False
+        return False  # type: ignore[unreachable]
 
 
 def create_terminal_writer(
     config: Config, file: Optional[TextIO] = None
 ) -> TerminalWriter:
     """Create a TerminalWriter instance configured according to the options
-    in the config object. Every code which requires a TerminalWriter object
-    and has access to a config object should use this function.
+    in the config object.
+
+    Every code which requires a TerminalWriter object and has access to a
+    config object should use this function.
     """
     tw = TerminalWriter(file=file)
+
     if config.option.color == "yes":
         tw.hasmarkup = True
     elif config.option.color == "no":
@@ -1405,6 +1535,7 @@ def create_terminal_writer(
         tw.code_highlight = True
     elif config.option.code_highlight == "no":
         tw.code_highlight = False
+
     return tw
 
 
@@ -1415,7 +1546,7 @@ def _strtobool(val: str) -> bool:
     are 'n', 'no', 'f', 'false', 'off', and '0'.  Raises ValueError if
     'val' is anything else.
 
-    .. note:: copied from distutils.util
+    .. note:: Copied from distutils.util.
     """
     val = val.lower()
     if val in ("y", "yes", "t", "true", "on", "1"):
@@ -1424,3 +1555,51 @@ def _strtobool(val: str) -> bool:
         return False
     else:
         raise ValueError("invalid truth value {!r}".format(val))
+
+
+@lru_cache(maxsize=50)
+def parse_warning_filter(
+    arg: str, *, escape: bool
+) -> "Tuple[str, str, Type[Warning], str, int]":
+    """Parse a warnings filter string.
+
+    This is copied from warnings._setoption, but does not apply the filter,
+    only parses it, and makes the escaping optional.
+    """
+    parts = arg.split(":")
+    if len(parts) > 5:
+        raise warnings._OptionError("too many fields (max 5): {!r}".format(arg))
+    while len(parts) < 5:
+        parts.append("")
+    action_, message, category_, module, lineno_ = [s.strip() for s in parts]
+    action = warnings._getaction(action_)  # type: str # type: ignore[attr-defined]
+    category = warnings._getcategory(
+        category_
+    )  # type: Type[Warning] # type: ignore[attr-defined]
+    if message and escape:
+        message = re.escape(message)
+    if module and escape:
+        module = re.escape(module) + r"\Z"
+    if lineno_:
+        try:
+            lineno = int(lineno_)
+            if lineno < 0:
+                raise ValueError
+        except (ValueError, OverflowError) as e:
+            raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e
+    else:
+        lineno = 0
+    return action, message, category, module, lineno
+
+
+def apply_warning_filters(
+    config_filters: Iterable[str], cmdline_filters: Iterable[str]
+) -> None:
+    """Applies pytest-configured filters to the warnings module"""
+    # Filters should have this precedence: cmdline options, config.
+    # Filters should be applied in the inverse order of precedence.
+    for arg in config_filters:
+        warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
+
+    for arg in cmdline_filters:
+        warnings.filterwarnings(*parse_warning_filter(arg, escape=True))
index 084ce16e59b7c7eb5a6fcc2a93caf04304711877..636021df455b4e1c385474bb0269a57b07195f03 100644 (file)
@@ -16,6 +16,7 @@ from typing import Union
 import py
 
 import _pytest._io
+from _pytest.compat import final
 from _pytest.compat import TYPE_CHECKING
 from _pytest.config.exceptions import UsageError
 
@@ -26,10 +27,11 @@ if TYPE_CHECKING:
 FILE_OR_DIR = "file_or_dir"
 
 
+@final
 class Parser:
-    """ Parser for command line arguments and ini-file values.
+    """Parser for command line arguments and ini-file values.
 
-    :ivar extra_info: dict of generic param -> value to display in case
+    :ivar extra_info: Dict of generic param -> value to display in case
         there's an error processing the command line arguments.
     """
 
@@ -56,11 +58,11 @@ class Parser:
     def getgroup(
         self, name: str, description: str = "", after: Optional[str] = None
     ) -> "OptionGroup":
-        """ get (or create) a named option Group.
+        """Get (or create) a named option Group.
 
-        :name: name of the option group.
-        :description: long description for --help output.
-        :after: name of other group, used for ordering --help output.
+        :name: Name of the option group.
+        :description: Long description for --help output.
+        :after: Name of another group, used for ordering --help output.
 
         The returned group object has an ``addoption`` method with the same
         signature as :py:func:`parser.addoption
@@ -79,15 +81,14 @@ class Parser:
         return group
 
     def addoption(self, *opts: str, **attrs: Any) -> None:
-        """ register a command line option.
+        """Register a command line option.
 
-        :opts: option names, can be short or long options.
-        :attrs: same attributes which the ``add_argument()`` function of the
-           `argparse library
-           <https://docs.python.org/library/argparse.html>`_
+        :opts: Option names, can be short or long options.
+        :attrs: Same attributes which the ``add_argument()`` function of the
+           `argparse library <https://docs.python.org/library/argparse.html>`_
            accepts.
 
-        After command line parsing options are available on the pytest config
+        After command line parsing, options are available on the pytest config
         object via ``config.option.NAME`` where ``NAME`` is usually set
         by passing a ``dest`` attribute, for example
         ``addoption("--long", dest="NAME", ...)``.
@@ -141,9 +142,7 @@ class Parser:
         args: Sequence[Union[str, py.path.local]],
         namespace: Optional[argparse.Namespace] = None,
     ) -> argparse.Namespace:
-        """parses and returns a namespace object with known arguments at this
-        point.
-        """
+        """Parse and return a namespace object with known arguments at this point."""
         return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
 
     def parse_known_and_unknown_args(
@@ -151,9 +150,8 @@ class Parser:
         args: Sequence[Union[str, py.path.local]],
         namespace: Optional[argparse.Namespace] = None,
     ) -> Tuple[argparse.Namespace, List[str]]:
-        """parses and returns a namespace object with known arguments, and
-        the remaining arguments unknown at this point.
-        """
+        """Parse and return a namespace object with known arguments, and
+        the remaining arguments unknown at this point."""
         optparser = self._getparser()
         strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
         return optparser.parse_known_args(strargs, namespace=namespace)
@@ -165,12 +163,12 @@ class Parser:
         type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None,
         default=None,
     ) -> None:
-        """ register an ini-file option.
+        """Register an ini-file option.
 
-        :name: name of the ini-variable
-        :type: type of the variable, can be ``pathlist``, ``args``, ``linelist``
+        :name: Name of the ini-variable.
+        :type: Type of the variable, can be ``pathlist``, ``args``, ``linelist``
                or ``bool``.
-        :default: default value if no ini-file option exists but is queried.
+        :default: Default value if no ini-file option exists but is queried.
 
         The value of ini-variables can be retrieved via a call to
         :py:func:`config.getini(name) <_pytest.config.Config.getini>`.
@@ -181,10 +179,8 @@ class Parser:
 
 
 class ArgumentError(Exception):
-    """
-    Raised if an Argument instance is created with invalid or
-    inconsistent arguments.
-    """
+    """Raised if an Argument instance is created with invalid or
+    inconsistent arguments."""
 
     def __init__(self, msg: str, option: Union["Argument", str]) -> None:
         self.msg = msg
@@ -198,17 +194,18 @@ class ArgumentError(Exception):
 
 
 class Argument:
-    """class that mimics the necessary behaviour of optparse.Option
+    """Class that mimics the necessary behaviour of optparse.Option.
+
+    It's currently a least effort implementation and ignoring choices
+    and integer prefixes.
 
-    it's currently a least effort implementation
-    and ignoring choices and integer prefixes
     https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
     """
 
     _typ_map = {"int": int, "string": str, "float": float, "complex": complex}
 
     def __init__(self, *names: str, **attrs: Any) -> None:
-        """store parms in private vars for use in add_argument"""
+        """Store parms in private vars for use in add_argument."""
         self._attrs = attrs
         self._short_opts = []  # type: List[str]
         self._long_opts = []  # type: List[str]
@@ -224,7 +221,7 @@ class Argument:
         except KeyError:
             pass
         else:
-            # this might raise a keyerror as well, don't want to catch that
+            # This might raise a keyerror as well, don't want to catch that.
             if isinstance(typ, str):
                 if typ == "choice":
                     warnings.warn(
@@ -247,12 +244,12 @@ class Argument:
                         stacklevel=4,
                     )
                     attrs["type"] = Argument._typ_map[typ]
-                # used in test_parseopt -> test_parse_defaultgetter
+                # Used in test_parseopt -> test_parse_defaultgetter.
                 self.type = attrs["type"]
             else:
                 self.type = typ
         try:
-            # attribute existence is tested in Config._processopt
+            # Attribute existence is tested in Config._processopt.
             self.default = attrs["default"]
         except KeyError:
             pass
@@ -273,7 +270,7 @@ class Argument:
         return self._short_opts + self._long_opts
 
     def attrs(self) -> Mapping[str, Any]:
-        # update any attributes set by processopt
+        # Update any attributes set by processopt.
         attrs = "default dest help".split()
         attrs.append(self.dest)
         for attr in attrs:
@@ -289,9 +286,10 @@ class Argument:
         return self._attrs
 
     def _set_opt_strings(self, opts: Sequence[str]) -> None:
-        """directly from optparse
+        """Directly from optparse.
 
-        might not be necessary as this is passed to argparse later on"""
+        Might not be necessary as this is passed to argparse later on.
+        """
         for opt in opts:
             if len(opt) < 2:
                 raise ArgumentError(
@@ -340,12 +338,12 @@ class OptionGroup:
         self.parser = parser
 
     def addoption(self, *optnames: str, **attrs: Any) -> None:
-        """ add an option to this group.
+        """Add an option to this group.
 
-        if a shortened version of a long option is specified it will
+        If a shortened version of a long option is specified, it will
         be suppressed in the help. addoption('--twowords', '--two-words')
         results in help showing '--two-words' only, but --twowords gets
-        accepted **and** the automatic destination is in args.twowords
+        accepted **and** the automatic destination is in args.twowords.
         """
         conflict = set(optnames).intersection(
             name for opt in self.options for name in opt.names()
@@ -386,7 +384,7 @@ class MyOptionParser(argparse.ArgumentParser):
             allow_abbrev=False,
         )
         # extra_info is a dict of (param -> value) to display if there's
-        # an usage error to provide more contextual information to the user
+        # an usage error to provide more contextual information to the user.
         self.extra_info = extra_info if extra_info else {}
 
     def error(self, message: str) -> "NoReturn":
@@ -405,7 +403,7 @@ class MyOptionParser(argparse.ArgumentParser):
         args: Optional[Sequence[str]] = None,
         namespace: Optional[argparse.Namespace] = None,
     ) -> argparse.Namespace:
-        """allow splitting of positional arguments"""
+        """Allow splitting of positional arguments."""
         parsed, unrecognized = self.parse_known_args(args, namespace)
         if unrecognized:
             for arg in unrecognized:
@@ -457,15 +455,15 @@ class MyOptionParser(argparse.ArgumentParser):
 
 
 class DropShorterLongHelpFormatter(argparse.HelpFormatter):
-    """shorten help for long options that differ only in extra hyphens
+    """Shorten help for long options that differ only in extra hyphens.
 
-    - collapse **long** options that are the same except for extra hyphens
-    - shortcut if there are only two options and one of them is a short one
-    - cache result on action object as this is called at least 2 times
+    - Collapse **long** options that are the same except for extra hyphens.
+    - Shortcut if there are only two options and one of them is a short one.
+    - Cache result on the action object as this is called at least 2 times.
     """
 
     def __init__(self, *args: Any, **kwargs: Any) -> None:
-        """Use more accurate terminal width via pylib."""
+        # Use more accurate terminal width.
         if "width" not in kwargs:
             kwargs["width"] = _pytest._io.get_terminal_width()
         super().__init__(*args, **kwargs)
index 19fe5cb08ed4a188840d83ce7e5b1532080b6a7e..4f1320e758d506763261c4bd8dc21453dda4b5ef 100644 (file)
@@ -1,9 +1,11 @@
+from _pytest.compat import final
+
+
+@final
 class UsageError(Exception):
-    """ error in pytest usage or invocation"""
+    """Error in pytest usage or invocation."""
 
 
 class PrintHelp(Exception):
-    """Raised when pytest should print it's help to skip the rest of the
+    """Raised when pytest should print its help to skip the rest of the
     argument parsing and validation."""
-
-    pass
index 08a71122dcd18cc79d80ad197bd1623f858c0023..facf30a87a2d10023f1ba55385cc1b61922ec40f 100644 (file)
@@ -1,27 +1,31 @@
+import itertools
 import os
 from typing import Dict
 from typing import Iterable
 from typing import List
 from typing import Optional
+from typing import Sequence
 from typing import Tuple
 from typing import Union
 
 import iniconfig
-import py
 
 from .exceptions import UsageError
 from _pytest.compat import TYPE_CHECKING
 from _pytest.outcomes import fail
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import commonpath
+from _pytest.pathlib import Path
 
 if TYPE_CHECKING:
     from . import Config
 
 
-def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig:
-    """Parses the given generic '.ini' file using legacy IniConfig parser, returning
+def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
+    """Parse the given generic '.ini' file using legacy IniConfig parser, returning
     the parsed object.
 
-    Raises UsageError if the file cannot be parsed.
+    Raise UsageError if the file cannot be parsed.
     """
     try:
         return iniconfig.IniConfig(path)
@@ -30,26 +34,26 @@ def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig:
 
 
 def load_config_dict_from_file(
-    filepath: py.path.local,
+    filepath: Path,
 ) -> Optional[Dict[str, Union[str, List[str]]]]:
-    """Loads pytest configuration from the given file path, if supported.
+    """Load pytest configuration from the given file path, if supported.
 
     Return None if the file does not contain valid pytest configuration.
     """
 
-    # configuration from ini files are obtained from the [pytest] section, if present.
-    if filepath.ext == ".ini":
+    # Configuration from ini files are obtained from the [pytest] section, if present.
+    if filepath.suffix == ".ini":
         iniconfig = _parse_ini_config(filepath)
 
         if "pytest" in iniconfig:
             return dict(iniconfig["pytest"].items())
         else:
-            # "pytest.ini" files are always the source of configuration, even if empty
-            if filepath.basename == "pytest.ini":
+            # "pytest.ini" files are always the source of configuration, even if empty.
+            if filepath.name == "pytest.ini":
                 return {}
 
-    # '.cfg' files are considered if they contain a "[tool:pytest]" section
-    elif filepath.ext == ".cfg":
+    # '.cfg' files are considered if they contain a "[tool:pytest]" section.
+    elif filepath.suffix == ".cfg":
         iniconfig = _parse_ini_config(filepath)
 
         if "tool:pytest" in iniconfig.sections:
@@ -59,8 +63,8 @@ def load_config_dict_from_file(
             # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
             fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
 
-    # '.toml' files are considered if they contain a [tool.pytest.ini_options] table
-    elif filepath.ext == ".toml":
+    # '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
+    elif filepath.suffix == ".toml":
         import toml
 
         config = toml.load(str(filepath))
@@ -79,14 +83,12 @@ def load_config_dict_from_file(
 
 
 def locate_config(
-    args: Iterable[Union[str, py.path.local]]
+    args: Iterable[Path],
 ) -> Tuple[
-    Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]],
+    Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]],
 ]:
-    """
-    Search in the list of arguments for a valid ini-file for pytest,
-    and return a tuple of (rootdir, inifile, cfg-dict).
-    """
+    """Search in the list of arguments for a valid ini-file for pytest,
+    and return a tuple of (rootdir, inifile, cfg-dict)."""
     config_names = [
         "pytest.ini",
         "pyproject.toml",
@@ -95,62 +97,70 @@ def locate_config(
     ]
     args = [x for x in args if not str(x).startswith("-")]
     if not args:
-        args = [py.path.local()]
+        args = [Path.cwd()]
     for arg in args:
-        arg = py.path.local(arg)
-        for base in arg.parts(reverse=True):
+        argpath = absolutepath(arg)
+        for base in itertools.chain((argpath,), reversed(argpath.parents)):
             for config_name in config_names:
-                p = base.join(config_name)
-                if p.isfile():
+                p = base / config_name
+                if p.is_file():
                     ini_config = load_config_dict_from_file(p)
                     if ini_config is not None:
                         return base, p, ini_config
     return None, None, {}
 
 
-def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local:
-    common_ancestor = None
+def get_common_ancestor(paths: Iterable[Path]) -> Path:
+    common_ancestor = None  # type: Optional[Path]
     for path in paths:
         if not path.exists():
             continue
         if common_ancestor is None:
             common_ancestor = path
         else:
-            if path.relto(common_ancestor) or path == common_ancestor:
+            if common_ancestor in path.parents or path == common_ancestor:
                 continue
-            elif common_ancestor.relto(path):
+            elif path in common_ancestor.parents:
                 common_ancestor = path
             else:
-                shared = path.common(common_ancestor)
+                shared = commonpath(path, common_ancestor)
                 if shared is not None:
                     common_ancestor = shared
     if common_ancestor is None:
-        common_ancestor = py.path.local()
-    elif common_ancestor.isfile():
-        common_ancestor = common_ancestor.dirpath()
+        common_ancestor = Path.cwd()
+    elif common_ancestor.is_file():
+        common_ancestor = common_ancestor.parent
     return common_ancestor
 
 
-def get_dirs_from_args(args: Iterable[str]) -> List[py.path.local]:
+def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
     def is_option(x: str) -> bool:
         return x.startswith("-")
 
     def get_file_part_from_node_id(x: str) -> str:
         return x.split("::")[0]
 
-    def get_dir_from_path(path: py.path.local) -> py.path.local:
-        if path.isdir():
+    def get_dir_from_path(path: Path) -> Path:
+        if path.is_dir():
             return path
-        return py.path.local(path.dirname)
+        return path.parent
+
+    def safe_exists(path: Path) -> bool:
+        # This can throw on paths that contain characters unrepresentable at the OS level,
+        # or with invalid syntax on Windows (https://bugs.python.org/issue35306)
+        try:
+            return path.exists()
+        except OSError:
+            return False
 
     # These look like paths but may not exist
     possible_paths = (
-        py.path.local(get_file_part_from_node_id(arg))
+        absolutepath(get_file_part_from_node_id(arg))
         for arg in args
         if not is_option(arg)
     )
 
-    return [get_dir_from_path(path) for path in possible_paths if path.exists()]
+    return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
 
 
 CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
@@ -158,15 +168,15 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte
 
 def determine_setup(
     inifile: Optional[str],
-    args: List[str],
+    args: Sequence[str],
     rootdir_cmd_arg: Optional[str] = None,
     config: Optional["Config"] = None,
-) -> Tuple[py.path.local, Optional[py.path.local], Dict[str, Union[str, List[str]]]]:
+) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
     rootdir = None
     dirs = get_dirs_from_args(args)
     if inifile:
-        inipath_ = py.path.local(inifile)
-        inipath = inipath_  # type: Optional[py.path.local]
+        inipath_ = absolutepath(inifile)
+        inipath = inipath_  # type: Optional[Path]
         inicfg = load_config_dict_from_file(inipath_) or {}
         if rootdir_cmd_arg is None:
             rootdir = get_common_ancestor(dirs)
@@ -174,8 +184,10 @@ def determine_setup(
         ancestor = get_common_ancestor(dirs)
         rootdir, inipath, inicfg = locate_config([ancestor])
         if rootdir is None and rootdir_cmd_arg is None:
-            for possible_rootdir in ancestor.parts(reverse=True):
-                if possible_rootdir.join("setup.py").exists():
+            for possible_rootdir in itertools.chain(
+                (ancestor,), reversed(ancestor.parents)
+            ):
+                if (possible_rootdir / "setup.py").is_file():
                     rootdir = possible_rootdir
                     break
             else:
@@ -183,16 +195,16 @@ def determine_setup(
                     rootdir, inipath, inicfg = locate_config(dirs)
                 if rootdir is None:
                     if config is not None:
-                        cwd = config.invocation_dir
+                        cwd = config.invocation_params.dir
                     else:
-                        cwd = py.path.local()
+                        cwd = Path.cwd()
                     rootdir = get_common_ancestor([cwd, ancestor])
                     is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
                     if is_fs_root:
                         rootdir = ancestor
     if rootdir_cmd_arg:
-        rootdir = py.path.local(os.path.expandvars(rootdir_cmd_arg))
-        if not rootdir.isdir():
+        rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
+        if not rootdir.is_dir():
             raise UsageError(
                 "Directory '{}' not found. Check your '--rootdir' option.".format(
                     rootdir
index 3677d3bf915f52b25d0a25339df8d3ee8d884298..6f641fb2d9667c50f43bec7786ca0ab9fbcd9d8d 100644 (file)
@@ -1,9 +1,13 @@
-""" interactive debugging with PDB, the Python Debugger. """
+"""Interactive debugging with PDB, the Python Debugger."""
 import argparse
 import functools
 import sys
 import types
+from typing import Any
+from typing import Callable
 from typing import Generator
+from typing import List
+from typing import Optional
 from typing import Tuple
 from typing import Union
 
@@ -20,6 +24,8 @@ from _pytest.nodes import Node
 from _pytest.reports import BaseReport
 
 if TYPE_CHECKING:
+    from typing import Type
+
     from _pytest.capture import CaptureManager
     from _pytest.runner import CallInfo
 
@@ -87,22 +93,24 @@ def pytest_configure(config: Config) -> None:
 
 
 class pytestPDB:
-    """ Pseudo PDB that defers to the real pdb. """
+    """Pseudo PDB that defers to the real pdb."""
 
-    _pluginmanager = None  # type: PytestPluginManager
+    _pluginmanager = None  # type: Optional[PytestPluginManager]
     _config = None  # type: Config
-    _saved = []  # type: list
+    _saved = (
+        []
+    )  # type: List[Tuple[Callable[..., None], Optional[PytestPluginManager], Config]]
     _recursive_debug = 0
-    _wrapped_pdb_cls = None
+    _wrapped_pdb_cls = None  # type: Optional[Tuple[Type[Any], Type[Any]]]
 
     @classmethod
-    def _is_capturing(cls, capman: "CaptureManager") -> Union[str, bool]:
+    def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]:
         if capman:
             return capman.is_capturing()
         return False
 
     @classmethod
-    def _import_pdb_cls(cls, capman: "CaptureManager"):
+    def _import_pdb_cls(cls, capman: Optional["CaptureManager"]):
         if not cls._config:
             import pdb
 
@@ -141,7 +149,7 @@ class pytestPDB:
         return wrapped_cls
 
     @classmethod
-    def _get_pdb_wrapper_class(cls, pdb_cls, capman: "CaptureManager"):
+    def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
         import _pytest.config
 
         # Type ignored because mypy doesn't support "dynamic"
@@ -173,9 +181,11 @@ class pytestPDB:
                                 "PDB continue (IO-capturing resumed for %s)"
                                 % capturing,
                             )
+                        assert capman is not None
                         capman.resume()
                     else:
                         tw.sep(">", "PDB continue")
+                assert cls._pluginmanager is not None
                 cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
                 self._continued = True
                 return ret
@@ -226,13 +236,13 @@ class pytestPDB:
 
     @classmethod
     def _init_pdb(cls, method, *args, **kwargs):
-        """ Initialize PDB debugging, dropping any IO capturing. """
+        """Initialize PDB debugging, dropping any IO capturing."""
         import _pytest.config
 
-        if cls._pluginmanager is not None:
-            capman = cls._pluginmanager.getplugin("capturemanager")
+        if cls._pluginmanager is None:
+            capman = None  # type: Optional[CaptureManager]
         else:
-            capman = None
+            capman = cls._pluginmanager.getplugin("capturemanager")
         if capman:
             capman.suspend(in_=True)
 
@@ -274,7 +284,7 @@ class pytestPDB:
 
 class PdbInvoke:
     def pytest_exception_interact(
-        self, node: Node, call: "CallInfo", report: BaseReport
+        self, node: Node, call: "CallInfo[Any]", report: BaseReport
     ) -> None:
         capman = node.config.pluginmanager.getplugin("capturemanager")
         if capman:
@@ -298,16 +308,16 @@ class PdbTrace:
 
 
 def wrap_pytest_function_for_tracing(pyfuncitem):
-    """Changes the python function object of the given Function item by a wrapper which actually
-    enters pdb before calling the python function itself, effectively leaving the user
-    in the pdb prompt in the first statement of the function.
-    """
+    """Change the Python function object of the given Function item by a
+    wrapper which actually enters pdb before calling the python function
+    itself, effectively leaving the user in the pdb prompt in the first
+    statement of the function."""
     _pdb = pytestPDB._init_pdb("runcall")
     testfunction = pyfuncitem.obj
 
     # we can't just return `partial(pdb.runcall, testfunction)` because (on
     # python < 3.7.4) runcall's first param is `func`, which means we'd get
-    # an exception if one of the kwargs to testfunction was called `func`
+    # an exception if one of the kwargs to testfunction was called `func`.
     @functools.wraps(testfunction)
     def wrapper(*args, **kwargs):
         func = functools.partial(testfunction, *args, **kwargs)
@@ -318,7 +328,7 @@ def wrap_pytest_function_for_tracing(pyfuncitem):
 
 def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
     """Wrap the given pytestfunct item for tracing support if --trace was given in
-    the command line"""
+    the command line."""
     if pyfuncitem.config.getvalue("trace"):
         wrap_pytest_function_for_tracing(pyfuncitem)
 
index 868318a2bc60601b5599f69e7a496881388468bf..ecdb60d37f5c61fd9bc46d604d52ee2e44d2af69 100644 (file)
@@ -1,6 +1,5 @@
-"""
-This module contains deprecation messages and bits of code used elsewhere in the codebase
-that is planned to be removed in the next pytest release.
+"""Deprecation messages and bits of code used elsewhere in the codebase that
+is planned to be removed in the next pytest release.
 
 Keeping it in a central location makes it easy to track what is deprecated and should
 be removed when the time comes.
@@ -20,45 +19,12 @@ DEPRECATED_EXTERNAL_PLUGINS = {
     "pytest_faulthandler",
 }
 
-FUNCARGNAMES = PytestDeprecationWarning(
-    "The `funcargnames` attribute was an alias for `fixturenames`, "
-    "since pytest 2.3 - use the newer attribute instead."
-)
 
 FILLFUNCARGS = PytestDeprecationWarning(
     "The `_fillfuncargs` function is deprecated, use "
     "function._request._fillfixtures() instead if you cannot avoid reaching into internals."
 )
 
-RESULT_LOG = PytestDeprecationWarning(
-    "--result-log is deprecated, please try the new pytest-reportlog plugin.\n"
-    "See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information."
-)
-
-FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning(
-    "Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them "
-    "as a keyword argument instead."
-)
-
-NODE_USE_FROM_PARENT = UnformattedWarning(
-    PytestDeprecationWarning,
-    "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
-    "See "
-    "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
-    " for more details.",
-)
-
-JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning(
-    "The 'junit_family' default value will change to 'xunit2' in pytest 6.0. See:\n"
-    "  https://docs.pytest.org/en/stable/deprecations.html#junit-family-default-value-change-to-xunit2\n"
-    "for more information."
-)
-
-COLLECT_DIRECTORY_HOOK = PytestDeprecationWarning(
-    "The pytest_collect_directory hook is not working.\n"
-    "Please use collect_ignore in conftests or pytest_collection_modifyitems."
-)
-
 PYTEST_COLLECT_MODULE = UnformattedWarning(
     PytestDeprecationWarning,
     "pytest.collect.{name} was moved to pytest.{name}\n"
@@ -66,12 +32,6 @@ PYTEST_COLLECT_MODULE = UnformattedWarning(
 )
 
 
-TERMINALWRITER_WRITER = PytestDeprecationWarning(
-    "The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.\n"
-    "See https://docs.pytest.org/en/stable/deprecations.html#terminalreporter-writer for more information."
-)
-
-
 MINUS_K_DASH = PytestDeprecationWarning(
     "The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead."
 )
@@ -85,3 +45,8 @@ WARNING_CAPTURED_HOOK = PytestDeprecationWarning(
     "The pytest_warning_captured is deprecated and will be removed in a future release.\n"
     "Please use pytest_warning_recorded instead."
 )
+
+FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestDeprecationWarning(
+    "The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; "
+    "use self.session.gethookproxy() and self.session.isinitpath() instead. "
+)
index ebf0d584cc3fa186f4c0e4dfedbc2918c7f8f3fd..c744bb369ea301013c8a90cce96e220e9350344a 100644 (file)
@@ -1,4 +1,4 @@
-""" discover and run doctests in modules and test files."""
+"""Discover and run doctests in modules and test files."""
 import bdb
 import inspect
 import platform
@@ -32,6 +32,7 @@ from _pytest.compat import TYPE_CHECKING
 from _pytest.config import Config
 from _pytest.config.argparsing import Parser
 from _pytest.fixtures import FixtureRequest
+from _pytest.nodes import Collector
 from _pytest.outcomes import OutcomeException
 from _pytest.pathlib import import_path
 from _pytest.python_api import approx
@@ -118,7 +119,7 @@ def pytest_unconfigure() -> None:
 
 
 def pytest_collect_file(
-    path: py.path.local, parent
+    path: py.path.local, parent: Collector,
 ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]:
     config = parent.config
     if path.ext == ".py":
@@ -171,9 +172,10 @@ def _init_runner_class() -> "Type[doctest.DocTestRunner]":
     import doctest
 
     class PytestDoctestRunner(doctest.DebugRunner):
-        """
-        Runner to collect failures.  Note that the out variable in this case is
-        a list instead of a stdout-like object
+        """Runner to collect failures.
+
+        Note that the out variable in this case is a list instead of a
+        stdout-like object.
         """
 
         def __init__(
@@ -261,9 +263,7 @@ class DoctestItem(pytest.Item):
         dtest: "doctest.DocTest"
     ):
         # incompatible signature due to to imposed limits on sublcass
-        """
-        the public named constructor
-        """
+        """The public named constructor."""
         return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
 
     def setup(self) -> None:
@@ -289,9 +289,7 @@ class DoctestItem(pytest.Item):
             raise MultipleDoctestFailures(failures)
 
     def _disable_output_capturing_for_darwin(self) -> None:
-        """
-        Disable output capturing. Otherwise, stdout is lost to doctest (#985)
-        """
+        """Disable output capturing. Otherwise, stdout is lost to doctest (#985)."""
         if platform.system() != "Darwin":
             return
         capman = self.config.pluginmanager.getplugin("capturemanager")
@@ -403,7 +401,7 @@ def _get_continue_on_failure(config):
     continue_on_failure = config.getvalue("doctest_continue_on_failure")
     if continue_on_failure:
         # We need to turn off this if we use pdb since we should stop at
-        # the first failure
+        # the first failure.
         if config.getvalue("usepdb"):
             continue_on_failure = False
     return continue_on_failure
@@ -415,8 +413,8 @@ class DoctestTextfile(pytest.Module):
     def collect(self) -> Iterable[DoctestItem]:
         import doctest
 
-        # inspired by doctest.testfile; ideally we would use it directly,
-        # but it doesn't support passing a custom checker
+        # Inspired by doctest.testfile; ideally we would use it directly,
+        # but it doesn't support passing a custom checker.
         encoding = self.config.getini("doctest_encoding")
         text = self.fspath.read_text(encoding)
         filename = str(self.fspath)
@@ -441,9 +439,8 @@ class DoctestTextfile(pytest.Module):
 
 
 def _check_all_skipped(test: "doctest.DocTest") -> None:
-    """raises pytest.skip() if all examples in the given DocTest have the SKIP
-    option set.
-    """
+    """Raise pytest.skip() if all examples in the given DocTest have the SKIP
+    option set."""
     import doctest
 
     all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
@@ -452,9 +449,8 @@ def _check_all_skipped(test: "doctest.DocTest") -> None:
 
 
 def _is_mocked(obj: object) -> bool:
-    """
-    returns if a object is possibly a mock object by checking the existence of a highly improbable attribute
-    """
+    """Return if an object is possibly a mock object by checking the
+    existence of a highly improbable attribute."""
     return (
         safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
         is not None
@@ -463,10 +459,8 @@ def _is_mocked(obj: object) -> bool:
 
 @contextmanager
 def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
-    """
-    contextmanager which replaces ``inspect.unwrap`` with a version
-    that's aware of mock objects and doesn't recurse on them
-    """
+    """Context manager which replaces ``inspect.unwrap`` with a version
+    that's aware of mock objects and doesn't recurse into them."""
     real_unwrap = inspect.unwrap
 
     def _mock_aware_unwrap(
@@ -498,16 +492,15 @@ class DoctestModule(pytest.Module):
         import doctest
 
         class MockAwareDocTestFinder(doctest.DocTestFinder):
-            """
-            a hackish doctest finder that overrides stdlib internals to fix a stdlib bug
+            """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.
 
             https://github.com/pytest-dev/pytest/issues/3456
             https://bugs.python.org/issue25532
             """
 
             def _find_lineno(self, obj, source_lines):
-                """
-                Doctest code does not take into account `@property`, this is a hackish way to fix it.
+                """Doctest code does not take into account `@property`, this
+                is a hackish way to fix it.
 
                 https://bugs.python.org/issue17446
                 """
@@ -542,7 +535,7 @@ class DoctestModule(pytest.Module):
                     pytest.skip("unable to import module %r" % self.fspath)
                 else:
                     raise
-        # uses internal doctest module parsing mechanism
+        # Uses internal doctest module parsing mechanism.
         finder = MockAwareDocTestFinder()
         optionflags = get_optionflags(self)
         runner = _get_runner(
@@ -560,9 +553,7 @@ class DoctestModule(pytest.Module):
 
 
 def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
-    """
-    Used by DoctestTextfile and DoctestItem to setup fixture information.
-    """
+    """Used by DoctestTextfile and DoctestItem to setup fixture information."""
 
     def func() -> None:
         pass
@@ -582,11 +573,9 @@ def _init_checker_class() -> "Type[doctest.OutputChecker]":
     import re
 
     class LiteralsOutputChecker(doctest.OutputChecker):
-        """
-        Based on doctest_nose_plugin.py from the nltk project
-        (https://github.com/nltk/nltk) and on the "numtest" doctest extension
-        by Sebastien Boisgerault (https://github.com/boisgera/numtest).
-        """
+        # Based on doctest_nose_plugin.py from the nltk project
+        # (https://github.com/nltk/nltk) and on the "numtest" doctest extension
+        # by Sebastien Boisgerault (https://github.com/boisgera/numtest).
 
         _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
         _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
@@ -647,8 +636,8 @@ def _init_checker_class() -> "Type[doctest.OutputChecker]":
                 return got
             offset = 0
             for w, g in zip(wants, gots):
-                fraction = w.group("fraction")
-                exponent = w.group("exponent1")
+                fraction = w.group("fraction")  # type: Optional[str]
+                exponent = w.group("exponent1")  # type: Optional[str]
                 if exponent is None:
                     exponent = w.group("exponent2")
                 if fraction is None:
@@ -671,8 +660,7 @@ def _init_checker_class() -> "Type[doctest.OutputChecker]":
 
 
 def _get_checker() -> "doctest.OutputChecker":
-    """
-    Returns a doctest.OutputChecker subclass that supports some
+    """Return a doctest.OutputChecker subclass that supports some
     additional options:
 
     * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
@@ -692,36 +680,31 @@ def _get_checker() -> "doctest.OutputChecker":
 
 
 def _get_allow_unicode_flag() -> int:
-    """
-    Registers and returns the ALLOW_UNICODE flag.
-    """
+    """Register and return the ALLOW_UNICODE flag."""
     import doctest
 
     return doctest.register_optionflag("ALLOW_UNICODE")
 
 
 def _get_allow_bytes_flag() -> int:
-    """
-    Registers and returns the ALLOW_BYTES flag.
-    """
+    """Register and return the ALLOW_BYTES flag."""
     import doctest
 
     return doctest.register_optionflag("ALLOW_BYTES")
 
 
 def _get_number_flag() -> int:
-    """
-    Registers and returns the NUMBER flag.
-    """
+    """Register and return the NUMBER flag."""
     import doctest
 
     return doctest.register_optionflag("NUMBER")
 
 
 def _get_report_choice(key: str) -> int:
-    """
-    This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid
-    importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests.
+    """Return the actual `doctest` module flag value.
+
+    We want to do it as late as possible to avoid importing `doctest` and all
+    its dependencies when parsing options, as it adds overhead and breaks tests.
     """
     import doctest
 
@@ -736,7 +719,6 @@ def _get_report_choice(key: str) -> int:
 
 @pytest.fixture(scope="session")
 def doctest_namespace() -> Dict[str, Any]:
-    """
-    Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests.
-    """
+    """Fixture that returns a :py:class:`dict` that will be injected into the
+    namespace of doctests."""
     return dict()
index 0d969840b3d988b088930e1d393159349bd91e21..d0cc0430c490dd4923ff6818a333df67bf74fd73 100644 (file)
@@ -30,18 +30,15 @@ def pytest_configure(config: Config) -> None:
         # of enabling faulthandler before each test executes.
         config.pluginmanager.register(FaultHandlerHooks(), "faulthandler-hooks")
     else:
-        from _pytest.warnings import _issue_warning_captured
-
         # Do not handle dumping to stderr if faulthandler is already enabled, so warn
         # users that the option is being ignored.
         timeout = FaultHandlerHooks.get_timeout_config_value(config)
         if timeout > 0:
-            _issue_warning_captured(
+            config.issue_config_time_warning(
                 pytest.PytestConfigWarning(
                     "faulthandler module enabled before pytest configuration step, "
                     "'faulthandler_timeout' option ignored"
                 ),
-                config.hook,
                 stacklevel=2,
             )
 
@@ -100,8 +97,7 @@ class FaultHandlerHooks:
 
     @pytest.hookimpl(tryfirst=True)
     def pytest_enter_pdb(self) -> None:
-        """Cancel any traceback dumping due to timeout before entering pdb.
-        """
+        """Cancel any traceback dumping due to timeout before entering pdb."""
         import faulthandler
 
         faulthandler.cancel_dump_traceback_later()
@@ -109,8 +105,7 @@ class FaultHandlerHooks:
     @pytest.hookimpl(tryfirst=True)
     def pytest_exception_interact(self) -> None:
         """Cancel any traceback dumping due to an interactive exception being
-        raised.
-        """
+        raised."""
         import faulthandler
 
         faulthandler.cancel_dump_traceback_later()
index 9521a7a17e8d0759906e43e1db4d8ad3b25ab572..f526f484b291ec18f13537f8d8ee3e188ef1768a 100644 (file)
@@ -1,6 +1,6 @@
 import functools
 import inspect
-import itertools
+import os
 import sys
 import warnings
 from collections import defaultdict
@@ -32,6 +32,7 @@ from _pytest._code.code import TerminalRepr
 from _pytest._io import TerminalWriter
 from _pytest.compat import _format_args
 from _pytest.compat import _PytestWrapper
+from _pytest.compat import final
 from _pytest.compat import get_real_func
 from _pytest.compat import get_real_method
 from _pytest.compat import getfuncargnames
@@ -46,11 +47,12 @@ from _pytest.compat import TYPE_CHECKING
 from _pytest.config import _PluggyPlugin
 from _pytest.config import Config
 from _pytest.config.argparsing import Parser
-from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
-from _pytest.deprecated import FUNCARGNAMES
+from _pytest.deprecated import FILLFUNCARGS
+from _pytest.mark import Mark
 from _pytest.mark import ParameterSet
 from _pytest.outcomes import fail
 from _pytest.outcomes import TEST_OUTCOME
+from _pytest.pathlib import absolutepath
 
 if TYPE_CHECKING:
     from typing import Deque
@@ -95,8 +97,8 @@ _FixtureCachedResult = Union[
 
 
 @attr.s(frozen=True)
-class PseudoFixtureDef:
-    cached_result = attr.ib(type="_FixtureCachedResult")
+class PseudoFixtureDef(Generic[_FixtureValue]):
+    cached_result = attr.ib(type="_FixtureCachedResult[_FixtureValue]")
     scope = attr.ib(type="_Scope")
 
 
@@ -118,31 +120,8 @@ def pytest_sessionstart(session: "Session") -> None:
 
 scopename2class = {}  # type: Dict[str, Type[nodes.Node]]
 
-scope2props = dict(session=())  # type: Dict[str, Tuple[str, ...]]
-scope2props["package"] = ("fspath",)
-scope2props["module"] = ("fspath", "module")
-scope2props["class"] = scope2props["module"] + ("cls",)
-scope2props["instance"] = scope2props["class"] + ("instance",)
-scope2props["function"] = scope2props["instance"] + ("function", "keywords")
 
-
-def scopeproperty(name=None, doc=None):
-    def decoratescope(func):
-        scopename = name or func.__name__
-
-        def provide(self):
-            if func.__name__ in scope2props[self.scope]:
-                return func(self)
-            raise AttributeError(
-                "{} not available in {}-scoped context".format(scopename, self.scope)
-            )
-
-        return property(provide, None, None, func.__doc__)
-
-    return decoratescope
-
-
-def get_scope_package(node, fixturedef: "FixtureDef"):
+def get_scope_package(node, fixturedef: "FixtureDef[object]"):
     import pytest
 
     cls = pytest.Package
@@ -167,15 +146,16 @@ def get_scope_node(node, scope):
 def add_funcarg_pseudo_fixture_def(
     collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
 ) -> None:
-    # this function will transform all collected calls to a functions
+    # This function will transform all collected calls to functions
     # if they use direct funcargs (i.e. direct parametrization)
     # because we want later test execution to be able to rely on
     # an existing FixtureDef structure for all arguments.
     # XXX we can probably avoid this algorithm  if we modify CallSpec2
     # to directly care for creating the fixturedefs within its methods.
     if not metafunc._calls[0].funcargs:
-        return  # this function call does not have direct parametrization
-    # collect funcargs of all callspecs into a list of values
+        # This function call does not have direct parametrization.
+        return
+    # Collect funcargs of all callspecs into a list of values.
     arg2params = {}  # type: Dict[str, List[object]]
     arg2scope = {}  # type: Dict[str, _Scope]
     for callspec in metafunc._calls:
@@ -190,11 +170,11 @@ def add_funcarg_pseudo_fixture_def(
                 arg2scope[argname] = scopes[scopenum]
         callspec.funcargs.clear()
 
-    # register artificial FixtureDef's so that later at test execution
+    # Register artificial FixtureDef's so that later at test execution
     # time we can rely on a proper FixtureDef to exist for fixture setup.
     arg2fixturedefs = metafunc._arg2fixturedefs
     for argname, valuelist in arg2params.items():
-        # if we have a scope that is higher than function we need
+        # If we have a scope that is higher than function, we need
         # to make sure we only ever create an according fixturedef on
         # a per-scope basis. We thus store and cache the fixturedef on the
         # node related to the scope.
@@ -204,7 +184,7 @@ def add_funcarg_pseudo_fixture_def(
             node = get_scope_node(collector, scope)
             if node is None:
                 assert scope == "class" and isinstance(collector, _pytest.python.Module)
-                # use module-level collector for class-scope (for now)
+                # Use module-level collector for class-scope (for now).
                 node = collector
         if node and argname in node._name2pseudofixturedef:
             arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]]
@@ -225,7 +205,7 @@ def add_funcarg_pseudo_fixture_def(
 
 
 def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
-    """ return fixturemarker or None if it doesn't exist or raised
+    """Return fixturemarker or None if it doesn't exist or raised
     exceptions."""
     try:
         fixturemarker = getattr(
@@ -243,7 +223,7 @@ _Key = Tuple[object, ...]
 
 
 def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator[_Key]:
-    """ return list of keys for all parametrized arguments which match
+    """Return list of keys for all parametrized arguments which match
     the specified scope. """
     assert scopenum < scopenum_function  # function
     try:
@@ -270,10 +250,10 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator
             yield key
 
 
-# algorithm for sorting on a per-parametrized resource setup basis
-# it is called for scopenum==0 (session) first and performs sorting
+# Algorithm for sorting on a per-parametrized resource setup basis.
+# It is called for scopenum==0 (session) first and performs sorting
 # down to the lower scopes such as to minimize number of "high scope"
-# setups and teardowns
+# setups and teardowns.
 
 
 def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]":
@@ -340,7 +320,8 @@ def reorder_items_atscope(
                 no_argkey_group[item] = None
             else:
                 slicing_argkey, _ = argkeys.popitem()
-                # we don't have to remove relevant items from later in the deque because they'll just be ignored
+                # We don't have to remove relevant items from later in the
+                # deque because they'll just be ignored.
                 matching_items = [
                     i for i in scoped_items_by_argkey[slicing_argkey] if i in items
                 ]
@@ -359,9 +340,8 @@ def reorder_items_atscope(
 
 
 def fillfixtures(function: "Function") -> None:
-    """ fill missing funcargs for a test function. """
-    # Uncomment this after 6.0 release (#7361)
-    # warnings.warn(FILLFUNCARGS, stacklevel=2)
+    """Fill missing funcargs for a test function."""
+    warnings.warn(FILLFUNCARGS, stacklevel=2)
     try:
         request = function._request
     except AttributeError:
@@ -374,7 +354,7 @@ def fillfixtures(function: "Function") -> None:
         function._fixtureinfo = fi
         request = function._request = FixtureRequest(function)
         request._fillfixtures()
-        # prune out funcargs for jstests
+        # Prune out funcargs for jstests.
         newfuncargs = {}
         for name in fi.argnames:
             newfuncargs[name] = function.funcargs[name]
@@ -389,17 +369,17 @@ def get_direct_param_fixture_func(request):
 
 @attr.s(slots=True)
 class FuncFixtureInfo:
-    # original function argument names
+    # Original function argument names.
     argnames = attr.ib(type=Tuple[str, ...])
-    # argnames that function immediately requires. These include argnames +
+    # Argnames that function immediately requires. These include argnames +
     # fixture names specified via usefixtures and via autouse=True in fixture
     # definitions.
     initialnames = attr.ib(type=Tuple[str, ...])
     names_closure = attr.ib(type=List[str])
-    name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef"]])
+    name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef[Any]"]])
 
     def prune_dependency_tree(self) -> None:
-        """Recompute names_closure from initialnames and name2fixturedefs
+        """Recompute names_closure from initialnames and name2fixturedefs.
 
         Can only reduce names_closure, which means that the new closure will
         always be a subset of the old one. The order is preserved.
@@ -413,7 +393,7 @@ class FuncFixtureInfo:
         working_set = set(self.initialnames)
         while working_set:
             argname = working_set.pop()
-            # argname may be smth not included in the original names_closure,
+            # Argname may be smth not included in the original names_closure,
             # in which case we ignore it. This currently happens with pseudo
             # FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
             # So they introduce the new dependency 'request' which might have
@@ -427,20 +407,20 @@ class FuncFixtureInfo:
 
 
 class FixtureRequest:
-    """ A request for a fixture from a test or fixture function.
+    """A request for a fixture from a test or fixture function.
 
-    A request object gives access to the requesting test context
-    and has an optional ``param`` attribute in case
-    the fixture is parametrized indirectly.
+    A request object gives access to the requesting test context and has
+    an optional ``param`` attribute in case the fixture is parametrized
+    indirectly.
     """
 
     def __init__(self, pyfuncitem) -> None:
         self._pyfuncitem = pyfuncitem
-        #: fixture for which this request is being performed
+        #: Fixture for which this request is being performed.
         self.fixturename = None  # type: Optional[str]
-        #: Scope string, one of "function", "class", "module", "session"
+        #: Scope string, one of "function", "class", "module", "session".
         self.scope = "function"  # type: _Scope
-        self._fixture_defs = {}  # type: Dict[str, FixtureDef]
+        self._fixture_defs = {}  # type: Dict[str, FixtureDef[Any]]
         fixtureinfo = pyfuncitem._fixtureinfo  # type: FuncFixtureInfo
         self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
         self._arg2index = {}  # type: Dict[str, int]
@@ -450,35 +430,29 @@ class FixtureRequest:
 
     @property
     def fixturenames(self) -> List[str]:
-        """names of all active fixtures in this request"""
+        """Names of all active fixtures in this request."""
         result = list(self._pyfuncitem._fixtureinfo.names_closure)
         result.extend(set(self._fixture_defs).difference(result))
         return result
 
-    @property
-    def funcargnames(self) -> List[str]:
-        """ alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
-        warnings.warn(FUNCARGNAMES, stacklevel=2)
-        return self.fixturenames
-
     @property
     def node(self):
-        """ underlying collection node (depends on current request scope)"""
+        """Underlying collection node (depends on current request scope)."""
         return self._getscopeitem(self.scope)
 
-    def _getnextfixturedef(self, argname: str) -> "FixtureDef":
+    def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
         fixturedefs = self._arg2fixturedefs.get(argname, None)
         if fixturedefs is None:
-            # we arrive here because of a dynamic call to
+            # We arrive here because of a dynamic call to
             # getfixturevalue(argname) usage which was naturally
-            # not known at parsing/collection time
+            # not known at parsing/collection time.
             assert self._pyfuncitem.parent is not None
             parentid = self._pyfuncitem.parent.nodeid
             fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
             # TODO: Fix this type ignore. Either add assert or adjust types.
             #       Can this be None here?
             self._arg2fixturedefs[argname] = fixturedefs  # type: ignore[assignment]
-        # fixturedefs list is immutable so we maintain a decreasing index
+        # fixturedefs list is immutable so we maintain a decreasing index.
         index = self._arg2index.get(argname, 0) - 1
         if fixturedefs is None or (-index > len(fixturedefs)):
             raise FixtureLookupError(argname, self)
@@ -487,57 +461,72 @@ class FixtureRequest:
 
     @property
     def config(self) -> Config:
-        """ the pytest config object associated with this request. """
+        """The pytest config object associated with this request."""
         return self._pyfuncitem.config  # type: ignore[no-any-return] # noqa: F723
 
-    @scopeproperty()
+    @property
     def function(self):
-        """ test function object if the request has a per-function scope. """
+        """Test function object if the request has a per-function scope."""
+        if self.scope != "function":
+            raise AttributeError(
+                "function not available in {}-scoped context".format(self.scope)
+            )
         return self._pyfuncitem.obj
 
-    @scopeproperty("class")
+    @property
     def cls(self):
-        """ class (can be None) where the test function was collected. """
+        """Class (can be None) where the test function was collected."""
+        if self.scope not in ("class", "function"):
+            raise AttributeError(
+                "cls not available in {}-scoped context".format(self.scope)
+            )
         clscol = self._pyfuncitem.getparent(_pytest.python.Class)
         if clscol:
             return clscol.obj
 
     @property
     def instance(self):
-        """ instance (can be None) on which test function was collected. """
-        # unittest support hack, see _pytest.unittest.TestCaseFunction
+        """Instance (can be None) on which test function was collected."""
+        # unittest support hack, see _pytest.unittest.TestCaseFunction.
         try:
             return self._pyfuncitem._testcase
         except AttributeError:
             function = getattr(self, "function", None)
             return getattr(function, "__self__", None)
 
-    @scopeproperty()
+    @property
     def module(self):
-        """ python module object where the test function was collected. """
+        """Python module object where the test function was collected."""
+        if self.scope not in ("function", "class", "module"):
+            raise AttributeError(
+                "module not available in {}-scoped context".format(self.scope)
+            )
         return self._pyfuncitem.getparent(_pytest.python.Module).obj
 
-    @scopeproperty()
+    @property
     def fspath(self) -> py.path.local:
-        """ the file system path of the test module which collected this test. """
+        """The file system path of the test module which collected this test."""
+        if self.scope not in ("function", "class", "module", "package"):
+            raise AttributeError(
+                "module not available in {}-scoped context".format(self.scope)
+            )
         # TODO: Remove ignore once _pyfuncitem is properly typed.
         return self._pyfuncitem.fspath  # type: ignore
 
     @property
     def keywords(self):
-        """ keywords/markers dictionary for the underlying node. """
+        """Keywords/markers dictionary for the underlying node."""
         return self.node.keywords
 
     @property
     def session(self):
-        """ pytest session object. """
+        """Pytest session object."""
         return self._pyfuncitem.session
 
     def addfinalizer(self, finalizer: Callable[[], object]) -> None:
-        """ add finalizer/teardown function to be called after the
-        last test within the requesting test context finished
-        execution. """
-        # XXX usually this method is shadowed by fixturedef specific ones
+        """Add finalizer/teardown function to be called after the last test
+        within the requesting test context finished execution."""
+        # XXX usually this method is shadowed by fixturedef specific ones.
         self._addfinalizer(finalizer, scope=self.scope)
 
     def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
@@ -547,17 +536,19 @@ class FixtureRequest:
         )
 
     def applymarker(self, marker) -> None:
-        """ Apply a marker to a single test function invocation.
+        """Apply a marker to a single test function invocation.
+
         This method is useful if you don't want to have a keyword/marker
         on all function invocations.
 
-        :arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
-            created by a call to ``pytest.mark.NAME(...)``.
+        :param marker:
+            A :py:class:`_pytest.mark.MarkDecorator` object created by a call
+            to ``pytest.mark.NAME(...)``.
         """
         self.node.add_marker(marker)
 
     def raiseerror(self, msg: Optional[str]) -> "NoReturn":
-        """ raise a FixtureLookupError with the given message. """
+        """Raise a FixtureLookupError with the given message."""
         raise self._fixturemanager.FixtureLookupError(None, self, msg)
 
     def _fillfixtures(self) -> None:
@@ -568,14 +559,14 @@ class FixtureRequest:
                 item.funcargs[argname] = self.getfixturevalue(argname)
 
     def getfixturevalue(self, argname: str) -> Any:
-        """ Dynamically run a named fixture function.
+        """Dynamically run a named fixture function.
 
         Declaring fixtures via function argument is recommended where possible.
         But if you can only decide whether to use another fixture at test
         setup time, you may use this function to retrieve it inside a fixture
         or test function body.
 
-        :raise pytest.FixtureLookupError:
+        :raises pytest.FixtureLookupError:
             If the given fixture could not be found.
         """
         fixturedef = self._get_active_fixturedef(argname)
@@ -584,7 +575,7 @@ class FixtureRequest:
 
     def _get_active_fixturedef(
         self, argname: str
-    ) -> Union["FixtureDef", PseudoFixtureDef]:
+    ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
         try:
             return self._fixture_defs[argname]
         except KeyError:
@@ -596,15 +587,15 @@ class FixtureRequest:
                     scope = "function"  # type: _Scope
                     return PseudoFixtureDef(cached_result, scope)
                 raise
-        # remove indent to prevent the python3 exception
-        # from leaking into the call
+        # Remove indent to prevent the python3 exception
+        # from leaking into the call.
         self._compute_fixture_value(fixturedef)
         self._fixture_defs[argname] = fixturedef
         return fixturedef
 
-    def _get_fixturestack(self) -> List["FixtureDef"]:
+    def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
         current = self
-        values = []  # type: List[FixtureDef]
+        values = []  # type: List[FixtureDef[Any]]
         while 1:
             fixturedef = getattr(current, "_fixturedef", None)
             if fixturedef is None:
@@ -614,11 +605,13 @@ class FixtureRequest:
             assert isinstance(current, SubRequest)
             current = current._parent_request
 
-    def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None:
-        """
-        Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will
-        force the FixtureDef object to throw away any previous results and compute a new fixture value, which
-        will be stored into the FixtureDef object itself.
+    def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
+        """Create a SubRequest based on "self" and call the execute method
+        of the given FixtureDef object.
+
+        This will force the FixtureDef object to throw away any previous
+        results and compute a new fixture value, which will be stored into
+        the FixtureDef object itself.
         """
         # prepare a subrequest object before calling fixture function
         # (latter managed by fixturedef)
@@ -668,26 +661,26 @@ class FixtureRequest:
                 fail(msg, pytrace=False)
         else:
             param_index = funcitem.callspec.indices[argname]
-            # if a parametrize invocation set a scope it will override
-            # the static scope defined with the fixture function
+            # If a parametrize invocation set a scope it will override
+            # the static scope defined with the fixture function.
             paramscopenum = funcitem.callspec._arg2scopenum.get(argname)
             if paramscopenum is not None:
                 scope = scopes[paramscopenum]
 
         subrequest = SubRequest(self, scope, param, param_index, fixturedef)
 
-        # check if a higher-level scoped fixture accesses a lower level one
+        # Check if a higher-level scoped fixture accesses a lower level one.
         subrequest._check_scope(argname, self.scope, scope)
         try:
-            # call the fixture function
+            # Call the fixture function.
             fixturedef.execute(request=subrequest)
         finally:
             self._schedule_finalizers(fixturedef, subrequest)
 
     def _schedule_finalizers(
-        self, fixturedef: "FixtureDef", subrequest: "SubRequest"
+        self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
     ) -> None:
-        # if fixture function failed it might have registered finalizers
+        # If fixture function failed it might have registered finalizers.
         self.session._setupstate.addfinalizer(
             functools.partial(fixturedef.finish, request=subrequest), subrequest.node
         )
@@ -696,7 +689,7 @@ class FixtureRequest:
         if argname == "request":
             return
         if scopemismatch(invoking_scope, requested_scope):
-            # try to report something helpful
+            # Try to report something helpful.
             lines = self._factorytraceback()
             fail(
                 "ScopeMismatch: You tried to access the %r scoped "
@@ -718,7 +711,7 @@ class FixtureRequest:
 
     def _getscopeitem(self, scope):
         if scope == "function":
-            # this might also be a non-function Item despite its attribute name
+            # This might also be a non-function Item despite its attribute name.
             return self._pyfuncitem
         if scope == "package":
             # FIXME: _fixturedef is not defined on FixtureRequest (this class),
@@ -727,7 +720,7 @@ class FixtureRequest:
         else:
             node = get_scope_node(self._pyfuncitem, scope)
         if node is None and scope == "class":
-            # fallback to function item itself
+            # Fallback to function item itself.
             node = self._pyfuncitem
         assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
             scope, self._pyfuncitem
@@ -738,9 +731,9 @@ class FixtureRequest:
         return "<FixtureRequest for %r>" % (self.node)
 
 
+@final
 class SubRequest(FixtureRequest):
-    """ a sub request for handling getting a fixture from a
-    test function/fixture. """
+    """A sub request for handling getting a fixture from a test function/fixture."""
 
     def __init__(
         self,
@@ -748,10 +741,10 @@ class SubRequest(FixtureRequest):
         scope: "_Scope",
         param,
         param_index: int,
-        fixturedef: "FixtureDef",
+        fixturedef: "FixtureDef[object]",
     ) -> None:
         self._parent_request = request
-        self.fixturename = fixturedef.argname  # type: str
+        self.fixturename = fixturedef.argname
         if param is not NOTSET:
             self.param = param
         self.param_index = param_index
@@ -770,11 +763,11 @@ class SubRequest(FixtureRequest):
         self._fixturedef.addfinalizer(finalizer)
 
     def _schedule_finalizers(
-        self, fixturedef: "FixtureDef", subrequest: "SubRequest"
+        self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
     ) -> None:
-        # if the executing fixturedef was not explicitly requested in the argument list (via
+        # If the executing fixturedef was not explicitly requested in the argument list (via
         # getfixturevalue inside the fixture call) then ensure this fixture def will be finished
-        # first
+        # first.
         if fixturedef.argname not in self.fixturenames:
             fixturedef.addfinalizer(
                 functools.partial(self._fixturedef.finish, request=self)
@@ -792,8 +785,7 @@ def scopemismatch(currentscope: "_Scope", newscope: "_Scope") -> bool:
 
 def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int:
     """Look up the index of ``scope`` and raise a descriptive value error
-    if not defined.
-    """
+    if not defined."""
     strscopes = scopes  # type: Sequence[str]
     try:
         return strscopes.index(scope)
@@ -806,8 +798,9 @@ def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int:
         )
 
 
+@final
 class FixtureLookupError(LookupError):
-    """ could not return a requested Fixture (missing or invalid). """
+    """Could not return a requested fixture (missing or invalid)."""
 
     def __init__(
         self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = None
@@ -824,8 +817,8 @@ class FixtureLookupError(LookupError):
         stack.extend(map(lambda x: x.func, self.fixturestack))
         msg = self.msg
         if msg is not None:
-            # the last fixture raise an error, let's present
-            # it at the requesting side
+            # The last fixture raise an error, let's present
+            # it at the requesting side.
             stack = stack[:-1]
         for function in stack:
             fspath, lineno = getfslineno(function)
@@ -926,8 +919,9 @@ def call_fixture_func(
 
 
 def _teardown_yield_fixture(fixturefunc, it) -> None:
-    """Executes the teardown of a fixture function by advancing the iterator after the
-    yield and ensure the iteration ends (if not it means there is more than one yield in the function)"""
+    """Execute the teardown of a fixture function by advancing the iterator
+    after the yield and ensure the iteration ends (if not it means there is
+    more than one yield in the function)."""
     try:
         next(it)
     except StopIteration:
@@ -961,8 +955,9 @@ def _eval_scope_callable(
     return result
 
 
+@final
 class FixtureDef(Generic[_FixtureValue]):
-    """ A container for a factory definition. """
+    """A container for a factory definition."""
 
     def __init__(
         self,
@@ -990,7 +985,8 @@ class FixtureDef(Generic[_FixtureValue]):
         else:
             scope_ = scope
         self.scopenum = scope2index(
-            scope_ or "function",
+            # TODO: Check if the `or` here is really necessary.
+            scope_ or "function",  # type: ignore[unreachable]
             descr="Fixture '{}'".format(func.__name__),
             where=baseid,
         )
@@ -1024,16 +1020,15 @@ class FixtureDef(Generic[_FixtureValue]):
         finally:
             hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
             hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
-            # even if finalization fails, we invalidate
-            # the cached fixture value and remove
-            # all finalizers because they may be bound methods which will
-            # keep instances alive
+            # Even if finalization fails, we invalidate the cached fixture
+            # value and remove all finalizers because they may be bound methods
+            # which will keep instances alive.
             self.cached_result = None
             self._finalizers = []
 
     def execute(self, request: SubRequest) -> _FixtureValue:
-        # get required arguments and register our own finish()
-        # with their finalization
+        # Get required arguments and register our own finish()
+        # with their finalization.
         for argname in self.argnames:
             fixturedef = request._get_active_fixturedef(argname)
             if argname != "request":
@@ -1044,7 +1039,7 @@ class FixtureDef(Generic[_FixtureValue]):
         my_cache_key = self.cache_key(request)
         if self.cached_result is not None:
             # note: comparison with `==` can fail (or be expensive) for e.g.
-            # numpy arrays (#6497)
+            # numpy arrays (#6497).
             cache_key = self.cached_result[1]
             if my_cache_key is cache_key:
                 if self.cached_result[2] is not None:
@@ -1053,8 +1048,8 @@ class FixtureDef(Generic[_FixtureValue]):
                 else:
                     result = self.cached_result[0]
                     return result
-            # we have a previous but differently parametrized fixture instance
-            # so we need to tear it down before creating a new one
+            # We have a previous but differently parametrized fixture instance
+            # so we need to tear it down before creating a new one.
             self.finish(request)
             assert self.cached_result is None
 
@@ -1074,21 +1069,20 @@ class FixtureDef(Generic[_FixtureValue]):
 def resolve_fixture_function(
     fixturedef: FixtureDef[_FixtureValue], request: FixtureRequest
 ) -> "_FixtureFunc[_FixtureValue]":
-    """Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific
-    instances and bound methods.
-    """
+    """Get the actual callable that can be called to obtain the fixture
+    value, dealing with unittest-specific instances and bound methods."""
     fixturefunc = fixturedef.func
     if fixturedef.unittest:
         if request.instance is not None:
-            # bind the unbound method to the TestCase instance
+            # Bind the unbound method to the TestCase instance.
             fixturefunc = fixturedef.func.__get__(request.instance)  # type: ignore[union-attr]
     else:
-        # the fixture function needs to be bound to the actual
+        # The fixture function needs to be bound to the actual
         # request.instance so that code working with "fixturedef" behaves
         # as expected.
         if request.instance is not None:
-            # handle the case where fixture is defined not in a test class, but some other class
-            # (for example a plugin class with a fixture), see #2270
+            # Handle the case where fixture is defined not in a test class, but some other class
+            # (for example a plugin class with a fixture), see #2270.
             if hasattr(fixturefunc, "__self__") and not isinstance(
                 request.instance, fixturefunc.__self__.__class__  # type: ignore[union-attr]
             ):
@@ -1102,7 +1096,7 @@ def resolve_fixture_function(
 def pytest_fixture_setup(
     fixturedef: FixtureDef[_FixtureValue], request: SubRequest
 ) -> _FixtureValue:
-    """ Execution of fixture setup. """
+    """Execution of fixture setup."""
     kwargs = {}
     for argname in fixturedef.argnames:
         fixdef = request._get_active_fixturedef(argname)
@@ -1152,8 +1146,7 @@ def _params_converter(
 
 def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
     """Wrap the given fixture function so we can raise an error about it being called directly,
-    instead of used as an argument in a test function.
-    """
+    instead of used as an argument in a test function."""
     message = (
         'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
         "but are created automatically when test functions request them as parameters.\n"
@@ -1165,13 +1158,14 @@ def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
     def result(*args, **kwargs):
         fail(message, pytrace=False)
 
-    # keep reference to the original function in our own custom attribute so we don't unwrap
-    # further than this point and lose useful wrappings like @mock.patch (#3774)
+    # Keep reference to the original function in our own custom attribute so we don't unwrap
+    # further than this point and lose useful wrappings like @mock.patch (#3774).
     result.__pytest_wrapped__ = _PytestWrapper(function)  # type: ignore[attr-defined]
 
     return result
 
 
+@final
 @attr.s(frozen=True)
 class FixtureFunctionMarker:
     scope = attr.ib(type="Union[_Scope, Callable[[str, Config], _Scope]]")
@@ -1228,7 +1222,7 @@ def fixture(
     ] = ...,
     name: Optional[str] = ...
 ) -> _FixtureFunction:
-    raise NotImplementedError()
+    ...
 
 
 @overload  # noqa: F811
@@ -1246,12 +1240,12 @@ def fixture(  # noqa: F811
     ] = ...,
     name: Optional[str] = None
 ) -> FixtureFunctionMarker:
-    raise NotImplementedError()
+    ...
 
 
 def fixture(  # noqa: F811
     fixture_function: Optional[_FixtureFunction] = None,
-    *args: Any,
+    *,
     scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function",
     params: Optional[Iterable[object]] = None,
     autouse: bool = False,
@@ -1269,95 +1263,50 @@ def fixture(  # noqa: F811
     fixture function.
 
     The name of the fixture function can later be referenced to cause its
-    invocation ahead of running tests: test
-    modules or classes can use the ``pytest.mark.usefixtures(fixturename)``
-    marker.
-
-    Test functions can directly use fixture names as input
-    arguments in which case the fixture instance returned from the fixture
-    function will be injected.
-
-    Fixtures can provide their values to test functions using ``return`` or ``yield``
-    statements. When using ``yield`` the code block after the ``yield`` statement is executed
-    as teardown code regardless of the test outcome, and must yield exactly once.
-
-    :arg scope: the scope for which this fixture is shared, one of
-                ``"function"`` (default), ``"class"``, ``"module"``,
-                ``"package"`` or ``"session"``.
-
-                This parameter may also be a callable which receives ``(fixture_name, config)``
-                as parameters, and must return a ``str`` with one of the values mentioned above.
-
-                See :ref:`dynamic scope` in the docs for more information.
-
-    :arg params: an optional list of parameters which will cause multiple
-                invocations of the fixture function and all of the tests
-                using it.
-                The current parameter is available in ``request.param``.
-
-    :arg autouse: if True, the fixture func is activated for all tests that
-                can see it.  If False (the default) then an explicit
-                reference is needed to activate the fixture.
-
-    :arg ids: list of string ids each corresponding to the params
-                so that they are part of the test id. If no ids are provided
-                they will be generated automatically from the params.
-
-    :arg name: the name of the fixture. This defaults to the name of the
-                decorated function. If a fixture is used in the same module in
-                which it is defined, the function name of the fixture will be
-                shadowed by the function arg that requests the fixture; one way
-                to resolve this is to name the decorated function
-                ``fixture_<fixturename>`` and then use
-                ``@pytest.fixture(name='<fixturename>')``.
+    invocation ahead of running tests: test modules or classes can use the
+    ``pytest.mark.usefixtures(fixturename)`` marker.
+
+    Test functions can directly use fixture names as input arguments in which
+    case the fixture instance returned from the fixture function will be
+    injected.
+
+    Fixtures can provide their values to test functions using ``return`` or
+    ``yield`` statements. When using ``yield`` the code block after the
+    ``yield`` statement is executed as teardown code regardless of the test
+    outcome, and must yield exactly once.
+
+    :param scope:
+        The scope for which this fixture is shared; one of ``"function"``
+        (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``.
+
+        This parameter may also be a callable which receives ``(fixture_name, config)``
+        as parameters, and must return a ``str`` with one of the values mentioned above.
+
+        See :ref:`dynamic scope` in the docs for more information.
+
+    :param params:
+        An optional list of parameters which will cause multiple invocations
+        of the fixture function and all of the tests using it. The current
+        parameter is available in ``request.param``.
+
+    :param autouse:
+        If True, the fixture func is activated for all tests that can see it.
+        If False (the default), an explicit reference is needed to activate
+        the fixture.
+
+    :param ids:
+        List of string ids each corresponding to the params so that they are
+        part of the test id. If no ids are provided they will be generated
+        automatically from the params.
+
+    :param name:
+        The name of the fixture. This defaults to the name of the decorated
+        function. If a fixture is used in the same module in which it is
+        defined, the function name of the fixture will be shadowed by the
+        function arg that requests the fixture; one way to resolve this is to
+        name the decorated function ``fixture_<fixturename>`` and then use
+        ``@pytest.fixture(name='<fixturename>')``.
     """
-    # Positional arguments backward compatibility.
-    # If a kwarg is equal to its default, assume it was not explicitly
-    # passed, i.e. not duplicated. The more correct way is to use a
-    # **kwargs and check `in`, but that obfuscates the function signature.
-    if isinstance(fixture_function, str):
-        # It's actually the first positional argument, scope.
-        args = (fixture_function, *args)
-        fixture_function = None
-    duplicated_args = []
-    if len(args) > 0:
-        if scope == "function":
-            scope = args[0]
-        else:
-            duplicated_args.append("scope")
-    if len(args) > 1:
-        if params is None:
-            params = args[1]
-        else:
-            duplicated_args.append("params")
-    if len(args) > 2:
-        if autouse is False:
-            autouse = args[2]
-        else:
-            duplicated_args.append("autouse")
-    if len(args) > 3:
-        if ids is None:
-            ids = args[3]
-        else:
-            duplicated_args.append("ids")
-    if len(args) > 4:
-        if name is None:
-            name = args[4]
-        else:
-            duplicated_args.append("name")
-    if len(args) > 5:
-        raise TypeError(
-            "fixture() takes 5 positional arguments but {} were given".format(len(args))
-        )
-    if duplicated_args:
-        raise TypeError(
-            "The fixture arguments are defined as positional and keyword: {}. "
-            "Use only keyword arguments.".format(", ".join(duplicated_args))
-        )
-    if args:
-        warnings.warn(FIXTURE_POSITIONAL_ARGUMENTS, stacklevel=2)
-    # End backward compatiblity.
-
     fixture_marker = FixtureFunctionMarker(
         scope=scope, params=params, autouse=autouse, ids=ids, name=name,
     )
@@ -1378,7 +1327,7 @@ def yield_fixture(
     ids=None,
     name=None
 ):
-    """ (return a) decorator to mark a yield-fixture factory function.
+    """(Return a) decorator to mark a yield-fixture factory function.
 
     .. deprecated:: 3.0
         Use :py:func:`pytest.fixture` directly instead.
@@ -1418,8 +1367,7 @@ def pytest_addoption(parser: Parser) -> None:
 
 
 class FixtureManager:
-    """
-    pytest fixtures definitions and information is stored and managed
+    """pytest fixture definitions and information is stored and managed
     from this class.
 
     During collection fm.parsefactories() is called multiple times to parse
@@ -1432,7 +1380,7 @@ class FixtureManager:
     which themselves offer a fixturenames attribute.
 
     The FuncFixtureInfo object holds information about fixtures and FixtureDefs
-    relevant for a particular function.  An initial list of fixtures is
+    relevant for a particular function. An initial list of fixtures is
     assembled like this:
 
     - ini-defined usefixtures
@@ -1442,7 +1390,7 @@ class FixtureManager:
 
     Subsequently the funcfixtureinfo.fixturenames attribute is computed
     as the closure of the fixtures needed to setup the initial fixtures,
-    i. e. fixtures needed by fixture functions themselves are appended
+    i.e. fixtures needed by fixture functions themselves are appended
     to the fixturenames list.
 
     Upon the test-setup phases all fixturenames are instantiated, retrieved
@@ -1455,21 +1403,21 @@ class FixtureManager:
     def __init__(self, session: "Session") -> None:
         self.session = session
         self.config = session.config  # type: Config
-        self._arg2fixturedefs = {}  # type: Dict[str, List[FixtureDef]]
-        self._holderobjseen = set()  # type: Set
+        self._arg2fixturedefs = {}  # type: Dict[str, List[FixtureDef[Any]]]
+        self._holderobjseen = set()  # type: Set[object]
         self._nodeid_and_autousenames = [
             ("", self.config.getini("usefixtures"))
         ]  # type: List[Tuple[str, List[str]]]
         session.config.pluginmanager.register(self, "funcmanage")
 
     def _get_direct_parametrize_args(self, node: "nodes.Node") -> List[str]:
-        """This function returns all the direct parametrization
-        arguments of a node, so we don't mistake them for fixtures
+        """Return all direct parametrization arguments of a node, so we don't
+        mistake them for fixtures.
 
-        Check https://github.com/pytest-dev/pytest/issues/5036
+        Check https://github.com/pytest-dev/pytest/issues/5036.
 
-        This things are done later as well when dealing with parametrization
-        so this could be improved
+        These things are done later as well when dealing with parametrization
+        so this could be improved.
         """
         parametrize_argnames = []  # type: List[str]
         for marker in node.iter_markers(name="parametrize"):
@@ -1489,10 +1437,10 @@ class FixtureManager:
         else:
             argnames = ()
 
-        usefixtures = itertools.chain.from_iterable(
-            mark.args for mark in node.iter_markers(name="usefixtures")
+        usefixtures = tuple(
+            arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
         )
-        initialnames = tuple(usefixtures) + argnames
+        initialnames = usefixtures + argnames
         fm = node.session._fixturemanager
         initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
             initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
@@ -1502,24 +1450,29 @@ class FixtureManager:
     def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
         nodeid = None
         try:
-            p = py.path.local(plugin.__file__)  # type: ignore[attr-defined]
+            p = absolutepath(plugin.__file__)  # type: ignore[attr-defined]
         except AttributeError:
             pass
         else:
             from _pytest import nodes
 
-            # construct the base nodeid which is later used to check
+            # Construct the base nodeid which is later used to check
             # what fixtures are visible for particular tests (as denoted
-            # by their test id)
-            if p.basename.startswith("conftest.py"):
-                nodeid = p.dirpath().relto(self.config.rootdir)
-                if p.sep != nodes.SEP:
-                    nodeid = nodeid.replace(p.sep, nodes.SEP)
+            # by their test id).
+            if p.name.startswith("conftest.py"):
+                try:
+                    nodeid = str(p.parent.relative_to(self.config.rootpath))
+                except ValueError:
+                    nodeid = ""
+                if nodeid == ".":
+                    nodeid = ""
+                if os.sep != nodes.SEP:
+                    nodeid = nodeid.replace(os.sep, nodes.SEP)
 
         self.parsefactories(plugin, nodeid)
 
     def _getautousenames(self, nodeid: str) -> List[str]:
-        """ return a tuple of fixture names to be used. """
+        """Return a list of fixture names to be used."""
         autousenames = []  # type: List[str]
         for baseid, basenames in self._nodeid_and_autousenames:
             if nodeid.startswith(baseid):
@@ -1533,13 +1486,13 @@ class FixtureManager:
 
     def getfixtureclosure(
         self, fixturenames: Tuple[str, ...], parentnode, ignore_args: Sequence[str] = ()
-    ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef]]]:
-        # collect the closure of all fixtures , starting with the given
+    ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
+        # Collect the closure of all fixtures, starting with the given
         # fixturenames as the initial set.  As we have to visit all
         # factory definitions anyway, we also return an arg2fixturedefs
         # mapping so that the caller can reuse it and does not have
         # to re-discover fixturedefs again for each fixturename
-        # (discovering matching fixtures for a given name/node is expensive)
+        # (discovering matching fixtures for a given name/node is expensive).
 
         parentid = parentnode.nodeid
         fixturenames_closure = self._getautousenames(parentid)
@@ -1551,12 +1504,12 @@ class FixtureManager:
 
         merge(fixturenames)
 
-        # at this point, fixturenames_closure contains what we call "initialnames",
+        # At this point, fixturenames_closure contains what we call "initialnames",
         # which is a set of fixturenames the function immediately requests. We
         # need to return it as well, so save this.
         initialnames = tuple(fixturenames_closure)
 
-        arg2fixturedefs = {}  # type: Dict[str, Sequence[FixtureDef]]
+        arg2fixturedefs = {}  # type: Dict[str, Sequence[FixtureDef[Any]]]
         lastlen = -1
         while lastlen != len(fixturenames_closure):
             lastlen = len(fixturenames_closure)
@@ -1582,37 +1535,52 @@ class FixtureManager:
         return initialnames, fixturenames_closure, arg2fixturedefs
 
     def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
+        """Generate new tests based on parametrized fixtures used by the given metafunc"""
+
+        def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]:
+            args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs)
+            return args
+
         for argname in metafunc.fixturenames:
-            faclist = metafunc._arg2fixturedefs.get(argname)
-            if faclist:
-                fixturedef = faclist[-1]
+            # Get the FixtureDefs for the argname.
+            fixture_defs = metafunc._arg2fixturedefs.get(argname)
+            if not fixture_defs:
+                # Will raise FixtureLookupError at setup time if not parametrized somewhere
+                # else (e.g @pytest.mark.parametrize)
+                continue
+
+            # If the test itself parametrizes using this argname, give it
+            # precedence.
+            if any(
+                argname in get_parametrize_mark_argnames(mark)
+                for mark in metafunc.definition.iter_markers("parametrize")
+            ):
+                continue
+
+            # In the common case we only look at the fixture def with the
+            # closest scope (last in the list). But if the fixture overrides
+            # another fixture, while requesting the super fixture, keep going
+            # in case the super fixture is parametrized (#1953).
+            for fixturedef in reversed(fixture_defs):
+                # Fixture is parametrized, apply it and stop.
                 if fixturedef.params is not None:
-                    markers = list(metafunc.definition.iter_markers("parametrize"))
-                    for parametrize_mark in markers:
-                        if "argnames" in parametrize_mark.kwargs:
-                            argnames = parametrize_mark.kwargs["argnames"]
-                        else:
-                            argnames = parametrize_mark.args[0]
-
-                        if not isinstance(argnames, (tuple, list)):
-                            argnames = [
-                                x.strip() for x in argnames.split(",") if x.strip()
-                            ]
-                        if argname in argnames:
-                            break
-                    else:
-                        metafunc.parametrize(
-                            argname,
-                            fixturedef.params,
-                            indirect=True,
-                            scope=fixturedef.scope,
-                            ids=fixturedef.ids,
-                        )
-            else:
-                continue  # will raise FixtureLookupError at setup time
+                    metafunc.parametrize(
+                        argname,
+                        fixturedef.params,
+                        indirect=True,
+                        scope=fixturedef.scope,
+                        ids=fixturedef.ids,
+                    )
+                    break
+
+                # Not requesting the overridden super fixture, stop.
+                if argname not in fixturedef.argnames:
+                    break
+
+                # Try next super fixture, if any.
 
     def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None:
-        # separate parametrized setups
+        # Separate parametrized setups.
         items[:] = reorder_items(items)
 
     def parsefactories(
@@ -1634,16 +1602,17 @@ class FixtureManager:
             obj = safe_getattr(holderobj, name, None)
             marker = getfixturemarker(obj)
             if not isinstance(marker, FixtureFunctionMarker):
-                # magic globals  with __getattr__ might have got us a wrong
-                # fixture attribute
+                # Magic globals  with __getattr__ might have got us a wrong
+                # fixture attribute.
                 continue
 
             if marker.name:
                 name = marker.name
 
-            # during fixture definition we wrap the original fixture function
-            # to issue a warning if called directly, so here we unwrap it in order to not emit the warning
-            # when pytest itself calls the fixture function
+            # During fixture definition we wrap the original fixture function
+            # to issue a warning if called directly, so here we unwrap it in
+            # order to not emit the warning when pytest itself calls the
+            # fixture function.
             obj = get_real_method(obj, holderobj)
 
             fixture_def = FixtureDef(
@@ -1675,13 +1644,12 @@ class FixtureManager:
 
     def getfixturedefs(
         self, argname: str, nodeid: str
-    ) -> Optional[Sequence[FixtureDef]]:
-        """
-        Gets a list of fixtures which are applicable to the given node id.
+    ) -> Optional[Sequence[FixtureDef[Any]]]:
+        """Get a list of fixtures which are applicable to the given node id.
 
-        :param str argname: name of the fixture to search for
-        :param str nodeid: full node id of the requesting test.
-        :return: list[FixtureDef]
+        :param str argname: Name of the fixture to search for.
+        :param str nodeid: Full node id of the requesting test.
+        :rtype: Sequence[FixtureDef]
         """
         try:
             fixturedefs = self._arg2fixturedefs[argname]
@@ -1690,8 +1658,8 @@ class FixtureManager:
         return tuple(self._matchfactories(fixturedefs, nodeid))
 
     def _matchfactories(
-        self, fixturedefs: Iterable[FixtureDef], nodeid: str
-    ) -> Iterator[FixtureDef]:
+        self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
+    ) -> Iterator[FixtureDef[Any]]:
         from _pytest import nodes
 
         for fixturedef in fixturedefs:
index 63c14ecebfa4ffd9442d91c8f2edc3b4a2bc3e7d..8b93ed5f7f86f89c4182a65bf434c100093f5ca3 100644 (file)
@@ -1,7 +1,5 @@
-"""
-Provides a function to report all internal modules for using freezing tools
-pytest
-"""
+"""Provides a function to report all internal modules for using freezing
+tools."""
 import types
 from typing import Iterator
 from typing import List
@@ -9,10 +7,8 @@ from typing import Union
 
 
 def freeze_includes() -> List[str]:
-    """
-    Returns a list of module names used by pytest that should be
-    included by cx_freeze.
-    """
+    """Return a list of module names used by pytest that should be
+    included by cx_freeze."""
     import py
     import _pytest
 
@@ -24,8 +20,7 @@ def freeze_includes() -> List[str]:
 def _iter_all_modules(
     package: Union[str, types.ModuleType], prefix: str = "",
 ) -> Iterator[str]:
-    """
-    Iterates over the names of all modules that can be found in the given
+    """Iterate over the names of all modules that can be found in the given
     package, recursively.
 
         >>> import _pytest
index f3623b8a10322cc4cf4994ce59a213220d94b92d..348a65edec687f1ac8b2cdf04db2bdc4146f2793 100644 (file)
@@ -1,4 +1,4 @@
-""" version info, help messages, tracing configuration.  """
+"""Version info, help messages, tracing configuration."""
 import os
 import sys
 from argparse import Action
@@ -16,8 +16,9 @@ from _pytest.config.argparsing import Parser
 
 
 class HelpAction(Action):
-    """This is an argparse Action that will raise an exception in
-    order to skip the rest of the argument parsing when --help is passed.
+    """An argparse Action that will raise an exception in order to skip the
+    rest of the argument parsing when --help is passed.
+
     This prevents argparse from quitting due to missing required arguments
     when any are defined, for example by ``pytest_addoption``.
     This is similar to the way that the builtin argparse --help option is
@@ -37,7 +38,7 @@ class HelpAction(Action):
     def __call__(self, parser, namespace, values, option_string=None):
         setattr(namespace, self.dest, self.const)
 
-        # We should only skip the rest of the parsing after preparse is done
+        # We should only skip the rest of the parsing after preparse is done.
         if getattr(parser._parser, "after_preparse", False):
             raise PrintHelp
 
index d21c4d4d9efb1ffa8e6c22312e0093afd5946f19..2f0a04a06a73122ee89b81348c88fdf0cdb4514a 100644 (file)
@@ -1,4 +1,5 @@
-""" hook specifications for pytest plugins, invoked from main.py and builtin plugins.  """
+"""Hook specifications for pytest plugins which are invoked by pytest itself
+and by builtin plugins."""
 from typing import Any
 from typing import Dict
 from typing import List
@@ -11,8 +12,8 @@ from typing import Union
 import py.path
 from pluggy import HookspecMarker
 
-from .deprecated import COLLECT_DIRECTORY_HOOK
 from _pytest.compat import TYPE_CHECKING
+from _pytest.deprecated import WARNING_CAPTURED_HOOK
 
 if TYPE_CHECKING:
     import pdb
@@ -51,11 +52,10 @@ hookspec = HookspecMarker("pytest")
 
 @hookspec(historic=True)
 def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
-    """called at plugin registration time to allow adding new hooks via a call to
+    """Called at plugin registration time to allow adding new hooks via a call to
     ``pluginmanager.add_hookspecs(module_or_class, prefix)``.
 
-
-    :param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
+    :param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager.
 
     .. note::
         This hook is incompatible with ``hookwrapper=True``.
@@ -66,10 +66,10 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
 def pytest_plugin_registered(
     plugin: "_PluggyPlugin", manager: "PytestPluginManager"
 ) -> None:
-    """ a new pytest plugin got registered.
+    """A new pytest plugin got registered.
 
-    :param plugin: the plugin module or instance
-    :param _pytest.config.PytestPluginManager manager: pytest plugin manager
+    :param plugin: The plugin module or instance.
+    :param _pytest.config.PytestPluginManager manager: pytest plugin manager.
 
     .. note::
         This hook is incompatible with ``hookwrapper=True``.
@@ -78,7 +78,7 @@ def pytest_plugin_registered(
 
 @hookspec(historic=True)
 def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None:
-    """register argparse-style options and ini-style config values,
+    """Register argparse-style options and ini-style config values,
     called once at the beginning of a test run.
 
     .. note::
@@ -87,15 +87,16 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
         files situated at the tests root directory due to how pytest
         :ref:`discovers plugins during startup <pluginorder>`.
 
-    :arg _pytest.config.argparsing.Parser parser: To add command line options, call
+    :param _pytest.config.argparsing.Parser parser:
+        To add command line options, call
         :py:func:`parser.addoption(...) <_pytest.config.argparsing.Parser.addoption>`.
         To add ini-file values call :py:func:`parser.addini(...)
         <_pytest.config.argparsing.Parser.addini>`.
 
-    :arg _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager,
-        which can be used to install :py:func:`hookspec`'s or :py:func:`hookimpl`'s
-        and allow one plugin to call another plugin's hooks to change how
-        command line options are added.
+    :param _pytest.config.PytestPluginManager pluginmanager:
+        pytest plugin manager, which can be used to install :py:func:`hookspec`'s
+        or :py:func:`hookimpl`'s and allow one plugin to call another plugin's hooks
+        to change how command line options are added.
 
     Options can later be accessed through the
     :py:class:`config <_pytest.config.Config>` object, respectively:
@@ -116,8 +117,7 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
 
 @hookspec(historic=True)
 def pytest_configure(config: "Config") -> None:
-    """
-    Allows plugins and conftest files to perform initial configuration.
+    """Allow plugins and conftest files to perform initial configuration.
 
     This hook is called for every plugin and initial conftest file
     after command line options have been parsed.
@@ -128,7 +128,7 @@ def pytest_configure(config: "Config") -> None:
     .. note::
         This hook is incompatible with ``hookwrapper=True``.
 
-    :arg _pytest.config.Config config: pytest config object
+    :param _pytest.config.Config config: The pytest config object.
     """
 
 
@@ -142,16 +142,17 @@ def pytest_configure(config: "Config") -> None:
 def pytest_cmdline_parse(
     pluginmanager: "PytestPluginManager", args: List[str]
 ) -> Optional["Config"]:
-    """return initialized config object, parsing the specified args.
+    """Return an initialized config object, parsing the specified args.
 
-    Stops at first non-None result, see :ref:`firstresult`
+    Stops at first non-None result, see :ref:`firstresult`.
 
     .. note::
-        This hook will only be called for plugin classes passed to the ``plugins`` arg when using `pytest.main`_ to
-        perform an in-process test run.
+        This hook will only be called for plugin classes passed to the
+        ``plugins`` arg when using `pytest.main`_ to perform an in-process
+        test run.
 
-    :param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
-    :param list[str] args: list of arguments passed on the command line
+    :param _pytest.config.PytestPluginManager pluginmanager: Pytest plugin manager.
+    :param List[str] args: List of arguments passed on the command line.
     """
 
 
@@ -164,37 +165,37 @@ def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None:
     .. note::
         This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
 
-    :param _pytest.config.Config config: pytest config object
-    :param list[str] args: list of arguments passed on the command line
+    :param _pytest.config.Config config: The pytest config object.
+    :param List[str] args: Arguments passed on the command line.
     """
 
 
 @hookspec(firstresult=True)
 def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
-    """ called for performing the main command line action. The default
+    """Called for performing the main command line action. The default
     implementation will invoke the configure hooks and runtest_mainloop.
 
     .. note::
         This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
 
-    Stops at first non-None result, see :ref:`firstresult`
+    Stops at first non-None result, see :ref:`firstresult`.
 
-    :param _pytest.config.Config config: pytest config object
+    :param _pytest.config.Config config: The pytest config object.
     """
 
 
 def pytest_load_initial_conftests(
     early_config: "Config", parser: "Parser", args: List[str]
 ) -> None:
-    """ implements the loading of initial conftest files ahead
+    """Called to implement the loading of initial conftest files ahead
     of command line option parsing.
 
     .. note::
         This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
 
-    :param _pytest.config.Config early_config: pytest config object
-    :param list[str] args: list of arguments passed on the command line
-    :param _pytest.config.argparsing.Parser parser: to add command line options
+    :param _pytest.config.Config early_config: The pytest config object.
+    :param List[str] args: Arguments passed on the command line.
+    :param _pytest.config.argparsing.Parser parser: To add command line options.
     """
 
 
@@ -205,45 +206,57 @@ def pytest_load_initial_conftests(
 
 @hookspec(firstresult=True)
 def pytest_collection(session: "Session") -> Optional[object]:
-    """Perform the collection protocol for the given session.
+    """Perform the collection phase for the given session.
 
     Stops at first non-None result, see :ref:`firstresult`.
     The return value is not used, but only stops further processing.
 
-    The hook is meant to set `session.items` to a sequence of items at least,
-    but normally should follow this procedure:
+    The default collection phase is this (see individual hooks for full details):
+
+    1. Starting from ``session`` as the initial collector:
+
+      1. ``pytest_collectstart(collector)``
+      2. ``report = pytest_make_collect_report(collector)``
+      3. ``pytest_exception_interact(collector, call, report)`` if an interactive exception occurred
+      4. For each collected node:
+
+        1. If an item, ``pytest_itemcollected(item)``
+        2. If a collector, recurse into it.
+
+      5. ``pytest_collectreport(report)``
+
+    2. ``pytest_collection_modifyitems(session, config, items)``
+
+      1. ``pytest_deselected(items)`` for any deselected items (may be called multiple times)
 
-      1. Call the pytest_collectstart hook.
-      2. Call the pytest_collectreport hook.
-      3. Call the pytest_collection_modifyitems hook.
-      4. Call the pytest_collection_finish hook.
-      5. Set session.testscollected to the amount of collect items.
-      6. Set `session.items` to a list of items.
+    3. ``pytest_collection_finish(session)``
+    4. Set ``session.items`` to the list of collected items
+    5. Set ``session.testscollected`` to the number of collected items
 
     You can implement this hook to only perform some action before collection,
     for example the terminal plugin uses it to start displaying the collection
     counter (and returns `None`).
 
-    :param _pytest.main.Session session: the pytest session object
+    :param pytest.Session session: The pytest session object.
     """
 
 
 def pytest_collection_modifyitems(
     session: "Session", config: "Config", items: List["Item"]
 ) -> None:
-    """ called after collection has been performed, may filter or re-order
+    """Called after collection has been performed. May filter or re-order
     the items in-place.
 
-    :param _pytest.main.Session session: the pytest session object
-    :param _pytest.config.Config config: pytest config object
-    :param List[_pytest.nodes.Item] items: list of item objects
+    :param pytest.Session session: The pytest session object.
+    :param _pytest.config.Config config: The pytest config object.
+    :param List[pytest.Item] items: List of item objects.
     """
 
 
 def pytest_collection_finish(session: "Session") -> None:
     """Called after collection has been performed and modified.
 
-    :param _pytest.main.Session session: the pytest session object
+    :param pytest.Session session: The pytest session object.
     """
 
 
@@ -256,27 +269,19 @@ def pytest_ignore_collect(path: py.path.local, config: "Config") -> Optional[boo
 
     Stops at first non-None result, see :ref:`firstresult`.
 
-    :param path: a :py:class:`py.path.local` - the path to analyze
-    :param _pytest.config.Config config: pytest config object
-    """
-
-
-@hookspec(firstresult=True, warn_on_impl=COLLECT_DIRECTORY_HOOK)
-def pytest_collect_directory(path: py.path.local, parent) -> Optional[object]:
-    """Called before traversing a directory for collection files.
-
-    Stops at first non-None result, see :ref:`firstresult`.
-
-    :param path: a :py:class:`py.path.local` - the path to analyze
+    :param py.path.local path: The path to analyze.
+    :param _pytest.config.Config config: The pytest config object.
     """
 
 
-def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]":
-    """Return collection Node or None for the given path.
+def pytest_collect_file(
+    path: py.path.local, parent: "Collector"
+) -> "Optional[Collector]":
+    """Create a Collector for the given path, or None if not relevant.
 
-    Any new node needs to have the specified ``parent`` as a parent.
+    The new node needs to have the specified ``parent`` as a parent.
 
-    :param path: a :py:class:`py.path.local` - the path to collect
+    :param py.path.local path: The path to collect.
     """
 
 
@@ -284,7 +289,7 @@ def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]":
 
 
 def pytest_collectstart(collector: "Collector") -> None:
-    """ collector starts collecting. """
+    """Collector starts collecting."""
 
 
 def pytest_itemcollected(item: "Item") -> None:
@@ -292,18 +297,22 @@ def pytest_itemcollected(item: "Item") -> None:
 
 
 def pytest_collectreport(report: "CollectReport") -> None:
-    """ collector finished collecting. """
+    """Collector finished collecting."""
 
 
 def pytest_deselected(items: Sequence["Item"]) -> None:
-    """Called for deselected test items, e.g. by keyword."""
+    """Called for deselected test items, e.g. by keyword.
+
+    May be called multiple times.
+    """
 
 
 @hookspec(firstresult=True)
 def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectReport]":
-    """ perform ``collector.collect()`` and return a CollectReport.
+    """Perform ``collector.collect()`` and return a CollectReport.
 
-    Stops at first non-None result, see :ref:`firstresult` """
+    Stops at first non-None result, see :ref:`firstresult`.
+    """
 
 
 # -------------------------------------------------------------------------
@@ -321,7 +330,7 @@ def pytest_pycollect_makemodule(path: py.path.local, parent) -> Optional["Module
 
     Stops at first non-None result, see :ref:`firstresult`.
 
-    :param path: a :py:class:`py.path.local` - the path of module to collect
+    :param py.path.local path: The path of module to collect.
     """
 
 
@@ -337,28 +346,31 @@ def pytest_pycollect_makeitem(
 
 @hookspec(firstresult=True)
 def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
-    """ call underlying test function.
+    """Call underlying test function.
 
-    Stops at first non-None result, see :ref:`firstresult` """
+    Stops at first non-None result, see :ref:`firstresult`.
+    """
 
 
 def pytest_generate_tests(metafunc: "Metafunc") -> None:
-    """ generate (multiple) parametrized calls to a test function."""
+    """Generate (multiple) parametrized calls to a test function."""
 
 
 @hookspec(firstresult=True)
 def pytest_make_parametrize_id(
     config: "Config", val: object, argname: str
 ) -> Optional[str]:
-    """Return a user-friendly string representation of the given ``val`` that will be used
-    by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``.
+    """Return a user-friendly string representation of the given ``val``
+    that will be used by @pytest.mark.parametrize calls, or None if the hook
+    doesn't know about ``val``.
+
     The parameter name is available as ``argname``, if required.
 
-    Stops at first non-None result, see :ref:`firstresult`
+    Stops at first non-None result, see :ref:`firstresult`.
 
-    :param _pytest.config.Config config: pytest config object
-    :param val: the parametrized value
-    :param str argname: the automatic parameter name produced by pytest
+    :param _pytest.config.Config config: The pytest config object.
+    :param val: The parametrized value.
+    :param str argname: The automatic parameter name produced by pytest.
     """
 
 
@@ -369,7 +381,7 @@ def pytest_make_parametrize_id(
 
 @hookspec(firstresult=True)
 def pytest_runtestloop(session: "Session") -> Optional[object]:
-    """Performs the main runtest loop (after collection finished).
+    """Perform the main runtest loop (after collection finished).
 
     The default hook implementation performs the runtest protocol for all items
     collected in the session (``session.items``), unless the collection failed
@@ -381,7 +393,7 @@ def pytest_runtestloop(session: "Session") -> Optional[object]:
     If at any point ``session.shouldfail`` or ``session.shouldstop`` are set, the
     loop is terminated after the runtest protocol for the current item is finished.
 
-    :param _pytest.main.Session session: The pytest session object.
+    :param pytest.Session session: The pytest session object.
 
     Stops at first non-None result, see :ref:`firstresult`.
     The return value is not used, but only stops further processing.
@@ -392,7 +404,7 @@ def pytest_runtestloop(session: "Session") -> Optional[object]:
 def pytest_runtest_protocol(
     item: "Item", nextitem: "Optional[Item]"
 ) -> Optional[object]:
-    """Performs the runtest protocol for a single test item.
+    """Perform the runtest protocol for a single test item.
 
     The default runtest protocol is this (see individual hooks for full details):
 
@@ -418,9 +430,8 @@ def pytest_runtest_protocol(
 
     - ``pytest_runtest_logfinish(nodeid, location)``
 
-    :arg item: Test item for which the runtest protocol is performed.
-
-    :arg nextitem: The scheduled-to-be-next test item (or None if this is the end my friend).
+    :param item: Test item for which the runtest protocol is performed.
+    :param nextitem: The scheduled-to-be-next test item (or None if this is the end my friend).
 
     Stops at first non-None result, see :ref:`firstresult`.
     The return value is not used, but only stops further processing.
@@ -476,10 +487,11 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
     includes running the teardown phase of fixtures required by the item (if
     they go out of scope).
 
-    :arg nextitem: The scheduled-to-be-next test item (None if no further
-                   test item is scheduled).  This argument can be used to
-                   perform exact teardowns, i.e. calling just enough finalizers
-                   so that nextitem only needs to call setup-functions.
+    :param nextitem:
+        The scheduled-to-be-next test item (None if no further test item is
+        scheduled). This argument can be used to perform exact teardowns,
+        i.e. calling just enough finalizers so that nextitem only needs to
+        call setup-functions.
     """
 
 
@@ -510,19 +522,15 @@ def pytest_runtest_logreport(report: "TestReport") -> None:
 def pytest_report_to_serializable(
     config: "Config", report: Union["CollectReport", "TestReport"],
 ) -> Optional[Dict[str, Any]]:
-    """
-    Serializes the given report object into a data structure suitable for sending
-    over the wire, e.g. converted to JSON.
-    """
+    """Serialize the given report object into a data structure suitable for
+    sending over the wire, e.g. converted to JSON."""
 
 
 @hookspec(firstresult=True)
 def pytest_report_from_serializable(
     config: "Config", data: Dict[str, Any],
 ) -> Optional[Union["CollectReport", "TestReport"]]:
-    """
-    Restores a report object previously serialized with pytest_report_to_serializable().
-    """
+    """Restore a report object previously serialized with pytest_report_to_serializable()."""
 
 
 # -------------------------------------------------------------------------
@@ -532,11 +540,11 @@ def pytest_report_from_serializable(
 
 @hookspec(firstresult=True)
 def pytest_fixture_setup(
-    fixturedef: "FixtureDef", request: "SubRequest"
+    fixturedef: "FixtureDef[Any]", request: "SubRequest"
 ) -> Optional[object]:
-    """Performs fixture setup execution.
+    """Perform fixture setup execution.
 
-    :return: The return value of the call to the fixture function.
+    :returns: The return value of the call to the fixture function.
 
     Stops at first non-None result, see :ref:`firstresult`.
 
@@ -548,7 +556,7 @@ def pytest_fixture_setup(
 
 
 def pytest_fixture_post_finalizer(
-    fixturedef: "FixtureDef", request: "SubRequest"
+    fixturedef: "FixtureDef[Any]", request: "SubRequest"
 ) -> None:
     """Called after fixture teardown, but before the cache is cleared, so
     the fixture result ``fixturedef.cached_result`` is still available (not
@@ -564,7 +572,7 @@ def pytest_sessionstart(session: "Session") -> None:
     """Called after the ``Session`` object has been created and before performing collection
     and entering the run test loop.
 
-    :param _pytest.main.Session session: the pytest session object
+    :param pytest.Session session: The pytest session object.
     """
 
 
@@ -573,15 +581,15 @@ def pytest_sessionfinish(
 ) -> None:
     """Called after whole test run finished, right before returning the exit status to the system.
 
-    :param _pytest.main.Session session: the pytest session object
-    :param int exitstatus: the status which pytest will return to the system
+    :param pytest.Session session: The pytest session object.
+    :param int exitstatus: The status which pytest will return to the system.
     """
 
 
 def pytest_unconfigure(config: "Config") -> None:
     """Called before test process is exited.
 
-    :param _pytest.config.Config config: pytest config object
+    :param _pytest.config.Config config: The pytest config object.
     """
 
 
@@ -596,22 +604,19 @@ def pytest_assertrepr_compare(
     """Return explanation for comparisons in failing assert expressions.
 
     Return None for no custom explanation, otherwise return a list
-    of strings.  The strings will be joined by newlines but any newlines
-    *in* a string will be escaped.  Note that all but the first line will
+    of strings. The strings will be joined by newlines but any newlines
+    *in* a string will be escaped. Note that all but the first line will
     be indented slightly, the intention is for the first line to be a summary.
 
-    :param _pytest.config.Config config: pytest config object
+    :param _pytest.config.Config config: The pytest config object.
     """
 
 
 def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> None:
-    """
-    **(Experimental)**
+    """**(Experimental)** Called whenever an assertion passes.
 
     .. versionadded:: 5.0
 
-    Hook called whenever an assertion *passes*.
-
     Use this hook to do some processing after a passing assertion.
     The original assertion information is available in the `orig` string
     and the pytest introspected assertion information is available in the
@@ -628,32 +633,32 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
     You need to **clean the .pyc** files in your project directory and interpreter libraries
     when enabling this option, as assertions will require to be re-written.
 
-    :param _pytest.nodes.Item item: pytest item object of current test
-    :param int lineno: line number of the assert statement
-    :param string orig: string with original assertion
-    :param string expl: string with assert explanation
+    :param pytest.Item item: pytest item object of current test.
+    :param int lineno: Line number of the assert statement.
+    :param str orig: String with the original assertion.
+    :param str expl: String with the assert explanation.
 
     .. note::
 
         This hook is **experimental**, so its parameters or even the hook itself might
         be changed/removed without warning in any future pytest release.
 
-        If you find this hook useful, please share your feedback opening an issue.
+        If you find this hook useful, please share your feedback in an issue.
     """
 
 
 # -------------------------------------------------------------------------
-# hooks for influencing reporting (invoked from _pytest_terminal)
+# Hooks for influencing reporting (invoked from _pytest_terminal).
 # -------------------------------------------------------------------------
 
 
 def pytest_report_header(
     config: "Config", startdir: py.path.local
 ) -> Union[str, List[str]]:
-    """ return a string or list of strings to be displayed as header info for terminal reporting.
+    """Return a string or list of strings to be displayed as header info for terminal reporting.
 
-    :param _pytest.config.Config config: pytest config object
-    :param startdir: py.path object with the starting dir
+    :param _pytest.config.Config config: The pytest config object.
+    :param py.path.local startdir: The starting dir.
 
     .. note::
 
@@ -673,16 +678,16 @@ def pytest_report_header(
 def pytest_report_collectionfinish(
     config: "Config", startdir: py.path.local, items: Sequence["Item"],
 ) -> Union[str, List[str]]:
-    """
-    .. versionadded:: 3.2
-
-    Return a string or list of strings to be displayed after collection has finished successfully.
+    """Return a string or list of strings to be displayed after collection
+    has finished successfully.
 
     These strings will be displayed after the standard "collected X items" message.
 
-    :param _pytest.config.Config config: pytest config object
-    :param startdir: py.path object with the starting dir
-    :param items: list of pytest items that are going to be executed; this list should not be modified.
+    .. versionadded:: 3.2
+
+    :param _pytest.config.Config config: The pytest config object.
+    :param py.path.local startdir: The starting dir.
+    :param items: List of pytest items that are going to be executed; this list should not be modified.
 
     .. note::
 
@@ -727,18 +732,16 @@ def pytest_terminal_summary(
 ) -> None:
     """Add a section to terminal summary reporting.
 
-    :param _pytest.terminal.TerminalReporter terminalreporter: the internal terminal reporter object
-    :param int exitstatus: the exit status that will be reported back to the OS
-    :param _pytest.config.Config config: pytest config object
+    :param _pytest.terminal.TerminalReporter terminalreporter: The internal terminal reporter object.
+    :param int exitstatus: The exit status that will be reported back to the OS.
+    :param _pytest.config.Config config: The pytest config object.
 
     .. versionadded:: 4.2
         The ``config`` parameter.
     """
 
 
-# Uncomment this after 6.0 release (#7361)
-# @hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK)
-@hookspec(historic=True)
+@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK)
 def pytest_warning_captured(
     warning_message: "warnings.WarningMessage",
     when: "Literal['config', 'collect', 'runtest']",
@@ -780,8 +783,7 @@ def pytest_warning_recorded(
     nodeid: str,
     location: Optional[Tuple[str, int, str]],
 ) -> None:
-    """
-    Process a warning captured by the internal pytest warnings plugin.
+    """Process a warning captured by the internal pytest warnings plugin.
 
     :param warnings.WarningMessage warning_message:
         The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
@@ -794,7 +796,8 @@ def pytest_warning_recorded(
         * ``"collect"``: during test collection.
         * ``"runtest"``: during test execution.
 
-    :param str nodeid: full id of the item
+    :param str nodeid:
+        Full id of the item.
 
     :param tuple|None location:
         When available, holds information about the execution context of the captured
@@ -823,12 +826,12 @@ def pytest_internalerror(
 def pytest_keyboard_interrupt(
     excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]",
 ) -> None:
-    """ called for keyboard interrupt. """
+    """Called for keyboard interrupt."""
 
 
 def pytest_exception_interact(
     node: Union["Item", "Collector"],
-    call: "CallInfo[object]",
+    call: "CallInfo[Any]",
     report: Union["CollectReport", "TestReport"],
 ) -> None:
     """Called when an exception was raised which can potentially be
@@ -846,20 +849,22 @@ def pytest_exception_interact(
 
 
 def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
-    """ called upon pdb.set_trace(), can be used by plugins to take special
-    action just before the python debugger enters in interactive mode.
+    """Called upon pdb.set_trace().
 
-    :param _pytest.config.Config config: pytest config object
-    :param pdb.Pdb pdb: Pdb instance
+    Can be used by plugins to take special action just before the python
+    debugger enters interactive mode.
+
+    :param _pytest.config.Config config: The pytest config object.
+    :param pdb.Pdb pdb: The Pdb instance.
     """
 
 
 def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
-    """ called when leaving pdb (e.g. with continue after pdb.set_trace()).
+    """Called when leaving pdb (e.g. with continue after pdb.set_trace()).
 
     Can be used by plugins to take special action just after the python
     debugger leaves interactive mode.
 
-    :param _pytest.config.Config config: pytest config object
-    :param pdb.Pdb pdb: Pdb instance
+    :param _pytest.config.Config config: The pytest config object.
+    :param pdb.Pdb pdb: The Pdb instance.
     """
index 8c68d196a2cbbf172324207141de196889fb2610..877b9be78bccb5207d76d0bf539d4fa4f7408419 100644 (file)
@@ -1,18 +1,16 @@
-"""
-    report test results in JUnit-XML format,
-    for use with Jenkins and build integration servers.
-
+"""Report test results in JUnit-XML format, for use with Jenkins and build
+integration servers.
 
 Based on initial code from Ross Lawley.
 
-Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/
-src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
+Output conforms to
+https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
 """
 import functools
 import os
 import platform
 import re
-import sys
+import xml.etree.ElementTree as ET
 from datetime import datetime
 from typing import Callable
 from typing import Dict
@@ -22,14 +20,11 @@ from typing import Optional
 from typing import Tuple
 from typing import Union
 
-import py
-
 import pytest
-from _pytest import deprecated
 from _pytest import nodes
 from _pytest import timing
 from _pytest._code.code import ExceptionRepr
-from _pytest.compat import TYPE_CHECKING
+from _pytest._code.code import ReprFileLocation
 from _pytest.config import Config
 from _pytest.config import filename_arg
 from _pytest.config.argparsing import Parser
@@ -37,41 +32,22 @@ from _pytest.fixtures import FixtureRequest
 from _pytest.reports import TestReport
 from _pytest.store import StoreKey
 from _pytest.terminal import TerminalReporter
-from _pytest.warnings import _issue_warning_captured
-
-if TYPE_CHECKING:
-    from typing import Type
 
 
 xml_key = StoreKey["LogXML"]()
 
 
-class Junit(py.xml.Namespace):
-    pass
-
-
-# We need to get the subset of the invalid unicode ranges according to
-# XML 1.0 which are valid in this python build.  Hence we calculate
-# this dynamically instead of hardcoding it.  The spec range of valid
-# chars is: Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
-#                    | [#x10000-#x10FFFF]
-_legal_chars = (0x09, 0x0A, 0x0D)
-_legal_ranges = ((0x20, 0x7E), (0x80, 0xD7FF), (0xE000, 0xFFFD), (0x10000, 0x10FFFF))
-_legal_xml_re = [
-    "{}-{}".format(chr(low), chr(high))
-    for (low, high) in _legal_ranges
-    if low < sys.maxunicode
-]
-_legal_xml_re = [chr(x) for x in _legal_chars] + _legal_xml_re
-illegal_xml_re = re.compile("[^%s]" % "".join(_legal_xml_re))
-del _legal_chars
-del _legal_ranges
-del _legal_xml_re
-
-_py_ext_re = re.compile(r"\.py$")
+def bin_xml_escape(arg: object) -> str:
+    r"""Visually escape invalid XML characters.
 
+    For example, transforms
+        'hello\aworld\b'
+    into
+        'hello#x07world#x08'
+    Note that the #xABs are *not* XML escapes - missing the ampersand &#xAB.
+    The idea is to escape visually for the user rather than for XML itself.
+    """
 
-def bin_xml_escape(arg: object) -> py.xml.raw:
     def repl(matchobj: Match[str]) -> str:
         i = ord(matchobj.group())
         if i <= 0xFF:
@@ -79,7 +55,13 @@ def bin_xml_escape(arg: object) -> py.xml.raw:
         else:
             return "#x%04X" % i
 
-    return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(str(arg))))
+    # The spec range of valid chars is:
+    # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+    # For an unknown(?) reason, we disallow #x7F (DEL) as well.
+    illegal_xml_re = (
+        "[^\u0009\u000A\u000D\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]"
+    )
+    return re.sub(illegal_xml_re, repl, str(arg))
 
 
 def merge_family(left, right) -> None:
@@ -96,11 +78,11 @@ families = {}
 families["_base"] = {"testcase": ["classname", "name"]}
 families["_base_legacy"] = {"testcase": ["file", "line", "url"]}
 
-# xUnit 1.x inherits legacy attributes
+# xUnit 1.x inherits legacy attributes.
 families["xunit1"] = families["_base"].copy()
 merge_family(families["xunit1"], families["_base_legacy"])
 
-# xUnit 2.x uses strict base attributes
+# xUnit 2.x uses strict base attributes.
 families["xunit2"] = families["_base"]
 
 
@@ -111,12 +93,12 @@ class _NodeReporter:
         self.add_stats = self.xml.add_stats
         self.family = self.xml.family
         self.duration = 0
-        self.properties = []  # type: List[Tuple[str, py.xml.raw]]
-        self.nodes = []  # type: List[py.xml.Tag]
-        self.attrs = {}  # type: Dict[str, Union[str, py.xml.raw]]
+        self.properties = []  # type: List[Tuple[str, str]]
+        self.nodes = []  # type: List[ET.Element]
+        self.attrs = {}  # type: Dict[str, str]
 
-    def append(self, node: py.xml.Tag) -> None:
-        self.xml.add_stats(type(node).__name__)
+    def append(self, node: ET.Element) -> None:
+        self.xml.add_stats(node.tag)
         self.nodes.append(node)
 
     def add_property(self, name: str, value: object) -> None:
@@ -125,17 +107,14 @@ class _NodeReporter:
     def add_attribute(self, name: str, value: object) -> None:
         self.attrs[str(name)] = bin_xml_escape(value)
 
-    def make_properties_node(self) -> Union[py.xml.Tag, str]:
-        """Return a Junit node containing custom properties, if any.
-        """
+    def make_properties_node(self) -> Optional[ET.Element]:
+        """Return a Junit node containing custom properties, if any."""
         if self.properties:
-            return Junit.properties(
-                [
-                    Junit.property(name=name, value=value)
-                    for name, value in self.properties
-                ]
-            )
-        return ""
+            properties = ET.Element("properties")
+            for name, value in self.properties:
+                properties.append(ET.Element("property", name=name, value=value))
+            return properties
+        return None
 
     def record_testreport(self, testreport: TestReport) -> None:
         names = mangle_test_address(testreport.nodeid)
@@ -147,15 +126,15 @@ class _NodeReporter:
             "classname": ".".join(classnames),
             "name": bin_xml_escape(names[-1]),
             "file": testreport.location[0],
-        }  # type: Dict[str, Union[str, py.xml.raw]]
+        }  # type: Dict[str, str]
         if testreport.location[1] is not None:
             attrs["line"] = str(testreport.location[1])
         if hasattr(testreport, "url"):
             attrs["url"] = testreport.url
         self.attrs = attrs
-        self.attrs.update(existing_attrs)  # restore any user-defined attributes
+        self.attrs.update(existing_attrs)  # Restore any user-defined attributes.
 
-        # Preserve legacy testcase behavior
+        # Preserve legacy testcase behavior.
         if self.family == "xunit1":
             return
 
@@ -167,16 +146,17 @@ class _NodeReporter:
                 temp_attrs[key] = self.attrs[key]
         self.attrs = temp_attrs
 
-    def to_xml(self) -> py.xml.Tag:
-        testcase = Junit.testcase(time="%.3f" % self.duration, **self.attrs)
-        testcase.append(self.make_properties_node())
-        for node in self.nodes:
-            testcase.append(node)
+    def to_xml(self) -> ET.Element:
+        testcase = ET.Element("testcase", self.attrs, time="%.3f" % self.duration)
+        properties = self.make_properties_node()
+        if properties is not None:
+            testcase.append(properties)
+        testcase.extend(self.nodes)
         return testcase
 
-    def _add_simple(self, kind: "Type[py.xml.Tag]", message: str, data=None) -> None:
-        data = bin_xml_escape(data)
-        node = kind(data, message=message)
+    def _add_simple(self, tag: str, message: str, data: Optional[str] = None) -> None:
+        node = ET.Element(tag, message=message)
+        node.text = bin_xml_escape(data)
         self.append(node)
 
     def write_captured_output(self, report: TestReport) -> None:
@@ -206,8 +186,9 @@ class _NodeReporter:
         return "\n".join([header.center(80, "-"), content, ""])
 
     def _write_content(self, report: TestReport, content: str, jheader: str) -> None:
-        tag = getattr(Junit, jheader)
-        self.append(tag(bin_xml_escape(content)))
+        tag = ET.Element(jheader)
+        tag.text = bin_xml_escape(content)
+        self.append(tag)
 
     def append_pass(self, report: TestReport) -> None:
         self.add_stats("passed")
@@ -215,32 +196,34 @@ class _NodeReporter:
     def append_failure(self, report: TestReport) -> None:
         # msg = str(report.longrepr.reprtraceback.extraline)
         if hasattr(report, "wasxfail"):
-            self._add_simple(Junit.skipped, "xfail-marked test passes unexpectedly")
+            self._add_simple("skipped", "xfail-marked test passes unexpectedly")
         else:
             assert report.longrepr is not None
-            if getattr(report.longrepr, "reprcrash", None) is not None:
-                message = report.longrepr.reprcrash.message
+            reprcrash = getattr(
+                report.longrepr, "reprcrash", None
+            )  # type: Optional[ReprFileLocation]
+            if reprcrash is not None:
+                message = reprcrash.message
             else:
                 message = str(report.longrepr)
             message = bin_xml_escape(message)
-            fail = Junit.failure(message=message)
-            fail.append(bin_xml_escape(report.longrepr))
-            self.append(fail)
+            self._add_simple("failure", message, str(report.longrepr))
 
     def append_collect_error(self, report: TestReport) -> None:
         # msg = str(report.longrepr.reprtraceback.extraline)
         assert report.longrepr is not None
-        self.append(
-            Junit.error(bin_xml_escape(report.longrepr), message="collection failure")
-        )
+        self._add_simple("error", "collection failure", str(report.longrepr))
 
     def append_collect_skipped(self, report: TestReport) -> None:
-        self._add_simple(Junit.skipped, "collection skipped", report.longrepr)
+        self._add_simple("skipped", "collection skipped", str(report.longrepr))
 
     def append_error(self, report: TestReport) -> None:
         assert report.longrepr is not None
-        if getattr(report.longrepr, "reprcrash", None) is not None:
-            reason = report.longrepr.reprcrash.message
+        reprcrash = getattr(
+            report.longrepr, "reprcrash", None
+        )  # type: Optional[ReprFileLocation]
+        if reprcrash is not None:
+            reason = reprcrash.message
         else:
             reason = str(report.longrepr)
 
@@ -248,46 +231,40 @@ class _NodeReporter:
             msg = 'failed on teardown with "{}"'.format(reason)
         else:
             msg = 'failed on setup with "{}"'.format(reason)
-        self._add_simple(Junit.error, msg, report.longrepr)
+        self._add_simple("error", msg, str(report.longrepr))
 
     def append_skipped(self, report: TestReport) -> None:
         if hasattr(report, "wasxfail"):
             xfailreason = report.wasxfail
             if xfailreason.startswith("reason: "):
                 xfailreason = xfailreason[8:]
-            self.append(
-                Junit.skipped(
-                    "", type="pytest.xfail", message=bin_xml_escape(xfailreason)
-                )
-            )
+            xfailreason = bin_xml_escape(xfailreason)
+            skipped = ET.Element("skipped", type="pytest.xfail", message=xfailreason)
+            self.append(skipped)
         else:
-            assert report.longrepr is not None
+            assert isinstance(report.longrepr, tuple)
             filename, lineno, skipreason = report.longrepr
             if skipreason.startswith("Skipped: "):
                 skipreason = skipreason[9:]
             details = "{}:{}: {}".format(filename, lineno, skipreason)
 
-            self.append(
-                Junit.skipped(
-                    bin_xml_escape(details),
-                    type="pytest.skip",
-                    message=bin_xml_escape(skipreason),
-                )
-            )
+            skipped = ET.Element("skipped", type="pytest.skip", message=skipreason)
+            skipped.text = bin_xml_escape(details)
+            self.append(skipped)
             self.write_captured_output(report)
 
     def finalize(self) -> None:
-        data = self.to_xml().unicode(indent=0)
+        data = self.to_xml()
         self.__dict__.clear()
         # Type ignored becuase mypy doesn't like overriding a method.
         # Also the return value doesn't match...
-        self.to_xml = lambda: py.xml.raw(data)  # type: ignore
+        self.to_xml = lambda: data  # type: ignore[assignment]
 
 
 def _warn_incompatibility_with_xunit2(
     request: FixtureRequest, fixture_name: str
 ) -> None:
-    """Emits a PytestWarning about the given fixture being incompatible with newer xunit revisions"""
+    """Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions."""
     from _pytest.warning_types import PytestWarning
 
     xml = request.config._store.get(xml_key, None)
@@ -355,18 +332,19 @@ def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], Non
 
 def _check_record_param_type(param: str, v: str) -> None:
     """Used by record_testsuite_property to check that the given parameter name is of the proper
-    type"""
+    type."""
     __tracebackhide__ = True
     if not isinstance(v, str):
-        msg = "{param} parameter needs to be a string, but {g} given"
+        msg = "{param} parameter needs to be a string, but {g} given"  # type: ignore[unreachable]
         raise TypeError(msg.format(param=param, g=type(v).__name__))
 
 
 @pytest.fixture(scope="session")
 def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]:
-    """
-    Records a new ``<property>`` tag as child of the root ``<testsuite>``. This is suitable to
-    writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family.
+    """Record a new ``<property>`` tag as child of the root ``<testsuite>``.
+
+    This is suitable to writing global information regarding the entire test
+    suite, and is compatible with ``xunit2`` JUnit family.
 
     This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
 
@@ -377,12 +355,18 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
             record_testsuite_property("STORAGE_TYPE", "CEPH")
 
     ``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
+
+    .. warning::
+
+        Currently this fixture **does not work** with the
+        `pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See issue
+        `#7767 <https://github.com/pytest-dev/pytest/issues/7767>`__ for details.
     """
 
     __tracebackhide__ = True
 
     def record_func(name: str, value: object) -> None:
-        """noop function in case --junitxml was not passed in the command-line"""
+        """No-op function in case --junitxml was not passed in the command-line."""
         __tracebackhide__ = True
         _check_record_param_type("name", name)
 
@@ -433,18 +417,17 @@ def pytest_addoption(parser: Parser) -> None:
         default="total",
     )  # choices=['total', 'call'])
     parser.addini(
-        "junit_family", "Emit XML for schema: one of legacy|xunit1|xunit2", default=None
+        "junit_family",
+        "Emit XML for schema: one of legacy|xunit1|xunit2",
+        default="xunit2",
     )
 
 
 def pytest_configure(config: Config) -> None:
     xmlpath = config.option.xmlpath
-    # prevent opening xmllog on worker nodes (xdist)
+    # Prevent opening xmllog on worker nodes (xdist).
     if xmlpath and not hasattr(config, "workerinput"):
         junit_family = config.getini("junit_family")
-        if not junit_family:
-            _issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2)
-            junit_family = "xunit1"
         config._store[xml_key] = LogXML(
             xmlpath,
             config.option.junitprefix,
@@ -471,10 +454,10 @@ def mangle_test_address(address: str) -> List[str]:
         names.remove("()")
     except ValueError:
         pass
-    # convert file path to dotted path
+    # Convert file path to dotted path.
     names[0] = names[0].replace(nodes.SEP, ".")
-    names[0] = _py_ext_re.sub("", names[0])
-    # put any params back
+    names[0] = re.sub(r"\.py$", "", names[0])
+    # Put any params back.
     names[-1] += possible_open_bracket + params
     return names
 
@@ -505,19 +488,19 @@ class LogXML:
             {}
         )  # type: Dict[Tuple[Union[str, TestReport], object], _NodeReporter]
         self.node_reporters_ordered = []  # type: List[_NodeReporter]
-        self.global_properties = []  # type: List[Tuple[str, py.xml.raw]]
+        self.global_properties = []  # type: List[Tuple[str, str]]
 
         # List of reports that failed on call but teardown is pending.
         self.open_reports = []  # type: List[TestReport]
         self.cnt_double_fail_tests = 0
 
-        # Replaces convenience family with real family
+        # Replaces convenience family with real family.
         if self.family == "legacy":
             self.family = "xunit1"
 
     def finalize(self, report: TestReport) -> None:
         nodeid = getattr(report, "nodeid", report)
-        # local hack to handle xdist report order
+        # Local hack to handle xdist report order.
         workernode = getattr(report, "node", None)
         reporter = self.node_reporters.pop((nodeid, workernode))
         if reporter is not None:
@@ -525,7 +508,7 @@ class LogXML:
 
     def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter:
         nodeid = getattr(report, "nodeid", report)  # type: Union[str, TestReport]
-        # local hack to handle xdist report order
+        # Local hack to handle xdist report order.
         workernode = getattr(report, "node", None)
 
         key = nodeid, workernode
@@ -551,13 +534,13 @@ class LogXML:
         return reporter
 
     def pytest_runtest_logreport(self, report: TestReport) -> None:
-        """handle a setup/call/teardown report, generating the appropriate
-        xml tags as necessary.
+        """Handle a setup/call/teardown report, generating the appropriate
+        XML tags as necessary.
 
-        note: due to plugins like xdist, this hook may be called in interlaced
-        order with reports from other nodes. for example:
+        Note: due to plugins like xdist, this hook may be called in interlaced
+        order with reports from other nodes. For example:
 
-        usual call order:
+        Usual call order:
             -> setup node1
             -> call node1
             -> teardown node1
@@ -565,7 +548,7 @@ class LogXML:
             -> call node2
             -> teardown node2
 
-        possible call order in xdist:
+        Possible call order in xdist:
             -> setup node1
             -> call node1
             -> setup node2
@@ -580,7 +563,7 @@ class LogXML:
                 reporter.append_pass(report)
         elif report.failed:
             if report.when == "teardown":
-                # The following vars are needed when xdist plugin is used
+                # The following vars are needed when xdist plugin is used.
                 report_wid = getattr(report, "worker_id", None)
                 report_ii = getattr(report, "item_index", None)
                 close_report = next(
@@ -598,7 +581,7 @@ class LogXML:
                 if close_report:
                     # We need to open new testcase in case we have failure in
                     # call and error in teardown in order to follow junit
-                    # schema
+                    # schema.
                     self.finalize(close_report)
                     self.cnt_double_fail_tests += 1
             reporter = self._opentestcase(report)
@@ -639,9 +622,8 @@ class LogXML:
                 self.open_reports.remove(close_report)
 
     def update_testcase_duration(self, report: TestReport) -> None:
-        """accumulates total duration for nodeid from given report and updates
-        the Junit.testcase with the new total if already created.
-        """
+        """Accumulate total duration for nodeid from given report and update
+        the Junit.testcase with the new total if already created."""
         if self.report_duration == "total" or report.when == self.report_duration:
             reporter = self.node_reporter(report)
             reporter.duration += getattr(report, "duration", 0.0)
@@ -657,7 +639,7 @@ class LogXML:
     def pytest_internalerror(self, excrepr: ExceptionRepr) -> None:
         reporter = self.node_reporter("internal")
         reporter.attrs.update(classname="pytest", name="internal")
-        reporter._add_simple(Junit.error, "internal error", excrepr)
+        reporter._add_simple("error", "internal error", str(excrepr))
 
     def pytest_sessionstart(self) -> None:
         self.suite_start_time = timing.time()
@@ -679,9 +661,8 @@ class LogXML:
         )
         logfile.write('<?xml version="1.0" encoding="utf-8"?>')
 
-        suite_node = Junit.testsuite(
-            self._get_global_properties_node(),
-            [x.to_xml() for x in self.node_reporters_ordered],
+        suite_node = ET.Element(
+            "testsuite",
             name=self.suite_name,
             errors=str(self.stats["error"]),
             failures=str(self.stats["failure"]),
@@ -691,7 +672,14 @@ class LogXML:
             timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(),
             hostname=platform.node(),
         )
-        logfile.write(Junit.testsuites([suite_node]).unicode(indent=0))
+        global_properties = self._get_global_properties_node()
+        if global_properties is not None:
+            suite_node.append(global_properties)
+        for node_reporter in self.node_reporters_ordered:
+            suite_node.append(node_reporter.to_xml())
+        testsuites = ET.Element("testsuites")
+        testsuites.append(suite_node)
+        logfile.write(ET.tostring(testsuites, encoding="unicode"))
         logfile.close()
 
     def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None:
@@ -702,14 +690,11 @@ class LogXML:
         _check_record_param_type("name", name)
         self.global_properties.append((name, bin_xml_escape(value)))
 
-    def _get_global_properties_node(self) -> Union[py.xml.Tag, str]:
-        """Return a Junit node containing custom properties, if any.
-        """
+    def _get_global_properties_node(self) -> Optional[ET.Element]:
+        """Return a Junit node containing custom properties, if any."""
         if self.global_properties:
-            return Junit.properties(
-                [
-                    Junit.property(name=name, value=value)
-                    for name, value in self.global_properties
-                ]
-            )
-        return ""
+            properties = ET.Element("properties")
+            for name, value in self.global_properties:
+                properties.append(ET.Element("property", name=name, value=value))
+            return properties
+        return None
index 6c422647dbc0ec1b1ee4676be4e4fa009454f312..c277ba5320c77df419e2d238a2651f388e9dd728 100644 (file)
@@ -1,4 +1,4 @@
-""" Access and control log capturing. """
+"""Access and control log capturing."""
 import logging
 import os
 import re
@@ -19,6 +19,7 @@ import pytest
 from _pytest import nodes
 from _pytest._io import TerminalWriter
 from _pytest.capture import CaptureManager
+from _pytest.compat import final
 from _pytest.compat import nullcontext
 from _pytest.config import _strtobool
 from _pytest.config import Config
@@ -43,9 +44,8 @@ def _remove_ansi_escape_sequences(text: str) -> str:
 
 
 class ColoredLevelFormatter(logging.Formatter):
-    """
-    Colorize the %(levelname)..s part of the log format passed to __init__.
-    """
+    """A logging formatter which colorizes the %(levelname)..s part of the
+    log format passed to __init__."""
 
     LOGLEVEL_COLOROPTS = {
         logging.CRITICAL: {"red"},
@@ -110,7 +110,7 @@ class PercentStyleMultiline(logging.PercentStyle):
 
     @staticmethod
     def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int:
-        """Determines the current auto indentation setting
+        """Determine the current auto indentation setting.
 
         Specify auto indent behavior (on/off/fixed) by passing in
         extra={"auto_indent": [value]} to the call to logging.log() or
@@ -128,12 +128,14 @@ class PercentStyleMultiline(logging.PercentStyle):
         Any other values for the option are invalid, and will silently be
         converted to the default.
 
-        :param any auto_indent_option: User specified option for indentation
-            from command line, config or extra kwarg. Accepts int, bool or str.
-            str option accepts the same range of values as boolean config options,
-            as well as positive integers represented in str form.
+        :param None|bool|int|str auto_indent_option:
+            User specified option for indentation from command line, config
+            or extra kwarg. Accepts int, bool or str. str option accepts the
+            same range of values as boolean config options, as well as
+            positive integers represented in str form.
 
-        :returns: indentation value, which can be
+        :returns:
+            Indentation value, which can be
             -1 (automatically determine indentation) or
             0 (auto-indent turned off) or
             >0 (explicitly set indentation position).
@@ -164,7 +166,7 @@ class PercentStyleMultiline(logging.PercentStyle):
     def format(self, record: logging.LogRecord) -> str:
         if "\n" in record.message:
             if hasattr(record, "auto_indent"):
-                # passed in from the "extra={}" kwarg on the call to logging.log()
+                # Passed in from the "extra={}" kwarg on the call to logging.log().
                 auto_indent = self._get_auto_indent(record.auto_indent)  # type: ignore[attr-defined]
             else:
                 auto_indent = self._auto_indent
@@ -178,7 +180,7 @@ class PercentStyleMultiline(logging.PercentStyle):
                         lines[0]
                     )
                 else:
-                    # optimizes logging by allowing a fixed indentation
+                    # Optimizes logging by allowing a fixed indentation.
                     indentation = auto_indent
                 lines[0] = formatted
                 return ("\n" + " " * indentation).join(lines)
@@ -316,7 +318,7 @@ class LogCaptureHandler(logging.StreamHandler):
     stream = None  # type: StringIO
 
     def __init__(self) -> None:
-        """Creates a new log handler."""
+        """Create a new log handler."""
         super().__init__(StringIO())
         self.records = []  # type: List[logging.LogRecord]
 
@@ -338,22 +340,22 @@ class LogCaptureHandler(logging.StreamHandler):
             raise
 
 
+@final
 class LogCaptureFixture:
     """Provides access and control of log capturing."""
 
     def __init__(self, item: nodes.Node) -> None:
-        """Creates a new funcarg."""
         self._item = item
-        # dict of log name -> log level
         self._initial_handler_level = None  # type: Optional[int]
+        # Dict of log name -> log level.
         self._initial_logger_levels = {}  # type: Dict[Optional[str], int]
 
     def _finalize(self) -> None:
-        """Finalizes the fixture.
+        """Finalize the fixture.
 
         This restores the log levels changed by :meth:`set_level`.
         """
-        # restore log levels
+        # Restore log levels.
         if self._initial_handler_level is not None:
             self.handler.setLevel(self._initial_handler_level)
         for logger_name, level in self._initial_logger_levels.items():
@@ -362,20 +364,20 @@ class LogCaptureFixture:
 
     @property
     def handler(self) -> LogCaptureHandler:
-        """
+        """Get the logging handler used by the fixture.
+
         :rtype: LogCaptureHandler
         """
         return self._item._store[caplog_handler_key]
 
     def get_records(self, when: str) -> List[logging.LogRecord]:
-        """
-        Get the logging records for one of the possible test phases.
+        """Get the logging records for one of the possible test phases.
 
         :param str when:
             Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".
 
+        :returns: The list of captured records at the given stage.
         :rtype: List[logging.LogRecord]
-        :return: the list of captured records at the given stage
 
         .. versionadded:: 3.4
         """
@@ -383,17 +385,17 @@ class LogCaptureFixture:
 
     @property
     def text(self) -> str:
-        """Returns the formatted log text."""
+        """The formatted log text."""
         return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
 
     @property
     def records(self) -> List[logging.LogRecord]:
-        """Returns the list of log records."""
+        """The list of log records."""
         return self.handler.records
 
     @property
     def record_tuples(self) -> List[Tuple[str, int, str]]:
-        """Returns a list of a stripped down version of log records intended
+        """A list of a stripped down version of log records intended
         for use in assertion comparison.
 
         The format of the tuple is:
@@ -404,15 +406,18 @@ class LogCaptureFixture:
 
     @property
     def messages(self) -> List[str]:
-        """Returns a list of format-interpolated log messages.
+        """A list of format-interpolated log messages.
+
+        Unlike 'records', which contains the format string and parameters for
+        interpolation, log messages in this list are all interpolated.
 
-        Unlike 'records', which contains the format string and parameters for interpolation, log messages in this list
-        are all interpolated.
-        Unlike 'text', which contains the output from the handler, log messages in this list are unadorned with
-        levels, timestamps, etc, making exact comparisons more reliable.
+        Unlike 'text', which contains the output from the handler, log
+        messages in this list are unadorned with levels, timestamps, etc,
+        making exact comparisons more reliable.
 
-        Note that traceback or stack info (from :func:`logging.exception` or the `exc_info` or `stack_info` arguments
-        to the logging functions) is not included, as this is added by the formatter in the handler.
+        Note that traceback or stack info (from :func:`logging.exception` or
+        the `exc_info` or `stack_info` arguments to the logging functions) is
+        not included, as this is added by the formatter in the handler.
 
         .. versionadded:: 3.7
         """
@@ -423,18 +428,17 @@ class LogCaptureFixture:
         self.handler.reset()
 
     def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
-        """Sets the level for capturing of logs. The level will be restored to its previous value at the end of
-        the test.
-
-        :param int level: the logger to level.
-        :param str logger: the logger to update the level. If not given, the root logger level is updated.
+        """Set the level of a logger for the duration of a test.
 
         .. versionchanged:: 3.4
-            The levels of the loggers changed by this function will be restored to their initial values at the
-            end of the test.
+            The levels of the loggers changed by this function will be
+            restored to their initial values at the end of the test.
+
+        :param int level: The level.
+        :param str logger: The logger to update. If not given, the root logger.
         """
         logger_obj = logging.getLogger(logger)
-        # save the original log-level to restore it during teardown
+        # Save the original log-level to restore it during teardown.
         self._initial_logger_levels.setdefault(logger, logger_obj.level)
         logger_obj.setLevel(level)
         if self._initial_handler_level is None:
@@ -445,11 +449,12 @@ class LogCaptureFixture:
     def at_level(
         self, level: int, logger: Optional[str] = None
     ) -> Generator[None, None, None]:
-        """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the
-        level is restored to its original value.
+        """Context manager that sets the level for capturing of logs. After
+        the end of the 'with' statement the level is restored to its original
+        value.
 
-        :param int level: the logger to level.
-        :param str logger: the logger to update the level. If not given, the root logger level is updated.
+        :param int level: The level.
+        :param str logger: The logger to update. If not given, the root logger.
         """
         logger_obj = logging.getLogger(logger)
         orig_level = logger_obj.level
@@ -510,11 +515,10 @@ def pytest_configure(config: Config) -> None:
 
 
 class LoggingPlugin:
-    """Attaches to the logging module and captures log messages for each test.
-    """
+    """Attaches to the logging module and captures log messages for each test."""
 
     def __init__(self, config: Config) -> None:
-        """Creates a new plugin to capture log messages.
+        """Create a new plugin to capture log messages.
 
         The formatter can be safely shared across all handlers so
         create a single one for the entire test session here.
@@ -573,7 +577,7 @@ class LoggingPlugin:
         self.log_cli_handler.setFormatter(log_cli_formatter)
 
     def _create_formatter(self, log_format, log_date_format, auto_indent):
-        # color option doesn't exist if terminal plugin is disabled
+        # Color option doesn't exist if terminal plugin is disabled.
         color = getattr(self._config.option, "color", "no")
         if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
             log_format
@@ -591,17 +595,17 @@ class LoggingPlugin:
         return formatter
 
     def set_log_path(self, fname: str) -> None:
-        """Public method, which can set filename parameter for
-        Logging.FileHandler(). Also creates parent directory if
-        it does not exist.
+        """Set the filename parameter for Logging.FileHandler().
+
+        Creates parent directory if it does not exist.
 
         .. warning::
-            Please considered as an experimental API.
+            This is an experimental API.
         """
         fpath = Path(fname)
 
         if not fpath.is_absolute():
-            fpath = Path(str(self._config.rootdir), fpath)
+            fpath = self._config.rootpath / fpath
 
         if not fpath.parent.exists():
             fpath.parent.mkdir(exist_ok=True, parents=True)
@@ -653,19 +657,17 @@ class LoggingPlugin:
 
     @pytest.hookimpl(hookwrapper=True)
     def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]:
-        """Runs all collected test items."""
-
         if session.config.option.collectonly:
             yield
             return
 
         if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
-            # setting verbose flag is needed to avoid messy test progress output
+            # The verbose flag is needed to avoid messy test progress output.
             self._config.option.verbose = 1
 
         with catching_logs(self.log_cli_handler, level=self.log_cli_level):
             with catching_logs(self.log_file_handler, level=self.log_file_level):
-                yield  # run all the tests
+                yield  # Run all the tests.
 
     @pytest.hookimpl
     def pytest_runtest_logstart(self) -> None:
@@ -677,7 +679,7 @@ class LoggingPlugin:
         self.log_cli_handler.set_when("logreport")
 
     def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]:
-        """Implements the internals of pytest_runtest_xxx() hook."""
+        """Implement the internals of the pytest_runtest_xxx() hooks."""
         with catching_logs(
             self.caplog_handler, level=self.log_level,
         ) as caplog_handler, catching_logs(
@@ -735,9 +737,7 @@ class LoggingPlugin:
 
 
 class _FileHandler(logging.FileHandler):
-    """
-    Custom FileHandler with pytest tweaks.
-    """
+    """A logging FileHandler with pytest tweaks."""
 
     def handleError(self, record: logging.LogRecord) -> None:
         # Handled by LogCaptureHandler.
@@ -745,12 +745,12 @@ class _FileHandler(logging.FileHandler):
 
 
 class _LiveLoggingStreamHandler(logging.StreamHandler):
-    """
-    Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
-    in each test.
+    """A logging StreamHandler used by the live logging feature: it will
+    write a newline before the first log message in each test.
 
-    During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured
-    and won't appear in the terminal.
+    During live logging we must also explicitly disable stdout/stderr
+    capturing otherwise it will get captured and won't appear in the
+    terminal.
     """
 
     # Officially stream needs to be a IO[str], but TerminalReporter
@@ -762,10 +762,6 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
         terminal_reporter: TerminalReporter,
         capture_manager: Optional[CaptureManager],
     ) -> None:
-        """
-        :param _pytest.terminal.TerminalReporter terminal_reporter:
-        :param _pytest.capture.CaptureManager capture_manager:
-        """
         logging.StreamHandler.__init__(self, stream=terminal_reporter)  # type: ignore[arg-type]
         self.capture_manager = capture_manager
         self.reset()
@@ -773,11 +769,11 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
         self._test_outcome_written = False
 
     def reset(self) -> None:
-        """Reset the handler; should be called before the start of each test"""
+        """Reset the handler; should be called before the start of each test."""
         self._first_record_emitted = False
 
     def set_when(self, when: Optional[str]) -> None:
-        """Prepares for the given test phase (setup/call/teardown)"""
+        """Prepare for the given test phase (setup/call/teardown)."""
         self._when = when
         self._section_name_shown = False
         if when == "start":
@@ -808,7 +804,7 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
 
 
 class _LiveLoggingNullHandler(logging.NullHandler):
-    """A handler used when live logging is disabled."""
+    """A logging handler used when live logging is disabled."""
 
     def reset(self) -> None:
         pass
index 7e68165066d1660c7313d3b8f03dcf62f8e22d92..ef106c46a433987387bdd610d1fc21cbc11809ca 100644 (file)
@@ -1,4 +1,4 @@
-""" core implementation of testing process: init, session, runtest loop. """
+"""Core implementation of the testing process: init, session, runtest loop."""
 import argparse
 import fnmatch
 import functools
@@ -21,17 +21,22 @@ import py
 
 import _pytest._code
 from _pytest import nodes
+from _pytest.compat import final
 from _pytest.compat import overload
 from _pytest.compat import TYPE_CHECKING
 from _pytest.config import Config
 from _pytest.config import directory_arg
 from _pytest.config import ExitCode
 from _pytest.config import hookimpl
+from _pytest.config import PytestPluginManager
 from _pytest.config import UsageError
 from _pytest.config.argparsing import Parser
 from _pytest.fixtures import FixtureManager
 from _pytest.outcomes import exit
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
 from _pytest.pathlib import Path
+from _pytest.pathlib import visit
 from _pytest.reports import CollectReport
 from _pytest.reports import TestReport
 from _pytest.runner import collect_one_node
@@ -42,8 +47,6 @@ if TYPE_CHECKING:
     from typing import Type
     from typing_extensions import Literal
 
-    from _pytest.python import Package
-
 
 def pytest_addoption(parser: Parser) -> None:
     parser.addini(
@@ -68,6 +71,20 @@ def pytest_addoption(parser: Parser) -> None:
         const=1,
         help="exit instantly on first error or failed test.",
     )
+    group = parser.getgroup("pytest-warnings")
+    group.addoption(
+        "-W",
+        "--pythonwarnings",
+        action="append",
+        help="set which warnings to report, see -W option of python itself.",
+    )
+    parser.addini(
+        "filterwarnings",
+        type="linelist",
+        help="Each line specifies a pattern for "
+        "warnings.filterwarnings. "
+        "Processed after -W/--pythonwarnings.",
+    )
     group._addoption(
         "--maxfail",
         metavar="num",
@@ -205,7 +222,7 @@ def validate_basetemp(path: str) -> str:
         raise argparse.ArgumentTypeError(msg)
 
     def is_ancestor(base: Path, query: Path) -> bool:
-        """ return True if query is an ancestor of base, else False."""
+        """Return whether query is an ancestor of base."""
         if base == query:
             return True
         for parent in base.parents:
@@ -227,7 +244,7 @@ def validate_basetemp(path: str) -> str:
 def wrap_session(
     config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]
 ) -> Union[int, ExitCode]:
-    """Skeleton command line program"""
+    """Skeleton command line program."""
     session = Session.from_config(config)
     session.exitstatus = ExitCode.OK
     initstate = 0
@@ -290,8 +307,8 @@ def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]:
 
 
 def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]:
-    """ default command line protocol for initialization, session,
-    running tests and reporting. """
+    """Default command line protocol for initialization, session,
+    running tests and reporting."""
     config.hook.pytest_collection(session=session)
     config.hook.pytest_runtestloop(session=session)
 
@@ -327,8 +344,8 @@ def pytest_runtestloop(session: "Session") -> bool:
 
 
 def _in_venv(path: py.path.local) -> bool:
-    """Attempts to detect if ``path`` is the root of a Virtual Environment by
-    checking for the existence of the appropriate activate script"""
+    """Attempt to detect if ``path`` is the root of a Virtual Environment by
+    checking for the existence of the appropriate activate script."""
     bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin")
     if not bindir.isdir():
         return False
@@ -388,30 +405,38 @@ def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> No
         items[:] = remaining
 
 
-class NoMatch(Exception):
-    """ raised if matching cannot locate a matching names. """
+class FSHookProxy:
+    def __init__(self, pm: PytestPluginManager, remove_mods) -> None:
+        self.pm = pm
+        self.remove_mods = remove_mods
+
+    def __getattr__(self, name: str):
+        x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
+        self.__dict__[name] = x
+        return x
 
 
 class Interrupted(KeyboardInterrupt):
-    """ signals an interrupted test run. """
+    """Signals that the test run was interrupted."""
 
-    __module__ = "builtins"  # for py3
+    __module__ = "builtins"  # For py3.
 
 
 class Failed(Exception):
-    """ signals a stop as failed test run. """
+    """Signals a stop as failed test run."""
 
 
 @attr.s
-class _bestrelpath_cache(dict):
-    path = attr.ib(type=py.path.local)
+class _bestrelpath_cache(Dict[Path, str]):
+    path = attr.ib(type=Path)
 
-    def __missing__(self, path: py.path.local) -> str:
-        r = self.path.bestrelpath(path)  # type: str
+    def __missing__(self, path: Path) -> str:
+        r = bestrelpath(self.path, path)
         self[path] = r
         return r
 
 
+@final
 class Session(nodes.FSCollector):
     Interrupted = Interrupted
     Failed = Failed
@@ -422,8 +447,8 @@ class Session(nodes.FSCollector):
     exitstatus = None  # type: Union[int, ExitCode]
 
     def __init__(self, config: Config) -> None:
-        nodes.FSCollector.__init__(
-            self, config.rootdir, parent=None, config=config, session=self, nodeid=""
+        super().__init__(
+            config.rootdir, parent=None, config=config, session=self, nodeid=""
         )
         self.testsfailed = 0
         self.testscollected = 0
@@ -433,23 +458,9 @@ class Session(nodes.FSCollector):
         self.startdir = config.invocation_dir
         self._initialpaths = frozenset()  # type: FrozenSet[py.path.local]
 
-        # Keep track of any collected nodes in here, so we don't duplicate fixtures
-        self._collection_node_cache1 = (
-            {}
-        )  # type: Dict[py.path.local, Sequence[nodes.Collector]]
-        self._collection_node_cache2 = (
-            {}
-        )  # type: Dict[Tuple[Type[nodes.Collector], py.path.local], nodes.Collector]
-        self._collection_node_cache3 = (
-            {}
-        )  # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport]
-
-        # Dirnames of pkgs with dunder-init files.
-        self._collection_pkg_roots = {}  # type: Dict[str, Package]
-
         self._bestrelpathcache = _bestrelpath_cache(
-            config.rootdir
-        )  # type: Dict[py.path.local, str]
+            config.rootpath
+        )  # type: Dict[Path, str]
 
         self.config.pluginmanager.register(self, name="session")
 
@@ -467,8 +478,8 @@ class Session(nodes.FSCollector):
             self.testscollected,
         )
 
-    def _node_location_to_relpath(self, node_path: py.path.local) -> str:
-        # bestrelpath is a quite slow function
+    def _node_location_to_relpath(self, node_path: Path) -> str:
+        # bestrelpath is a quite slow function.
         return self._bestrelpathcache[node_path]
 
     @hookimpl(tryfirst=True)
@@ -494,267 +505,288 @@ class Session(nodes.FSCollector):
         return path in self._initialpaths
 
     def gethookproxy(self, fspath: py.path.local):
-        return super()._gethookproxy(fspath)
+        # Check if we have the common case of running
+        # hooks with all conftest.py files.
+        pm = self.config.pluginmanager
+        my_conftestmodules = pm._getconftestmodules(
+            fspath, self.config.getoption("importmode")
+        )
+        remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
+        if remove_mods:
+            # One or more conftests are not in use at this fspath.
+            proxy = FSHookProxy(pm, remove_mods)
+        else:
+            # All plugins are active for this fspath.
+            proxy = self.config.hook
+        return proxy
+
+    def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
+        if direntry.name == "__pycache__":
+            return False
+        path = py.path.local(direntry.path)
+        ihook = self.gethookproxy(path.dirpath())
+        if ihook.pytest_ignore_collect(path=path, config=self.config):
+            return False
+        norecursepatterns = self.config.getini("norecursedirs")
+        if any(path.check(fnmatch=pat) for pat in norecursepatterns):
+            return False
+        return True
+
+    def _collectfile(
+        self, path: py.path.local, handle_dupes: bool = True
+    ) -> Sequence[nodes.Collector]:
+        assert (
+            path.isfile()
+        ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
+            path, path.isdir(), path.exists(), path.islink()
+        )
+        ihook = self.gethookproxy(path)
+        if not self.isinitpath(path):
+            if ihook.pytest_ignore_collect(path=path, config=self.config):
+                return ()
+
+        if handle_dupes:
+            keepduplicates = self.config.getoption("keepduplicates")
+            if not keepduplicates:
+                duplicate_paths = self.config.pluginmanager._duplicatepaths
+                if path in duplicate_paths:
+                    return ()
+                else:
+                    duplicate_paths.add(path)
+
+        return ihook.pytest_collect_file(path=path, parent=self)  # type: ignore[no-any-return]
 
     @overload
     def perform_collect(
         self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...
     ) -> Sequence[nodes.Item]:
-        raise NotImplementedError()
+        ...
 
     @overload  # noqa: F811
     def perform_collect(  # noqa: F811
         self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
     ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
-        raise NotImplementedError()
+        ...
 
     def perform_collect(  # noqa: F811
         self, args: Optional[Sequence[str]] = None, genitems: bool = True
     ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
+        """Perform the collection phase for this session.
+
+        This is called by the default
+        :func:`pytest_collection <_pytest.hookspec.pytest_collection>` hook
+        implementation; see the documentation of this hook for more details.
+        For testing purposes, it may also be called directly on a fresh
+        ``Session``.
+
+        This function normally recursively expands any collectors collected
+        from the session to their items, and only items are returned. For
+        testing purposes, this may be suppressed by passing ``genitems=False``,
+        in which case the return value contains these collectors unexpanded,
+        and ``session.items`` is empty.
+        """
+        if args is None:
+            args = self.config.args
+
+        self.trace("perform_collect", self, args)
+        self.trace.root.indent += 1
+
+        self._notfound = []  # type: List[Tuple[str, Sequence[nodes.Collector]]]
+        self._initial_parts = []  # type: List[Tuple[py.path.local, List[str]]]
+        self.items = []  # type: List[nodes.Item]
+
         hook = self.config.hook
+
+        items = self.items  # type: Sequence[Union[nodes.Item, nodes.Collector]]
         try:
-            items = self._perform_collect(args, genitems)
+            initialpaths = []  # type: List[py.path.local]
+            for arg in args:
+                fspath, parts = resolve_collection_argument(
+                    self.config.invocation_params.dir,
+                    arg,
+                    as_pypath=self.config.option.pyargs,
+                )
+                self._initial_parts.append((fspath, parts))
+                initialpaths.append(fspath)
+            self._initialpaths = frozenset(initialpaths)
+            rep = collect_one_node(self)
+            self.ihook.pytest_collectreport(report=rep)
+            self.trace.root.indent -= 1
+            if self._notfound:
+                errors = []
+                for arg, cols in self._notfound:
+                    line = "(no name {!r} in any of {!r})".format(arg, cols)
+                    errors.append("not found: {}\n{}".format(arg, line))
+                raise UsageError(*errors)
+            if not genitems:
+                items = rep.result
+            else:
+                if rep.passed:
+                    for node in rep.result:
+                        self.items.extend(self.genitems(node))
+
             self.config.pluginmanager.check_pending()
             hook.pytest_collection_modifyitems(
                 session=self, config=self.config, items=items
             )
         finally:
             hook.pytest_collection_finish(session=self)
+
         self.testscollected = len(items)
         return items
 
-    @overload
-    def _perform_collect(
-        self, args: Optional[Sequence[str]], genitems: "Literal[True]"
-    ) -> List[nodes.Item]:
-        raise NotImplementedError()
+    def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
+        from _pytest.python import Package
 
-    @overload  # noqa: F811
-    def _perform_collect(  # noqa: F811
-        self, args: Optional[Sequence[str]], genitems: bool
-    ) -> Union[List[Union[nodes.Item]], List[Union[nodes.Item, nodes.Collector]]]:
-        raise NotImplementedError()
-
-    def _perform_collect(  # noqa: F811
-        self, args: Optional[Sequence[str]], genitems: bool
-    ) -> Union[List[Union[nodes.Item]], List[Union[nodes.Item, nodes.Collector]]]:
-        if args is None:
-            args = self.config.args
-        self.trace("perform_collect", self, args)
-        self.trace.root.indent += 1
-        self._notfound = []  # type: List[Tuple[str, NoMatch]]
-        initialpaths = []  # type: List[py.path.local]
-        self._initial_parts = []  # type: List[Tuple[py.path.local, List[str]]]
-        self.items = items = []  # type: List[nodes.Item]
-        for arg in args:
-            fspath, parts = self._parsearg(arg)
-            self._initial_parts.append((fspath, parts))
-            initialpaths.append(fspath)
-        self._initialpaths = frozenset(initialpaths)
-        rep = collect_one_node(self)
-        self.ihook.pytest_collectreport(report=rep)
-        self.trace.root.indent -= 1
-        if self._notfound:
-            errors = []
-            for arg, exc in self._notfound:
-                line = "(no name {!r} in any of {!r})".format(arg, exc.args[0])
-                errors.append("not found: {}\n{}".format(arg, line))
-            raise UsageError(*errors)
-        if not genitems:
-            return rep.result
-        else:
-            if rep.passed:
-                for node in rep.result:
-                    self.items.extend(self.genitems(node))
-            return items
+        # Keep track of any collected nodes in here, so we don't duplicate fixtures.
+        node_cache1 = {}  # type: Dict[py.path.local, Sequence[nodes.Collector]]
+        node_cache2 = (
+            {}
+        )  # type: Dict[Tuple[Type[nodes.Collector], py.path.local], nodes.Collector]
 
-    def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
-        for fspath, parts in self._initial_parts:
-            self.trace("processing argument", (fspath, parts))
-            self.trace.root.indent += 1
-            try:
-                yield from self._collect(fspath, parts)
-            except NoMatch as exc:
-                report_arg = "::".join((str(fspath), *parts))
-                # we are inside a make_report hook so
-                # we cannot directly pass through the exception
-                self._notfound.append((report_arg, exc))
+        # Keep track of any collected collectors in matchnodes paths, so they
+        # are not collected more than once.
+        matchnodes_cache = (
+            {}
+        )  # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport]
 
-            self.trace.root.indent -= 1
-        self._collection_node_cache1.clear()
-        self._collection_node_cache2.clear()
-        self._collection_node_cache3.clear()
-        self._collection_pkg_roots.clear()
-
-    def _collect(
-        self, argpath: py.path.local, names: List[str]
-    ) -> Iterator[Union[nodes.Item, nodes.Collector]]:
-        from _pytest.python import Package
+        # Dirnames of pkgs with dunder-init files.
+        pkg_roots = {}  # type: Dict[str, Package]
+
+        for argpath, names in self._initial_parts:
+            self.trace("processing argument", (argpath, names))
+            self.trace.root.indent += 1
 
-        # Start with a Session root, and delve to argpath item (dir or file)
-        # and stack all Packages found on the way.
-        # No point in finding packages when collecting doctests
-        if not self.config.getoption("doctestmodules", False):
-            pm = self.config.pluginmanager
-            for parent in reversed(argpath.parts()):
-                if pm._confcutdir and pm._confcutdir.relto(parent):
-                    break
-
-                if parent.isdir():
-                    pkginit = parent.join("__init__.py")
-                    if pkginit.isfile():
-                        if pkginit not in self._collection_node_cache1:
+            # Start with a Session root, and delve to argpath item (dir or file)
+            # and stack all Packages found on the way.
+            # No point in finding packages when collecting doctests.
+            if not self.config.getoption("doctestmodules", False):
+                pm = self.config.pluginmanager
+                for parent in reversed(argpath.parts()):
+                    if pm._confcutdir and pm._confcutdir.relto(parent):
+                        break
+
+                    if parent.isdir():
+                        pkginit = parent.join("__init__.py")
+                        if pkginit.isfile() and pkginit not in node_cache1:
                             col = self._collectfile(pkginit, handle_dupes=False)
                             if col:
                                 if isinstance(col[0], Package):
-                                    self._collection_pkg_roots[str(parent)] = col[0]
-                                # always store a list in the cache, matchnodes expects it
-                                self._collection_node_cache1[col[0].fspath] = [col[0]]
-
-        # If it's a directory argument, recurse and look for any Subpackages.
-        # Let the Package collector deal with subnodes, don't collect here.
-        if argpath.check(dir=1):
-            assert not names, "invalid arg {!r}".format((argpath, names))
-
-            seen_dirs = set()  # type: Set[py.path.local]
-            for path in argpath.visit(
-                fil=self._visit_filter, rec=self._recurse, bf=True, sort=True
-            ):
-                dirpath = path.dirpath()
-                if dirpath not in seen_dirs:
-                    # Collect packages first.
-                    seen_dirs.add(dirpath)
-                    pkginit = dirpath.join("__init__.py")
-                    if pkginit.exists():
-                        for x in self._collectfile(pkginit):
+                                    pkg_roots[str(parent)] = col[0]
+                                node_cache1[col[0].fspath] = [col[0]]
+
+            # If it's a directory argument, recurse and look for any Subpackages.
+            # Let the Package collector deal with subnodes, don't collect here.
+            if argpath.check(dir=1):
+                assert not names, "invalid arg {!r}".format((argpath, names))
+
+                seen_dirs = set()  # type: Set[py.path.local]
+                for direntry in visit(str(argpath), self._recurse):
+                    if not direntry.is_file():
+                        continue
+
+                    path = py.path.local(direntry.path)
+                    dirpath = path.dirpath()
+
+                    if dirpath not in seen_dirs:
+                        # Collect packages first.
+                        seen_dirs.add(dirpath)
+                        pkginit = dirpath.join("__init__.py")
+                        if pkginit.exists():
+                            for x in self._collectfile(pkginit):
+                                yield x
+                                if isinstance(x, Package):
+                                    pkg_roots[str(dirpath)] = x
+                    if str(dirpath) in pkg_roots:
+                        # Do not collect packages here.
+                        continue
+
+                    for x in self._collectfile(path):
+                        key = (type(x), x.fspath)
+                        if key in node_cache2:
+                            yield node_cache2[key]
+                        else:
+                            node_cache2[key] = x
                             yield x
-                            if isinstance(x, Package):
-                                self._collection_pkg_roots[str(dirpath)] = x
-                if str(dirpath) in self._collection_pkg_roots:
-                    # Do not collect packages here.
+            else:
+                assert argpath.check(file=1)
+
+                if argpath in node_cache1:
+                    col = node_cache1[argpath]
+                else:
+                    collect_root = pkg_roots.get(argpath.dirname, self)
+                    col = collect_root._collectfile(argpath, handle_dupes=False)
+                    if col:
+                        node_cache1[argpath] = col
+
+                matching = []
+                work = [
+                    (col, names)
+                ]  # type: List[Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]]]
+                while work:
+                    self.trace("matchnodes", col, names)
+                    self.trace.root.indent += 1
+
+                    matchnodes, matchnames = work.pop()
+                    for node in matchnodes:
+                        if not matchnames:
+                            matching.append(node)
+                            continue
+                        if not isinstance(node, nodes.Collector):
+                            continue
+                        key = (type(node), node.nodeid)
+                        if key in matchnodes_cache:
+                            rep = matchnodes_cache[key]
+                        else:
+                            rep = collect_one_node(node)
+                            matchnodes_cache[key] = rep
+                        if rep.passed:
+                            submatchnodes = []
+                            for r in rep.result:
+                                # TODO: Remove parametrized workaround once collection structure contains
+                                # parametrization.
+                                if (
+                                    r.name == matchnames[0]
+                                    or r.name.split("[")[0] == matchnames[0]
+                                ):
+                                    submatchnodes.append(r)
+                            if submatchnodes:
+                                work.append((submatchnodes, matchnames[1:]))
+                            # XXX Accept IDs that don't have "()" for class instances.
+                            elif len(rep.result) == 1 and rep.result[0].name == "()":
+                                work.append((rep.result, matchnames))
+                        else:
+                            # Report collection failures here to avoid failing to run some test
+                            # specified in the command line because the module could not be
+                            # imported (#134).
+                            node.ihook.pytest_collectreport(report=rep)
+
+                    self.trace("matchnodes finished -> ", len(matching), "nodes")
+                    self.trace.root.indent -= 1
+
+                if not matching:
+                    report_arg = "::".join((str(argpath), *names))
+                    self._notfound.append((report_arg, col))
                     continue
 
-                for x in self._collectfile(path):
-                    key = (type(x), x.fspath)
-                    if key in self._collection_node_cache2:
-                        yield self._collection_node_cache2[key]
-                    else:
-                        self._collection_node_cache2[key] = x
-                        yield x
-        else:
-            assert argpath.check(file=1)
+                # If __init__.py was the only file requested, then the matched node will be
+                # the corresponding Package, and the first yielded item will be the __init__
+                # Module itself, so just use that. If this special case isn't taken, then all
+                # the files in the package will be yielded.
+                if argpath.basename == "__init__.py":
+                    assert isinstance(matching[0], nodes.Collector)
+                    try:
+                        yield next(iter(matching[0].collect()))
+                    except StopIteration:
+                        # The package collects nothing with only an __init__.py
+                        # file in it, which gets ignored by the default
+                        # "python_files" option.
+                        pass
+                    continue
 
-            if argpath in self._collection_node_cache1:
-                col = self._collection_node_cache1[argpath]
-            else:
-                collect_root = self._collection_pkg_roots.get(argpath.dirname, self)
-                col = collect_root._collectfile(argpath, handle_dupes=False)
-                if col:
-                    self._collection_node_cache1[argpath] = col
-            m = self.matchnodes(col, names)
-            # If __init__.py was the only file requested, then the matched node will be
-            # the corresponding Package, and the first yielded item will be the __init__
-            # Module itself, so just use that. If this special case isn't taken, then all
-            # the files in the package will be yielded.
-            if argpath.basename == "__init__.py":
-                assert isinstance(m[0], nodes.Collector)
-                try:
-                    yield next(iter(m[0].collect()))
-                except StopIteration:
-                    # The package collects nothing with only an __init__.py
-                    # file in it, which gets ignored by the default
-                    # "python_files" option.
-                    pass
-                return
-            yield from m
-
-    @staticmethod
-    def _visit_filter(f: py.path.local) -> bool:
-        # TODO: Remove type: ignore once `py` is typed.
-        return f.check(file=1)  # type: ignore
-
-    def _tryconvertpyarg(self, x: str) -> str:
-        """Convert a dotted module name to path."""
-        try:
-            spec = importlib.util.find_spec(x)
-        # AttributeError: looks like package module, but actually filename
-        # ImportError: module does not exist
-        # ValueError: not a module name
-        except (AttributeError, ImportError, ValueError):
-            return x
-        if spec is None or spec.origin is None or spec.origin == "namespace":
-            return x
-        elif spec.submodule_search_locations:
-            return os.path.dirname(spec.origin)
-        else:
-            return spec.origin
-
-    def _parsearg(self, arg: str) -> Tuple[py.path.local, List[str]]:
-        """ return (fspath, names) tuple after checking the file exists. """
-        strpath, *parts = str(arg).split("::")
-        if self.config.option.pyargs:
-            strpath = self._tryconvertpyarg(strpath)
-        relpath = strpath.replace("/", os.sep)
-        fspath = self.config.invocation_dir.join(relpath, abs=True)
-        if not fspath.check():
-            if self.config.option.pyargs:
-                raise UsageError(
-                    "file or package not found: " + arg + " (missing __init__.py?)"
-                )
-            raise UsageError("file not found: " + arg)
-        return (fspath, parts)
+                yield from matching
 
-    def matchnodes(
-        self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str],
-    ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
-        self.trace("matchnodes", matching, names)
-        self.trace.root.indent += 1
-        nodes = self._matchnodes(matching, names)
-        num = len(nodes)
-        self.trace("matchnodes finished -> ", num, "nodes")
-        self.trace.root.indent -= 1
-        if num == 0:
-            raise NoMatch(matching, names[:1])
-        return nodes
-
-    def _matchnodes(
-        self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str],
-    ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
-        if not matching or not names:
-            return matching
-        name = names[0]
-        assert name
-        nextnames = names[1:]
-        resultnodes = []  # type: List[Union[nodes.Item, nodes.Collector]]
-        for node in matching:
-            if isinstance(node, nodes.Item):
-                if not names:
-                    resultnodes.append(node)
-                continue
-            assert isinstance(node, nodes.Collector)
-            key = (type(node), node.nodeid)
-            if key in self._collection_node_cache3:
-                rep = self._collection_node_cache3[key]
-            else:
-                rep = collect_one_node(node)
-                self._collection_node_cache3[key] = rep
-            if rep.passed:
-                has_matched = False
-                for x in rep.result:
-                    # TODO: remove parametrized workaround once collection structure contains parametrization
-                    if x.name == name or x.name.split("[")[0] == name:
-                        resultnodes.extend(self.matchnodes([x], nextnames))
-                        has_matched = True
-                # XXX accept IDs that don't have "()" for class instances
-                if not has_matched and len(rep.result) == 1 and x.name == "()":
-                    nextnames.insert(0, name)
-                    resultnodes.extend(self.matchnodes([x], nextnames))
-            else:
-                # report collection failures here to avoid failing to run some test
-                # specified in the command line because the module could not be
-                # imported (#134)
-                node.ihook.pytest_collectreport(report=rep)
-        return resultnodes
+            self.trace.root.indent -= 1
 
     def genitems(
         self, node: Union[nodes.Item, nodes.Collector]
@@ -770,3 +802,67 @@ class Session(nodes.FSCollector):
                 for subnode in rep.result:
                     yield from self.genitems(subnode)
             node.ihook.pytest_collectreport(report=rep)
+
+
+def search_pypath(module_name: str) -> str:
+    """Search sys.path for the given a dotted module name, and return its file system path."""
+    try:
+        spec = importlib.util.find_spec(module_name)
+    # AttributeError: looks like package module, but actually filename
+    # ImportError: module does not exist
+    # ValueError: not a module name
+    except (AttributeError, ImportError, ValueError):
+        return module_name
+    if spec is None or spec.origin is None or spec.origin == "namespace":
+        return module_name
+    elif spec.submodule_search_locations:
+        return os.path.dirname(spec.origin)
+    else:
+        return spec.origin
+
+
+def resolve_collection_argument(
+    invocation_path: Path, arg: str, *, as_pypath: bool = False
+) -> Tuple[py.path.local, List[str]]:
+    """Parse path arguments optionally containing selection parts and return (fspath, names).
+
+    Command-line arguments can point to files and/or directories, and optionally contain
+    parts for specific tests selection, for example:
+
+        "pkg/tests/test_foo.py::TestClass::test_foo"
+
+    This function ensures the path exists, and returns a tuple:
+
+        (py.path.path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
+
+    When as_pypath is True, expects that the command-line argument actually contains
+    module paths instead of file-system paths:
+
+        "pkg.tests.test_foo::TestClass::test_foo"
+
+    In which case we search sys.path for a matching module, and then return the *path* to the
+    found module.
+
+    If the path doesn't exist, raise UsageError.
+    If the path is a directory and selection parts are present, raise UsageError.
+    """
+    strpath, *parts = str(arg).split("::")
+    if as_pypath:
+        strpath = search_pypath(strpath)
+    fspath = invocation_path / strpath
+    fspath = absolutepath(fspath)
+    if not fspath.exists():
+        msg = (
+            "module or package not found: {arg} (missing __init__.py?)"
+            if as_pypath
+            else "file or directory not found: {arg}"
+        )
+        raise UsageError(msg.format(arg=arg))
+    if parts and fspath.is_dir():
+        msg = (
+            "package argument cannot contain :: selection parts: {arg}"
+            if as_pypath
+            else "directory argument cannot contain :: selection parts: {arg}"
+        )
+        raise UsageError(msg.format(arg=arg))
+    return py.path.local(str(fspath)), parts
index 5d71a772526efc00311dbb5690977342756d255f..6a9b262307ef6c2e476c6fbca482fb4f76ef28f1 100644 (file)
@@ -1,5 +1,6 @@
-""" generic mechanism for marking and selecting python functions. """
+"""Generic mechanism for marking and selecting python functions."""
 import typing
+import warnings
 from typing import AbstractSet
 from typing import List
 from typing import Optional
@@ -22,13 +23,22 @@ from _pytest.config import ExitCode
 from _pytest.config import hookimpl
 from _pytest.config import UsageError
 from _pytest.config.argparsing import Parser
+from _pytest.deprecated import MINUS_K_COLON
+from _pytest.deprecated import MINUS_K_DASH
 from _pytest.store import StoreKey
 
 if TYPE_CHECKING:
     from _pytest.nodes import Item
 
 
-__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"]
+__all__ = [
+    "MARK_GEN",
+    "Mark",
+    "MarkDecorator",
+    "MarkGenerator",
+    "ParameterSet",
+    "get_empty_parameterset_mark",
+]
 
 
 old_mark_config_key = StoreKey[Optional[Config]]()
@@ -51,9 +61,9 @@ def param(
         def test_eval(test_input, expected):
             assert eval(test_input) == expected
 
-    :param values: variable args of the values of the parameter set, in order.
-    :keyword marks: a single mark or a list of marks to be applied to this parameter set.
-    :keyword str id: the id to attribute to this parameter set.
+    :param values: Variable args of the values of the parameter set, in order.
+    :keyword marks: A single mark or a list of marks to be applied to this parameter set.
+    :keyword str id: The id to attribute to this parameter set.
     """
     return ParameterSet.param(*values, marks=marks, id=id)
 
@@ -141,22 +151,22 @@ class KeywordMatcher:
     def from_item(cls, item: "Item") -> "KeywordMatcher":
         mapped_names = set()
 
-        # Add the names of the current item and any parent items
+        # Add the names of the current item and any parent items.
         import pytest
 
         for node in item.listchain():
             if not isinstance(node, (pytest.Instance, pytest.Session)):
                 mapped_names.add(node.name)
 
-        # Add the names added as extra keywords to current or parent items
+        # Add the names added as extra keywords to current or parent items.
         mapped_names.update(item.listextrakeywords())
 
-        # Add the names attached to the current function through direct assignment
+        # Add the names attached to the current function through direct assignment.
         function_obj = getattr(item, "function", None)
         if function_obj:
             mapped_names.update(function_obj.__dict__)
 
-        # add the markers to the keywords as we no longer handle them correctly
+        # Add the markers to the keywords as we no longer handle them correctly.
         mapped_names.update(mark.name for mark in item.iter_markers())
 
         return cls(mapped_names)
@@ -178,14 +188,12 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
 
     if keywordexpr.startswith("-"):
         # To be removed in pytest 7.0.0.
-        # Uncomment this after 6.0 release (#7361)
-        # warnings.warn(MINUS_K_DASH, stacklevel=2)
+        warnings.warn(MINUS_K_DASH, stacklevel=2)
         keywordexpr = "not " + keywordexpr[1:]
     selectuntil = False
     if keywordexpr[-1:] == ":":
         # To be removed in pytest 7.0.0.
-        # Uncomment this after 6.0 release (#7361)
-        # warnings.warn(MINUS_K_COLON, stacklevel=2)
+        warnings.warn(MINUS_K_COLON, stacklevel=2)
         selectuntil = True
         keywordexpr = keywordexpr[:-1]
 
index 73b7bf169924e6bc8c1f523951e554332a984ebf..f570010975719b362c629d18565c58d0538166d0 100644 (file)
@@ -1,5 +1,4 @@
-r"""
-Evaluate match expressions, as used by `-k` and `-m`.
+r"""Evaluate match expressions, as used by `-k` and `-m`.
 
 The grammar is:
 
@@ -213,10 +212,11 @@ class Expression:
     def evaluate(self, matcher: Callable[[str], bool]) -> bool:
         """Evaluate the match expression.
 
-        :param matcher: Given an identifier, should return whether it matches or not.
-                        Should be prepared to handle arbitrary strings as input.
+        :param matcher:
+            Given an identifier, should return whether it matches or not.
+            Should be prepared to handle arbitrary strings as input.
 
-        Returns whether the expression matches or not.
+        :returns: Whether the expression matches or not.
         """
         ret = eval(
             self.code, {"__builtins__": {}}, MatcherAdapter(matcher)
index 6567822999ac6d6c64196bc86609158233d1cd81..39a2321b3ff9e79ae94fafd3541b12ac08ee5e6a 100644 (file)
@@ -5,6 +5,7 @@ import warnings
 from typing import Any
 from typing import Callable
 from typing import Iterable
+from typing import Iterator
 from typing import List
 from typing import Mapping
 from typing import NamedTuple
@@ -19,6 +20,7 @@ import attr
 
 from .._code import getfslineno
 from ..compat import ascii_escaped
+from ..compat import final
 from ..compat import NOTSET
 from ..compat import NotSetType
 from ..compat import overload
@@ -30,6 +32,8 @@ from _pytest.warning_types import PytestUnknownMarkWarning
 if TYPE_CHECKING:
     from typing import Type
 
+    from ..nodes import Node
+
 
 EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
 
@@ -107,14 +111,15 @@ class ParameterSet(
         parameterset: Union["ParameterSet", Sequence[object], object],
         force_tuple: bool = False,
     ) -> "ParameterSet":
-        """
+        """Extract from an object or objects.
+
         :param parameterset:
-            a legacy style parameterset that may or may not be a tuple,
-            and may or may not be wrapped into a mess of mark objects
+            A legacy style parameterset that may or may not be a tuple,
+            and may or may not be wrapped into a mess of mark objects.
 
         :param force_tuple:
-            enforce tuple wrapping so single argument tuple values
-            don't get decomposed and break tests
+            Enforce tuple wrapping so single argument tuple values
+            don't get decomposed and break tests.
         """
 
         if isinstance(parameterset, cls):
@@ -166,7 +171,7 @@ class ParameterSet(
         del argvalues
 
         if parameters:
-            # check all parameter sets have the correct number of values
+            # Check all parameter sets have the correct number of values.
             for param in parameters:
                 if len(param.values) != len(argnames):
                     msg = (
@@ -186,8 +191,8 @@ class ParameterSet(
                         pytrace=False,
                     )
         else:
-            # empty parameter set (likely computed at runtime): create a single
-            # parameter set with NOTSET values, with the "empty parameter set" mark applied to it
+            # Empty parameter set (likely computed at runtime): create a single
+            # parameter set with NOTSET values, with the "empty parameter set" mark applied to it.
             mark = get_empty_parameterset_mark(config, argnames, func)
             parameters.append(
                 ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
@@ -195,6 +200,7 @@ class ParameterSet(
         return argnames, parameters
 
 
+@final
 @attr.s(frozen=True)
 class Mark:
     #: Name of the mark.
@@ -220,8 +226,7 @@ class Mark:
 
         Combines by appending args and merging kwargs.
 
-        :param other: The mark to combine with.
-        :type other: Mark
+        :param Mark other: The mark to combine with.
         :rtype: Mark
         """
         assert self.name == other.name
@@ -314,7 +319,7 @@ class MarkDecorator:
         Unlike calling the MarkDecorator, with_args() can be used even
         if the sole argument is a callable/class.
 
-        :return: MarkDecorator
+        :rtype: MarkDecorator
         """
         mark = Mark(self.name, args, kwargs)
         return self.__class__(self.mark.combined_with(mark))
@@ -324,13 +329,13 @@ class MarkDecorator:
     # the first match so it works out even if we break the rules.
     @overload
     def __call__(self, arg: _Markable) -> _Markable:  # type: ignore[misc]
-        raise NotImplementedError()
+        pass
 
     @overload  # noqa: F811
     def __call__(  # noqa: F811
         self, *args: object, **kwargs: object
     ) -> "MarkDecorator":
-        raise NotImplementedError()
+        pass
 
     def __call__(self, *args: object, **kwargs: object):  # noqa: F811
         """Call the MarkDecorator."""
@@ -344,9 +349,7 @@ class MarkDecorator:
 
 
 def get_unpacked_marks(obj) -> List[Mark]:
-    """
-    obtain the unpacked marks that are stored on an object
-    """
+    """Obtain the unpacked marks that are stored on an object."""
     mark_list = getattr(obj, "pytestmark", [])
     if not isinstance(mark_list, list):
         mark_list = [mark_list]
@@ -354,10 +357,9 @@ def get_unpacked_marks(obj) -> List[Mark]:
 
 
 def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]:
-    """
-    normalizes marker decorating helpers to mark objects
+    """Normalize marker decorating helpers to mark objects.
 
-    :type mark_list: List[Union[Mark, Markdecorator]]
+    :type List[Union[Mark, Markdecorator]] mark_list:
     :rtype: List[Mark]
     """
     extracted = [
@@ -388,11 +390,11 @@ if TYPE_CHECKING:
     class _SkipMarkDecorator(MarkDecorator):
         @overload  # type: ignore[override,misc]
         def __call__(self, arg: _Markable) -> _Markable:
-            raise NotImplementedError()
+            ...
 
         @overload  # noqa: F811
         def __call__(self, reason: str = ...) -> "MarkDecorator":  # noqa: F811
-            raise NotImplementedError()
+            ...
 
     class _SkipifMarkDecorator(MarkDecorator):
         def __call__(  # type: ignore[override]
@@ -401,12 +403,12 @@ if TYPE_CHECKING:
             *conditions: Union[str, bool],
             reason: str = ...
         ) -> MarkDecorator:
-            raise NotImplementedError()
+            ...
 
     class _XfailMarkDecorator(MarkDecorator):
         @overload  # type: ignore[override,misc]
         def __call__(self, arg: _Markable) -> _Markable:
-            raise NotImplementedError()
+            ...
 
         @overload  # noqa: F811
         def __call__(  # noqa: F811
@@ -420,7 +422,7 @@ if TYPE_CHECKING:
             ] = ...,
             strict: bool = ...
         ) -> MarkDecorator:
-            raise NotImplementedError()
+            ...
 
     class _ParametrizeMarkDecorator(MarkDecorator):
         def __call__(  # type: ignore[override]
@@ -437,21 +439,22 @@ if TYPE_CHECKING:
             ] = ...,
             scope: Optional[_Scope] = ...
         ) -> MarkDecorator:
-            raise NotImplementedError()
+            ...
 
     class _UsefixturesMarkDecorator(MarkDecorator):
         def __call__(  # type: ignore[override]
             self, *fixtures: str
         ) -> MarkDecorator:
-            raise NotImplementedError()
+            ...
 
     class _FilterwarningsMarkDecorator(MarkDecorator):
         def __call__(  # type: ignore[override]
             self, *filters: str
         ) -> MarkDecorator:
-            raise NotImplementedError()
+            ...
 
 
+@final
 class MarkGenerator:
     """Factory for :class:`MarkDecorator` objects - exposed as
     a ``pytest.mark`` singleton instance.
@@ -524,13 +527,15 @@ class MarkGenerator:
 MARK_GEN = MarkGenerator()
 
 
-class NodeKeywords(collections.abc.MutableMapping):
-    def __init__(self, node):
+# TODO(py36): inherit from typing.MutableMapping[str, Any].
+@final
+class NodeKeywords(collections.abc.MutableMapping):  # type: ignore[type-arg]
+    def __init__(self, node: "Node") -> None:
         self.node = node
         self.parent = node.parent
         self._markers = {node.name: True}
 
-    def __getitem__(self, key):
+    def __getitem__(self, key: str) -> Any:
         try:
             return self._markers[key]
         except KeyError:
@@ -538,17 +543,17 @@ class NodeKeywords(collections.abc.MutableMapping):
                 raise
             return self.parent.keywords[key]
 
-    def __setitem__(self, key, value):
+    def __setitem__(self, key: str, value: Any) -> None:
         self._markers[key] = value
 
-    def __delitem__(self, key):
+    def __delitem__(self, key: str) -> None:
         raise ValueError("cannot delete key in keywords dict")
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[str]:
         seen = self._seen()
         return iter(seen)
 
-    def _seen(self):
+    def _seen(self) -> Set[str]:
         seen = set(self._markers)
         if self.parent is not None:
             seen.update(self.parent.keywords)
index 2e5cca5262861cfbea19bb511a42643ddb109f4f..bbd96779da5a85863bdabaf4d4119478f6fd76f9 100644 (file)
@@ -1,4 +1,4 @@
-""" monkeypatching and mocking functionality.  """
+"""Monkeypatching and mocking functionality."""
 import os
 import re
 import sys
@@ -14,6 +14,7 @@ from typing import TypeVar
 from typing import Union
 
 import pytest
+from _pytest.compat import final
 from _pytest.compat import overload
 from _pytest.fixtures import fixture
 from _pytest.pathlib import Path
@@ -27,8 +28,10 @@ V = TypeVar("V")
 
 @fixture
 def monkeypatch() -> Generator["MonkeyPatch", None, None]:
-    """The returned ``monkeypatch`` fixture provides these
-    helper methods to modify objects, dictionaries or os.environ::
+    """A convenient fixture for monkey-patching.
+
+    The fixture provides these methods to modify objects, dictionaries or
+    os.environ::
 
         monkeypatch.setattr(obj, name, value, raising=True)
         monkeypatch.delattr(obj, name, raising=True)
@@ -39,10 +42,9 @@ def monkeypatch() -> Generator["MonkeyPatch", None, None]:
         monkeypatch.syspath_prepend(path)
         monkeypatch.chdir(path)
 
-    All modifications will be undone after the requesting
-    test function or fixture has finished. The ``raising``
-    parameter determines if a KeyError or AttributeError
-    will be raised if the set/deletion operation has no target.
+    All modifications will be undone after the requesting test function or
+    fixture has finished. The ``raising`` parameter determines if a KeyError
+    or AttributeError will be raised if the set/deletion operation has no target.
     """
     mpatch = MonkeyPatch()
     yield mpatch
@@ -50,7 +52,7 @@ def monkeypatch() -> Generator["MonkeyPatch", None, None]:
 
 
 def resolve(name: str) -> object:
-    # simplified from zope.dottedname
+    # Simplified from zope.dottedname.
     parts = name.split(".")
 
     used = parts.pop(0)
@@ -63,12 +65,11 @@ def resolve(name: str) -> object:
             pass
         else:
             continue
-        # we use explicit un-nesting of the handling block in order
-        # to avoid nested exceptions on python 3
+        # We use explicit un-nesting of the handling block in order
+        # to avoid nested exceptions.
         try:
             __import__(used)
         except ImportError as ex:
-            # str is used for py2 vs py3
             expected = str(ex).split()[-1]
             if expected == used:
                 raise
@@ -91,7 +92,7 @@ def annotated_getattr(obj: object, name: str, ann: str) -> object:
 
 
 def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]:
-    if not isinstance(import_path, str) or "." not in import_path:
+    if not isinstance(import_path, str) or "." not in import_path:  # type: ignore[unreachable]
         raise TypeError(
             "must be absolute import path string, not {!r}".format(import_path)
         )
@@ -110,9 +111,10 @@ class Notset:
 notset = Notset()
 
 
+@final
 class MonkeyPatch:
-    """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
-    """
+    """Object returned by the ``monkeypatch`` fixture keeping a record of
+    setattr/item/env/syspath changes."""
 
     def __init__(self) -> None:
         self._setattr = []  # type: List[Tuple[object, str, object]]
@@ -124,9 +126,10 @@ class MonkeyPatch:
 
     @contextmanager
     def context(self) -> Generator["MonkeyPatch", None, None]:
-        """
-        Context manager that returns a new :class:`MonkeyPatch` object which
-        undoes any patching done inside the ``with`` block upon exit:
+        """Context manager that returns a new :class:`MonkeyPatch` object
+        which undoes any patching done inside the ``with`` block upon exit.
+
+        Example:
 
         .. code-block:: python
 
@@ -151,13 +154,13 @@ class MonkeyPatch:
     def setattr(
         self, target: str, name: object, value: Notset = ..., raising: bool = ...,
     ) -> None:
-        raise NotImplementedError()
+        ...
 
     @overload  # noqa: F811
     def setattr(  # noqa: F811
         self, target: object, name: str, value: object, raising: bool = ...,
     ) -> None:
-        raise NotImplementedError()
+        ...
 
     def setattr(  # noqa: F811
         self,
@@ -166,18 +169,16 @@ class MonkeyPatch:
         value: object = notset,
         raising: bool = True,
     ) -> None:
-        """ Set attribute value on target, memorizing the old value.
-        By default raise AttributeError if the attribute did not exist.
+        """Set attribute value on target, memorizing the old value.
 
         For convenience you can specify a string as ``target`` which
         will be interpreted as a dotted import path, with the last part
-        being the attribute name.  Example:
+        being the attribute name. For example,
         ``monkeypatch.setattr("os.getcwd", lambda: "/")``
         would set the ``getcwd`` function of the ``os`` module.
 
-        The ``raising`` value determines if the setattr should fail
-        if the attribute is not already present (defaults to True
-        which means it will raise).
+        Raises AttributeError if the attribute does not exist, unless
+        ``raising`` is set to False.
         """
         __tracebackhide__ = True
         import inspect
@@ -215,15 +216,14 @@ class MonkeyPatch:
         name: Union[str, Notset] = notset,
         raising: bool = True,
     ) -> None:
-        """ Delete attribute ``name`` from ``target``, by default raise
-        AttributeError it the attribute did not previously exist.
+        """Delete attribute ``name`` from ``target``.
 
         If no ``name`` is specified and ``target`` is a string
         it will be interpreted as a dotted import path with the
         last part being the attribute name.
 
-        If ``raising`` is set to False, no exception will be raised if the
-        attribute is missing.
+        Raises AttributeError it the attribute does not exist, unless
+        ``raising`` is set to False.
         """
         __tracebackhide__ = True
         import inspect
@@ -249,15 +249,15 @@ class MonkeyPatch:
             delattr(target, name)
 
     def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
-        """ Set dictionary entry ``name`` to value. """
+        """Set dictionary entry ``name`` to value."""
         self._setitem.append((dic, name, dic.get(name, notset)))
         dic[name] = value
 
     def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
-        """ Delete ``name`` from dict. Raise KeyError if it doesn't exist.
+        """Delete ``name`` from dict.
 
-        If ``raising`` is set to False, no exception will be raised if the
-        key is missing.
+        Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
+        False.
         """
         if name not in dic:
             if raising:
@@ -267,11 +267,14 @@ class MonkeyPatch:
             del dic[name]
 
     def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
-        """ Set environment variable ``name`` to ``value``.  If ``prepend``
-        is a character, read the current environment variable value
-        and prepend the ``value`` adjoined with the ``prepend`` character."""
+        """Set environment variable ``name`` to ``value``.
+
+        If ``prepend`` is a character, read the current environment variable
+        value and prepend the ``value`` adjoined with the ``prepend``
+        character.
+        """
         if not isinstance(value, str):
-            warnings.warn(
+            warnings.warn(  # type: ignore[unreachable]
                 pytest.PytestWarning(
                     "Value of environment variable {name} type should be str, but got "
                     "{value!r} (type: {type}); converted to str implicitly".format(
@@ -286,17 +289,16 @@ class MonkeyPatch:
         self.setitem(os.environ, name, value)
 
     def delenv(self, name: str, raising: bool = True) -> None:
-        """ Delete ``name`` from the environment. Raise KeyError if it does
-        not exist.
+        """Delete ``name`` from the environment.
 
-        If ``raising`` is set to False, no exception will be raised if the
-        environment variable is missing.
+        Raises ``KeyError`` if it does not exist, unless ``raising`` is set to
+        False.
         """
         environ = os.environ  # type: MutableMapping[str, str]
         self.delitem(environ, name, raising=raising)
 
     def syspath_prepend(self, path) -> None:
-        """ Prepend ``path`` to ``sys.path`` list of import locations. """
+        """Prepend ``path`` to ``sys.path`` list of import locations."""
         from pkg_resources import fixup_namespace_packages
 
         if self._savesyspath is None:
@@ -318,7 +320,8 @@ class MonkeyPatch:
         invalidate_caches()
 
     def chdir(self, path) -> None:
-        """ Change the current working directory to the specified path.
+        """Change the current working directory to the specified path.
+
         Path can be a string or a py.path.local object.
         """
         if self._cwd is None:
@@ -326,15 +329,16 @@ class MonkeyPatch:
         if hasattr(path, "chdir"):
             path.chdir()
         elif isinstance(path, Path):
-            # modern python uses the fspath protocol here LEGACY
+            # Modern python uses the fspath protocol here LEGACY
             os.chdir(str(path))
         else:
             os.chdir(path)
 
     def undo(self) -> None:
-        """ Undo previous changes.  This call consumes the
-        undo stack. Calling it a second time has no effect unless
-        you do more monkeypatching after the undo call.
+        """Undo previous changes.
+
+        This call consumes the undo stack. Calling it a second time has no
+        effect unless you do more monkeypatching after the undo call.
 
         There is generally no need to call `undo()`, since it is
         called automatically during tear-down.
@@ -356,7 +360,7 @@ class MonkeyPatch:
                 try:
                     del dictionary[key]
                 except KeyError:
-                    pass  # was already deleted, so we have the desired state
+                    pass  # Was already deleted, so we have the desired state.
             else:
                 dictionary[key] = value
         self._setitem[:] = []
index 91cf7e5ac3541a5f3f1b5fda0aaf65f7c78dcddb..3665d8d5ef4c24dbeccfbabfd393ebcb83a672a9 100644 (file)
@@ -1,13 +1,13 @@
 import os
 import warnings
 from functools import lru_cache
+from typing import Any
 from typing import Callable
 from typing import Dict
 from typing import Iterable
 from typing import Iterator
 from typing import List
 from typing import Optional
-from typing import Sequence
 from typing import Set
 from typing import Tuple
 from typing import TypeVar
@@ -24,14 +24,14 @@ from _pytest.compat import overload
 from _pytest.compat import TYPE_CHECKING
 from _pytest.config import Config
 from _pytest.config import ConftestImportFailure
-from _pytest.config import PytestPluginManager
-from _pytest.deprecated import NODE_USE_FROM_PARENT
+from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
 from _pytest.fixtures import FixtureDef
 from _pytest.fixtures import FixtureLookupError
 from _pytest.mark.structures import Mark
 from _pytest.mark.structures import MarkDecorator
 from _pytest.mark.structures import NodeKeywords
 from _pytest.outcomes import fail
+from _pytest.pathlib import absolutepath
 from _pytest.pathlib import Path
 from _pytest.store import Store
 
@@ -66,19 +66,23 @@ def _splitnode(nodeid: str) -> Tuple[str, ...]:
         ['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo']
     """
     if nodeid == "":
-        # If there is no root node at all, return an empty list so the caller's logic can remain sane
+        # If there is no root node at all, return an empty list so the caller's
+        # logic can remain sane.
         return ()
     parts = nodeid.split(SEP)
-    # Replace single last element 'test_foo.py::Bar' with multiple elements 'test_foo.py', 'Bar'
+    # Replace single last element 'test_foo.py::Bar' with multiple elements
+    # 'test_foo.py', 'Bar'.
     parts[-1:] = parts[-1].split("::")
-    # Convert parts into a tuple to avoid possible errors with caching of a mutable type
+    # Convert parts into a tuple to avoid possible errors with caching of a
+    # mutable type.
     return tuple(parts)
 
 
 def ischildnode(baseid: str, nodeid: str) -> bool:
     """Return True if the nodeid is a child node of the baseid.
 
-    E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp'
+    E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz',
+    but not of 'foo/blorp'.
     """
     base_parts = _splitnode(baseid)
     node_parts = _splitnode(nodeid)
@@ -92,16 +96,24 @@ _NodeType = TypeVar("_NodeType", bound="Node")
 
 class NodeMeta(type):
     def __call__(self, *k, **kw):
-        warnings.warn(NODE_USE_FROM_PARENT.format(name=self.__name__), stacklevel=2)
-        return super().__call__(*k, **kw)
+        msg = (
+            "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
+            "See "
+            "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
+            " for more details."
+        ).format(name=self.__name__)
+        fail(msg, pytrace=False)
 
     def _create(self, *k, **kw):
         return super().__call__(*k, **kw)
 
 
 class Node(metaclass=NodeMeta):
-    """ base class for Collector and Item the test collection tree.
-    Collector subclasses have children, Items are terminal nodes."""
+    """Base class for Collector and Item, the components of the test
+    collection tree.
+
+    Collector subclasses have children; Items are leaf nodes.
+    """
 
     # Use __slots__ to make attribute access faster.
     # Note that __dict__ is still available.
@@ -125,13 +137,13 @@ class Node(metaclass=NodeMeta):
         fspath: Optional[py.path.local] = None,
         nodeid: Optional[str] = None,
     ) -> None:
-        #: a unique name within the scope of the parent node
+        #: A unique name within the scope of the parent node.
         self.name = name
 
-        #: the parent collector node.
+        #: The parent collector node.
         self.parent = parent
 
-        #: the pytest config object
+        #: The pytest config object.
         if config:
             self.config = config  # type: Config
         else:
@@ -139,7 +151,7 @@ class Node(metaclass=NodeMeta):
                 raise TypeError("config or parent must be provided")
             self.config = parent.config
 
-        #: the session this node is part of
+        #: The pytest session this node is part of.
         if session:
             self.session = session
         else:
@@ -147,20 +159,20 @@ class Node(metaclass=NodeMeta):
                 raise TypeError("session or parent must be provided")
             self.session = parent.session
 
-        #: filesystem path where this node was collected from (can be None)
+        #: Filesystem path where this node was collected from (can be None).
         self.fspath = fspath or getattr(parent, "fspath", None)
 
-        #: keywords/markers collected from all scopes
+        #: Keywords/markers collected from all scopes.
         self.keywords = NodeKeywords(self)
 
-        #: the marker objects belonging to this node
+        #: The marker objects belonging to this node.
         self.own_markers = []  # type: List[Mark]
 
-        #: allow adding of extra keywords to use for matching
+        #: Allow adding of extra keywords to use for matching.
         self.extra_keyword_matches = set()  # type: Set[str]
 
-        # used for storing artificial fixturedefs for direct parametrization
-        self._name2pseudofixturedef = {}  # type: Dict[str, FixtureDef]
+        # Used for storing artificial fixturedefs for direct parametrization.
+        self._name2pseudofixturedef = {}  # type: Dict[str, FixtureDef[Any]]
 
         if nodeid is not None:
             assert "::()" not in nodeid
@@ -178,15 +190,15 @@ class Node(metaclass=NodeMeta):
 
     @classmethod
     def from_parent(cls, parent: "Node", **kw):
-        """
-        Public Constructor for Nodes
+        """Public constructor for Nodes.
 
         This indirection got introduced in order to enable removing
         the fragile logic from the node constructors.
 
-        Subclasses can use ``super().from_parent(...)`` when overriding the construction
+        Subclasses can use ``super().from_parent(...)`` when overriding the
+        construction.
 
-        :param parent: the parent node of this test Node
+        :param parent: The parent node of this Node.
         """
         if "config" in kw:
             raise TypeError("config is not a valid argument for from_parent")
@@ -196,27 +208,27 @@ class Node(metaclass=NodeMeta):
 
     @property
     def ihook(self):
-        """ fspath sensitive hook proxy used to call pytest hooks"""
+        """fspath-sensitive hook proxy used to call pytest hooks."""
         return self.session.gethookproxy(self.fspath)
 
     def __repr__(self) -> str:
         return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
 
     def warn(self, warning: "PytestWarning") -> None:
-        """Issue a warning for this item.
+        """Issue a warning for this Node.
 
-        Warnings will be displayed after the test session, unless explicitly suppressed
+        Warnings will be displayed after the test session, unless explicitly suppressed.
 
-        :param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning.
+        :param Warning warning:
+            The warning instance to issue. Must be a subclass of PytestWarning.
 
-        :raise ValueError: if ``warning`` instance is not a subclass of PytestWarning.
+        :raises ValueError: If ``warning`` instance is not a subclass of PytestWarning.
 
         Example usage:
 
         .. code-block:: python
 
             node.warn(PytestWarning("some message"))
-
         """
         from _pytest.warning_types import PytestWarning
 
@@ -232,10 +244,11 @@ class Node(metaclass=NodeMeta):
             warning, category=None, filename=str(path), lineno=lineno + 1,
         )
 
-    # methods for ordering nodes
+    # Methods for ordering nodes.
+
     @property
     def nodeid(self) -> str:
-        """ a ::-separated string denoting its collection tree address. """
+        """A ::-separated string denoting its collection tree address."""
         return self._nodeid
 
     def __hash__(self) -> int:
@@ -248,8 +261,8 @@ class Node(metaclass=NodeMeta):
         pass
 
     def listchain(self) -> List["Node"]:
-        """ return list of all parent collectors up to self,
-            starting from root of collection tree. """
+        """Return list of all parent collectors up to self, starting from
+        the root of collection tree."""
         chain = []
         item = self  # type: Optional[Node]
         while item is not None:
@@ -261,12 +274,10 @@ class Node(metaclass=NodeMeta):
     def add_marker(
         self, marker: Union[str, MarkDecorator], append: bool = True
     ) -> None:
-        """dynamically add a marker object to the node.
+        """Dynamically add a marker object to the node.
 
-        :type marker: ``str`` or ``pytest.mark.*``  object
-        :param marker:
-            ``append=True`` whether to append the marker,
-            if ``False`` insert at position ``0``.
+        :param append:
+            Whether to append the marker, or prepend it.
         """
         from _pytest.mark import MARK_GEN
 
@@ -283,21 +294,19 @@ class Node(metaclass=NodeMeta):
             self.own_markers.insert(0, marker_.mark)
 
     def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]:
-        """
-        :param name: if given, filter the results by the name attribute
+        """Iterate over all markers of the node.
 
-        iterate over all markers of the node
+        :param name: If given, filter the results by the name attribute.
         """
         return (x[1] for x in self.iter_markers_with_node(name=name))
 
     def iter_markers_with_node(
         self, name: Optional[str] = None
     ) -> Iterator[Tuple["Node", Mark]]:
-        """
-        :param name: if given, filter the results by the name attribute
+        """Iterate over all markers of the node.
 
-        iterate over all markers of the node
-        returns sequence of tuples (node, mark)
+        :param name: If given, filter the results by the name attribute.
+        :returns: An iterator of (node, mark) tuples.
         """
         for node in reversed(self.listchain()):
             for mark in node.own_markers:
@@ -306,25 +315,25 @@ class Node(metaclass=NodeMeta):
 
     @overload
     def get_closest_marker(self, name: str) -> Optional[Mark]:
-        raise NotImplementedError()
+        ...
 
     @overload  # noqa: F811
     def get_closest_marker(self, name: str, default: Mark) -> Mark:  # noqa: F811
-        raise NotImplementedError()
+        ...
 
     def get_closest_marker(  # noqa: F811
         self, name: str, default: Optional[Mark] = None
     ) -> Optional[Mark]:
-        """return the first marker matching the name, from closest (for example function) to farther level (for example
-        module level).
+        """Return the first marker matching the name, from closest (for
+        example function) to farther level (for example module level).
 
-        :param default: fallback return value of no marker was found
-        :param name: name to filter by
+        :param default: Fallback return value if no marker was found.
+        :param name: Name to filter by.
         """
         return next(self.iter_markers(name=name), default)
 
     def listextrakeywords(self) -> Set[str]:
-        """ Return a set of all extra keywords in self and any parents."""
+        """Return a set of all extra keywords in self and any parents."""
         extra_keywords = set()  # type: Set[str]
         for item in self.listchain():
             extra_keywords.update(item.extra_keyword_matches)
@@ -334,7 +343,7 @@ class Node(metaclass=NodeMeta):
         return [x.name for x in self.listchain()]
 
     def addfinalizer(self, fin: Callable[[], object]) -> None:
-        """ register a function to be called when this node is finalized.
+        """Register a function to be called when this node is finalized.
 
         This method can only be called when this node is active
         in a setup chain, for example during self.setup().
@@ -342,15 +351,15 @@ class Node(metaclass=NodeMeta):
         self.session._setupstate.addfinalizer(fin, self)
 
     def getparent(self, cls: "Type[_NodeType]") -> Optional[_NodeType]:
-        """ get the next parent node (including ourself)
-        which is an instance of the given class"""
+        """Get the next parent node (including self) which is an instance of
+        the given class."""
         current = self  # type: Optional[Node]
         while current and not isinstance(current, cls):
             current = current.parent
         assert current is None or isinstance(current, cls)
         return current
 
-    def _prunetraceback(self, excinfo):
+    def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
         pass
 
     def _repr_failure_py(
@@ -393,7 +402,7 @@ class Node(metaclass=NodeMeta):
         # It will be better to just always display paths relative to invocation_dir, but
         # this requires a lot of plumbing (#6428).
         try:
-            abspath = Path(os.getcwd()) != Path(str(self.config.invocation_dir))
+            abspath = Path(os.getcwd()) != self.config.invocation_params.dir
         except OSError:
             abspath = True
 
@@ -411,8 +420,7 @@ class Node(metaclass=NodeMeta):
         excinfo: ExceptionInfo[BaseException],
         style: "Optional[_TracebackStyle]" = None,
     ) -> Union[str, TerminalRepr]:
-        """
-        Return a representation of a collection or test failure.
+        """Return a representation of a collection or test failure.
 
         :param excinfo: Exception information for the failure.
         """
@@ -422,13 +430,13 @@ class Node(metaclass=NodeMeta):
 def get_fslocation_from_item(
     node: "Node",
 ) -> Tuple[Union[str, py.path.local], Optional[int]]:
-    """Tries to extract the actual location from a node, depending on available attributes:
+    """Try to extract the actual location from a node, depending on available attributes:
 
     * "location": a pair (path, lineno)
     * "obj": a Python object that the node wraps.
     * "fspath": just a path
 
-    :rtype: a tuple of (str|LocalPath, int) with filename and line number.
+    :rtype: A tuple of (str|py.path.local, int) with filename and line number.
     """
     # See Item.location.
     location = getattr(
@@ -443,25 +451,22 @@ def get_fslocation_from_item(
 
 
 class Collector(Node):
-    """ Collector instances create children through collect()
-        and thus iteratively build a tree.
-    """
+    """Collector instances create children through collect() and thus
+    iteratively build a tree."""
 
     class CollectError(Exception):
-        """ an error during collection, contains a custom message. """
+        """An error during collection, contains a custom message."""
 
     def collect(self) -> Iterable[Union["Item", "Collector"]]:
-        """ returns a list of children (items and collectors)
-            for this collection node.
-        """
+        """Return a list of children (items and collectors) for this
+        collection node."""
         raise NotImplementedError("abstract")
 
     # TODO: This omits the style= parameter which breaks Liskov Substitution.
     def repr_failure(  # type: ignore[override]
         self, excinfo: ExceptionInfo[BaseException]
     ) -> Union[str, TerminalRepr]:
-        """
-        Return a representation of a collection failure.
+        """Return a representation of a collection failure.
 
         :param excinfo: Exception information for the failure.
         """
@@ -479,7 +484,7 @@ class Collector(Node):
 
         return self._repr_failure_py(excinfo, style=tbstyle)
 
-    def _prunetraceback(self, excinfo):
+    def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
         if hasattr(self, "fspath"):
             traceback = excinfo.traceback
             ntraceback = traceback.cut(path=self.fspath)
@@ -494,17 +499,6 @@ def _check_initialpaths_for_relpath(session, fspath):
             return fspath.relto(initial_path)
 
 
-class FSHookProxy:
-    def __init__(self, pm: PytestPluginManager, remove_mods) -> None:
-        self.pm = pm
-        self.remove_mods = remove_mods
-
-    def __getattr__(self, name: str):
-        x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
-        self.__dict__[name] = x
-        return x
-
-
 class FSCollector(Collector):
     def __init__(
         self,
@@ -534,73 +528,18 @@ class FSCollector(Collector):
 
         super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath)
 
-        self._norecursepatterns = self.config.getini("norecursedirs")
-
     @classmethod
     def from_parent(cls, parent, *, fspath, **kw):
-        """
-        The public constructor
-        """
+        """The public constructor."""
         return super().from_parent(parent=parent, fspath=fspath, **kw)
 
-    def _gethookproxy(self, fspath: py.path.local):
-        # check if we have the common case of running
-        # hooks with all conftest.py files
-        pm = self.config.pluginmanager
-        my_conftestmodules = pm._getconftestmodules(
-            fspath, self.config.getoption("importmode")
-        )
-        remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
-        if remove_mods:
-            # one or more conftests are not in use at this fspath
-            proxy = FSHookProxy(pm, remove_mods)
-        else:
-            # all plugins are active for this fspath
-            proxy = self.config.hook
-        return proxy
-
     def gethookproxy(self, fspath: py.path.local):
-        raise NotImplementedError()
-
-    def _recurse(self, dirpath: py.path.local) -> bool:
-        if dirpath.basename == "__pycache__":
-            return False
-        ihook = self._gethookproxy(dirpath.dirpath())
-        if ihook.pytest_ignore_collect(path=dirpath, config=self.config):
-            return False
-        for pat in self._norecursepatterns:
-            if dirpath.check(fnmatch=pat):
-                return False
-        ihook = self._gethookproxy(dirpath)
-        ihook.pytest_collect_directory(path=dirpath, parent=self)
-        return True
+        warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
+        return self.session.gethookproxy(fspath)
 
     def isinitpath(self, path: py.path.local) -> bool:
-        raise NotImplementedError()
-
-    def _collectfile(
-        self, path: py.path.local, handle_dupes: bool = True
-    ) -> Sequence[Collector]:
-        assert (
-            path.isfile()
-        ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
-            path, path.isdir(), path.exists(), path.islink()
-        )
-        ihook = self.gethookproxy(path)
-        if not self.isinitpath(path):
-            if ihook.pytest_ignore_collect(path=path, config=self.config):
-                return ()
-
-        if handle_dupes:
-            keepduplicates = self.config.getoption("keepduplicates")
-            if not keepduplicates:
-                duplicate_paths = self.config.pluginmanager._duplicatepaths
-                if path in duplicate_paths:
-                    return ()
-                else:
-                    duplicate_paths.add(path)
-
-        return ihook.pytest_collect_file(path=path, parent=self)  # type: ignore[no-any-return]
+        warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
+        return self.session.isinitpath(path)
 
 
 class File(FSCollector):
@@ -611,8 +550,9 @@ class File(FSCollector):
 
 
 class Item(Node):
-    """ a basic test invocation item. Note that for a single function
-    there might be multiple test invocation items.
+    """A basic test invocation item.
+
+    Note that for a single function there might be multiple test invocation items.
     """
 
     nextitem = None
@@ -628,17 +568,16 @@ class Item(Node):
         super().__init__(name, parent, config, session, nodeid=nodeid)
         self._report_sections = []  # type: List[Tuple[str, str, str]]
 
-        #: user properties is a list of tuples (name, value) that holds user
-        #: defined properties for this test.
+        #: A list of tuples (name, value) that holds user defined properties
+        #: for this test.
         self.user_properties = []  # type: List[Tuple[str, object]]
 
     def runtest(self) -> None:
         raise NotImplementedError("runtest must be implemented by Item subclass")
 
     def add_report_section(self, when: str, key: str, content: str) -> None:
-        """
-        Adds a new report section, similar to what's done internally to add stdout and
-        stderr captured output::
+        """Add a new report section, similar to what's done internally to add
+        stdout and stderr captured output::
 
             item.add_report_section("call", "stdout", "report section contents")
 
@@ -647,7 +586,6 @@ class Item(Node):
         :param str key:
             Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
             ``"stderr"`` internally.
-
         :param str content:
             The full contents as a string.
         """
@@ -660,10 +598,7 @@ class Item(Node):
     @cached_property
     def location(self) -> Tuple[str, Optional[int], str]:
         location = self.reportinfo()
-        if isinstance(location[0], py.path.local):
-            fspath = location[0]
-        else:
-            fspath = py.path.local(location[0])
+        fspath = absolutepath(str(location[0]))
         relfspath = self.session._node_location_to_relpath(fspath)
         assert type(location[2]) is str
         return (relfspath, location[1], location[2])
index 8bdc310ac181508d735722578953fbb343892d41..bb8f99772ac9eb75e16a6e08ad9c6926409cdaba 100644 (file)
@@ -1,4 +1,4 @@
-""" run test suites written for nose. """
+"""Run testsuites written for nose."""
 from _pytest import python
 from _pytest import unittest
 from _pytest.config import hookimpl
@@ -9,9 +9,9 @@ from _pytest.nodes import Item
 def pytest_runtest_setup(item):
     if is_potential_nosetest(item):
         if not call_optional(item.obj, "setup"):
-            # call module level setup if there is no object level one
+            # Call module level setup if there is no object level one.
             call_optional(item.parent.obj, "setup")
-        # XXX this implies we only call teardown when setup worked
+        # XXX This implies we only call teardown when setup worked.
         item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item)
 
 
@@ -22,8 +22,8 @@ def teardown_nose(item):
 
 
 def is_potential_nosetest(item: Item) -> bool:
-    # extra check needed since we do not do nose style setup/teardown
-    # on direct unittest style classes
+    # Extra check needed since we do not do nose style setup/teardown
+    # on direct unittest style classes.
     return isinstance(item, python.Function) and not isinstance(
         item, unittest.TestCaseFunction
     )
@@ -34,6 +34,6 @@ def call_optional(obj, name):
     isfixture = hasattr(method, "_pytestfixturefunction")
     if method is not None and not isfixture and callable(method):
         # If there's any problems allow the exception to raise rather than
-        # silently ignoring them
+        # silently ignoring them.
         method()
         return True
index 751cf9474fb3a845c18902c7ed92204b2c1a48f1..a2ddc3a1f1ad09509c5995acba29adc163d89f32 100644 (file)
@@ -1,7 +1,5 @@
-"""
-exception classes and constants handling test outcomes
-as well as functions creating them
-"""
+"""Exception classes and constants handling test outcomes as well as
+functions creating them."""
 import sys
 from typing import Any
 from typing import Callable
@@ -9,7 +7,7 @@ from typing import cast
 from typing import Optional
 from typing import TypeVar
 
-TYPE_CHECKING = False  # avoid circular import through compat
+TYPE_CHECKING = False  # Avoid circular import through compat.
 
 if TYPE_CHECKING:
     from typing import NoReturn
@@ -25,13 +23,12 @@ else:
 
 
 class OutcomeException(BaseException):
-    """ OutcomeException and its subclass instances indicate and
-        contain info about test and collection outcomes.
-    """
+    """OutcomeException and its subclass instances indicate and contain info
+    about test and collection outcomes."""
 
     def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
         if msg is not None and not isinstance(msg, str):
-            error_msg = (
+            error_msg = (  # type: ignore[unreachable]
                 "{} expected string as 'msg' parameter, got '{}' instead.\n"
                 "Perhaps you meant to use a mark?"
             )
@@ -67,13 +64,13 @@ class Skipped(OutcomeException):
 
 
 class Failed(OutcomeException):
-    """ raised from an explicit call to pytest.fail() """
+    """Raised from an explicit call to pytest.fail()."""
 
     __module__ = "builtins"
 
 
 class Exit(Exception):
-    """ raised for immediate program exits (no tracebacks/summaries)"""
+    """Raised for immediate program exits (no tracebacks/summaries)."""
 
     def __init__(
         self, msg: str = "unknown reason", returncode: Optional[int] = None
@@ -86,7 +83,7 @@ class Exit(Exception):
 # Elaborate hack to work around https://github.com/python/mypy/issues/2087.
 # Ideally would just be `exit.Exception = Exit` etc.
 
-_F = TypeVar("_F", bound=Callable)
+_F = TypeVar("_F", bound=Callable[..., object])
 _ET = TypeVar("_ET", bound="Type[BaseException]")
 
 
@@ -104,16 +101,15 @@ def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _E
     return decorate
 
 
-# exposed helper methods
+# Exposed helper methods.
 
 
 @_with_exception(Exit)
 def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn":
-    """
-    Exit testing process.
+    """Exit testing process.
 
-    :param str msg: message to display upon exit.
-    :param int returncode: return code to be used when exiting pytest.
+    :param str msg: Message to display upon exit.
+    :param int returncode: Return code to be used when exiting pytest.
     """
     __tracebackhide__ = True
     raise Exit(msg, returncode)
@@ -121,20 +117,20 @@ def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn":
 
 @_with_exception(Skipped)
 def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn":
-    """
-    Skip an executing test with the given message.
+    """Skip an executing test with the given message.
 
     This function should be called only during testing (setup, call or teardown) or
     during collection by using the ``allow_module_level`` flag.  This function can
     be called in doctests as well.
 
-    :kwarg bool allow_module_level: allows this function to be called at
-        module level, skipping the rest of the module. Default to False.
+    :param bool allow_module_level:
+        Allows this function to be called at module level, skipping the rest
+        of the module. Defaults to False.
 
     .. note::
-        It is better to use the :ref:`pytest.mark.skipif ref` marker when possible to declare a test to be
-        skipped under certain conditions like mismatching platforms or
-        dependencies.
+        It is better to use the :ref:`pytest.mark.skipif ref` marker when
+        possible to declare a test to be skipped under certain conditions
+        like mismatching platforms or dependencies.
         Similarly, use the ``# doctest: +SKIP`` directive (see `doctest.SKIP
         <https://docs.python.org/3/library/doctest.html#doctest.SKIP>`_)
         to skip a doctest statically.
@@ -145,11 +141,12 @@ def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn":
 
 @_with_exception(Failed)
 def fail(msg: str = "", pytrace: bool = True) -> "NoReturn":
-    """
-    Explicitly fail an executing test with the given message.
+    """Explicitly fail an executing test with the given message.
 
-    :param str msg: the message to show the user as reason for the failure.
-    :param bool pytrace: if false the msg represents the full failure information and no
+    :param str msg:
+        The message to show the user as reason for the failure.
+    :param bool pytrace:
+        If False, msg represents the full failure information and no
         python traceback will be reported.
     """
     __tracebackhide__ = True
@@ -157,19 +154,19 @@ def fail(msg: str = "", pytrace: bool = True) -> "NoReturn":
 
 
 class XFailed(Failed):
-    """ raised from an explicit call to pytest.xfail() """
+    """Raised from an explicit call to pytest.xfail()."""
 
 
 @_with_exception(XFailed)
 def xfail(reason: str = "") -> "NoReturn":
-    """
-    Imperatively xfail an executing test or setup functions with the given reason.
+    """Imperatively xfail an executing test or setup function with the given reason.
 
     This function should be called only during testing (setup, call or teardown).
 
     .. note::
-        It is better to use the :ref:`pytest.mark.xfail ref` marker when possible to declare a test to be
-        xfailed under certain conditions like known bugs or missing features.
+        It is better to use the :ref:`pytest.mark.xfail ref` marker when
+        possible to declare a test to be xfailed under certain conditions
+        like known bugs or missing features.
     """
     __tracebackhide__ = True
     raise XFailed(reason)
@@ -178,17 +175,20 @@ def xfail(reason: str = "") -> "NoReturn":
 def importorskip(
     modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
 ) -> Any:
-    """Imports and returns the requested module ``modname``, or skip the
+    """Import and return the requested module ``modname``, or skip the
     current test if the module cannot be imported.
 
-    :param str modname: the name of the module to import
-    :param str minversion: if given, the imported module's ``__version__``
-        attribute must be at least this minimal version, otherwise the test is
-        still skipped.
-    :param str reason: if given, this reason is shown as the message when the
-        module cannot be imported.
-    :returns: The imported module. This should be assigned to its canonical
-        name.
+    :param str modname:
+        The name of the module to import.
+    :param str minversion:
+        If given, the imported module's ``__version__`` attribute must be at
+        least this minimal version, otherwise the test is still skipped.
+    :param str reason:
+        If given, this reason is shown as the message when the module cannot
+        be imported.
+
+    :returns:
+        The imported module. This should be assigned to its canonical name.
 
     Example::
 
@@ -200,9 +200,9 @@ def importorskip(
     compile(modname, "", "eval")  # to catch syntaxerrors
 
     with warnings.catch_warnings():
-        # make sure to ignore ImportWarnings that might happen because
+        # Make sure to ignore ImportWarnings that might happen because
         # of existing directories with the same name we're trying to
-        # import but without a __init__.py file
+        # import but without a __init__.py file.
         warnings.simplefilter("ignore")
         try:
             __import__(modname)
index a3432c7a10c71fbcb24bbc6ddc0f51cccdc83937..0546d237762269c3708fd84531d2fe44e6c886f9 100644 (file)
@@ -1,4 +1,4 @@
-""" submit failure or test session information to a pastebin service. """
+"""Submit failure or test session information to a pastebin service."""
 import tempfile
 from io import StringIO
 from typing import IO
@@ -32,11 +32,11 @@ def pytest_addoption(parser: Parser) -> None:
 def pytest_configure(config: Config) -> None:
     if config.option.pastebin == "all":
         tr = config.pluginmanager.getplugin("terminalreporter")
-        # if no terminal reporter plugin is present, nothing we can do here;
+        # If no terminal reporter plugin is present, nothing we can do here;
         # this can happen when this function executes in a worker node
-        # when using pytest-xdist, for example
+        # when using pytest-xdist, for example.
         if tr is not None:
-            # pastebin file will be utf-8 encoded binary file
+            # pastebin file will be UTF-8 encoded binary file.
             config._store[pastebinfile_key] = tempfile.TemporaryFile("w+b")
             oldwrite = tr._tw.write
 
@@ -52,26 +52,25 @@ def pytest_configure(config: Config) -> None:
 def pytest_unconfigure(config: Config) -> None:
     if pastebinfile_key in config._store:
         pastebinfile = config._store[pastebinfile_key]
-        # get terminal contents and delete file
+        # Get terminal contents and delete file.
         pastebinfile.seek(0)
         sessionlog = pastebinfile.read()
         pastebinfile.close()
         del config._store[pastebinfile_key]
-        # undo our patching in the terminal reporter
+        # Undo our patching in the terminal reporter.
         tr = config.pluginmanager.getplugin("terminalreporter")
         del tr._tw.__dict__["write"]
-        # write summary
+        # Write summary.
         tr.write_sep("=", "Sending information to Paste Service")
         pastebinurl = create_new_paste(sessionlog)
         tr.write_line("pastebin session-log: %s\n" % pastebinurl)
 
 
 def create_new_paste(contents: Union[str, bytes]) -> str:
-    """
-    Creates a new paste using bpaste.net service.
+    """Create a new paste using the bpaste.net service.
 
-    :contents: paste contents string
-    :returns: url to the pasted contents or error message
+    :contents: Paste contents string.
+    :returns: URL to the pasted contents, or an error message.
     """
     import re
     from urllib.request import urlopen
index 92ba32082a535220924516fa3eb783affbbdfe45..355281039fd1af612b520b660573b9f709e9d1a9 100644 (file)
@@ -16,6 +16,7 @@ from os.path import isabs
 from os.path import sep
 from posixpath import sep as posix_sep
 from types import ModuleType
+from typing import Callable
 from typing import Iterable
 from typing import Iterator
 from typing import Optional
@@ -48,23 +49,21 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
 
 
 def ensure_reset_dir(path: Path) -> None:
-    """
-    ensures the given path is an empty directory
-    """
+    """Ensure the given path is an empty directory."""
     if path.exists():
         rm_rf(path)
     path.mkdir()
 
 
 def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
-    """Handles known read-only errors during rmtree.
+    """Handle known read-only errors during rmtree.
 
     The returned value is used only by our own tests.
     """
     exctype, excvalue = exc[:2]
 
-    # another process removed the file in the middle of the "rm_rf" (xdist for example)
-    # more context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
+    # Another process removed the file in the middle of the "rm_rf" (xdist for example).
+    # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
     if isinstance(excvalue, FileNotFoundError):
         return False
 
@@ -100,7 +99,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
     if p.is_file():
         for parent in p.parents:
             chmod_rw(str(parent))
-            # stop when we reach the original path passed to rm_rf
+            # Stop when we reach the original path passed to rm_rf.
             if parent == start_path:
                 break
     chmod_rw(str(path))
@@ -128,7 +127,7 @@ def ensure_extended_length_path(path: Path) -> Path:
 
 
 def get_extended_length_path_str(path: str) -> str:
-    """Converts to extended length path as a str"""
+    """Convert a path to a Windows extended length path."""
     long_path_prefix = "\\\\?\\"
     unc_long_path_prefix = "\\\\?\\UNC\\"
     if path.startswith((long_path_prefix, unc_long_path_prefix)):
@@ -141,15 +140,14 @@ def get_extended_length_path_str(path: str) -> str:
 
 def rm_rf(path: Path) -> None:
     """Remove the path contents recursively, even if some elements
-    are read-only.
-    """
+    are read-only."""
     path = ensure_extended_length_path(path)
     onerror = partial(on_rm_rf_error, start_path=path)
     shutil.rmtree(str(path), onerror=onerror)
 
 
 def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
-    """finds all elements in root that begin with the prefix, case insensitive"""
+    """Find all elements in root that begin with the prefix, case insensitive."""
     l_prefix = prefix.lower()
     for x in root.iterdir():
         if x.name.lower().startswith(l_prefix):
@@ -157,10 +155,10 @@ def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
 
 
 def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]:
-    """
-    :param iter: iterator over path names
-    :param prefix: expected prefix of the path names
-    :returns: the parts of the paths following the prefix
+    """Return the parts of the paths following the prefix.
+
+    :param iter: Iterator over path names.
+    :param prefix: Expected prefix of the path names.
     """
     p_len = len(prefix)
     for p in iter:
@@ -168,13 +166,12 @@ def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]:
 
 
 def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
-    """combines find_prefixes and extract_suffixes
-    """
+    """Combine find_prefixes and extract_suffixes."""
     return extract_suffixes(find_prefixed(root, prefix), prefix)
 
 
 def parse_num(maybe_num) -> int:
-    """parses number path suffixes, returns -1 on error"""
+    """Parse number path suffixes, returns -1 on error."""
     try:
         return int(maybe_num)
     except ValueError:
@@ -184,13 +181,13 @@ def parse_num(maybe_num) -> int:
 def _force_symlink(
     root: Path, target: Union[str, PurePath], link_to: Union[str, Path]
 ) -> None:
-    """helper to create the current symlink
+    """Helper to create the current symlink.
 
-    it's full of race conditions that are reasonably ok to ignore
-    for the context of best effort linking to the latest test run
+    It's full of race conditions that are reasonably OK to ignore
+    for the context of best effort linking to the latest test run.
 
-    the presumption being that in case of much parallelism
-    the inaccuracy is going to be acceptable
+    The presumption being that in case of much parallelism
+    the inaccuracy is going to be acceptable.
     """
     current_symlink = root.joinpath(target)
     try:
@@ -204,7 +201,7 @@ def _force_symlink(
 
 
 def make_numbered_dir(root: Path, prefix: str) -> Path:
-    """create a directory with an increased number as suffix for the given prefix"""
+    """Create a directory with an increased number as suffix for the given prefix."""
     for i in range(10):
         # try up to 10 times to create the folder
         max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
@@ -225,7 +222,7 @@ def make_numbered_dir(root: Path, prefix: str) -> Path:
 
 
 def create_cleanup_lock(p: Path) -> Path:
-    """crates a lock to prevent premature folder cleanup"""
+    """Create a lock to prevent premature folder cleanup."""
     lock_path = get_lock_path(p)
     try:
         fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
@@ -242,7 +239,7 @@ def create_cleanup_lock(p: Path) -> Path:
 
 
 def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
-    """registers a cleanup function for removing a lock, by default on atexit"""
+    """Register a cleanup function for removing a lock, by default on atexit."""
     pid = os.getpid()
 
     def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None:
@@ -259,7 +256,8 @@ def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
 
 
 def maybe_delete_a_numbered_dir(path: Path) -> None:
-    """removes a numbered directory if its lock can be obtained and it does not seem to be in use"""
+    """Remove a numbered directory if its lock can be obtained and it does
+    not seem to be in use."""
     path = ensure_extended_length_path(path)
     lock_path = None
     try:
@@ -276,8 +274,8 @@ def maybe_delete_a_numbered_dir(path: Path) -> None:
         #  * process cwd (Windows)
         return
     finally:
-        # if we created the lock, ensure we remove it even if we failed
-        # to properly remove the numbered dir
+        # If we created the lock, ensure we remove it even if we failed
+        # to properly remove the numbered dir.
         if lock_path is not None:
             try:
                 lock_path.unlink()
@@ -286,7 +284,7 @@ def maybe_delete_a_numbered_dir(path: Path) -> None:
 
 
 def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool:
-    """checks if `path` is deletable based on whether the lock file is expired"""
+    """Check if `path` is deletable based on whether the lock file is expired."""
     if path.is_symlink():
         return False
     lock = get_lock_path(path)
@@ -303,9 +301,9 @@ def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) ->
         return False
     else:
         if lock_time < consider_lock_dead_if_created_before:
-            # wa want to ignore any errors while trying to remove the lock such as:
-            # - PermissionDenied, like the file permissions have changed since the lock creation
-            # - FileNotFoundError, in case another pytest process got here first.
+            # We want to ignore any errors while trying to remove the lock such as:
+            # - PermissionDenied, like the file permissions have changed since the lock creation;
+            # - FileNotFoundError, in case another pytest process got here first;
             # and any other cause of failure.
             with contextlib.suppress(OSError):
                 lock.unlink()
@@ -314,13 +312,13 @@ def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) ->
 
 
 def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None:
-    """tries to cleanup a folder if we can ensure it's deletable"""
+    """Try to cleanup a folder if we can ensure it's deletable."""
     if ensure_deletable(path, consider_lock_dead_if_created_before):
         maybe_delete_a_numbered_dir(path)
 
 
 def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
-    """lists candidates for numbered directories to be removed - follows py.path"""
+    """List candidates for numbered directories to be removed - follows py.path."""
     max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
     max_delete = max_existing - keep
     paths = find_prefixed(root, prefix)
@@ -334,7 +332,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
 def cleanup_numbered_dir(
     root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
 ) -> None:
-    """cleanup for lock driven numbered directories"""
+    """Cleanup for lock driven numbered directories."""
     for path in cleanup_candidates(root, prefix, keep):
         try_cleanup(path, consider_lock_dead_if_created_before)
     for path in root.glob("garbage-*"):
@@ -344,7 +342,7 @@ def cleanup_numbered_dir(
 def make_numbered_dir_with_cleanup(
     root: Path, prefix: str, keep: int, lock_timeout: float
 ) -> Path:
-    """creates a numbered dir with a cleanup lock and removes old ones"""
+    """Create a numbered dir with a cleanup lock and remove old ones."""
     e = None
     for i in range(10):
         try:
@@ -368,9 +366,7 @@ def make_numbered_dir_with_cleanup(
     raise e
 
 
-def resolve_from_str(input: str, root: py.path.local) -> Path:
-    assert not isinstance(input, Path), "would break on py2"
-    rootpath = Path(root)
+def resolve_from_str(input: str, rootpath: Path) -> Path:
     input = expanduser(input)
     input = expandvars(input)
     if isabs(input):
@@ -380,17 +376,18 @@ def resolve_from_str(input: str, root: py.path.local) -> Path:
 
 
 def fnmatch_ex(pattern: str, path) -> bool:
-    """FNMatcher port from py.path.common which works with PurePath() instances.
+    """A port of FNMatcher from py.path.common which works with PurePath() instances.
 
-    The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions
-    for each part of the path, while this algorithm uses the whole path instead.
+    The difference between this algorithm and PurePath.match() is that the
+    latter matches "**" glob expressions for each part of the path, while
+    this algorithm uses the whole path instead.
 
     For example:
-        "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" with this algorithm, but not with
-        PurePath.match().
+        "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py"
+        with this algorithm, but not with PurePath.match().
 
-    This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according
-    this logic.
+    This algorithm was ported to keep backward-compatibility with existing
+    settings which assume paths match according this logic.
 
     References:
     * https://bugs.python.org/issue29249
@@ -420,7 +417,7 @@ def parts(s: str) -> Set[str]:
 
 
 def symlink_or_skip(src, dst, **kwargs):
-    """Makes a symlink or skips the test in case symlinks are not supported."""
+    """Make a symlink, or skip the test in case symlinks are not supported."""
     try:
         os.symlink(str(src), str(dst), **kwargs)
     except OSError as e:
@@ -428,7 +425,7 @@ def symlink_or_skip(src, dst, **kwargs):
 
 
 class ImportMode(Enum):
-    """Possible values for `mode` parameter of `import_path`"""
+    """Possible values for `mode` parameter of `import_path`."""
 
     prepend = "prepend"
     append = "append"
@@ -449,8 +446,7 @@ def import_path(
     *,
     mode: Union[str, ImportMode] = ImportMode.prepend
 ) -> ModuleType:
-    """
-    Imports and returns a module from the given path, which can be a file (a module) or
+    """Import and return a module from the given path, which can be a file (a module) or
     a directory (a package).
 
     The import mechanism used is controlled by the `mode` parameter:
@@ -466,7 +462,8 @@ def import_path(
       to import the module, which avoids having to use `__import__` and muck with `sys.path`
       at all. It effectively allows having same-named test modules in different places.
 
-    :raise ImportPathMismatchError: if after importing the given `path` and the module `__file__`
+    :raises ImportPathMismatchError:
+        If after importing the given `path` and the module `__file__`
         are different. Only raised in `prepend` and `append` modes.
     """
     mode = ImportMode(mode)
@@ -505,7 +502,7 @@ def import_path(
         pkg_root = path.parent
         module_name = path.stem
 
-    # change sys.path permanently: restoring it at the end of this function would cause surprising
+    # Change sys.path permanently: restoring it at the end of this function would cause surprising
     # problems because of delayed imports: for example, a conftest.py file imported by this function
     # might have local imports, which would fail at runtime if we restored sys.path.
     if mode is ImportMode.append:
@@ -545,7 +542,8 @@ def import_path(
 def resolve_package_path(path: Path) -> Optional[Path]:
     """Return the Python package path by looking for the last
     directory upwards which still contains an __init__.py.
-    Return None if it can not be determined.
+
+    Returns None if it can not be determined.
     """
     result = None
     for parent in itertools.chain((path,), path.parents):
@@ -556,3 +554,58 @@ def resolve_package_path(path: Path) -> Optional[Path]:
                 break
             result = parent
     return result
+
+
+def visit(
+    path: str, recurse: Callable[["os.DirEntry[str]"], bool]
+) -> Iterator["os.DirEntry[str]"]:
+    """Walk a directory recursively, in breadth-first order.
+
+    Entries at each directory level are sorted.
+    """
+    entries = sorted(os.scandir(path), key=lambda entry: entry.name)
+    yield from entries
+    for entry in entries:
+        if entry.is_dir(follow_symlinks=False) and recurse(entry):
+            yield from visit(entry.path, recurse)
+
+
+def absolutepath(path: Union[Path, str]) -> Path:
+    """Convert a path to an absolute path using os.path.abspath.
+
+    Prefer this over Path.resolve() (see #6523).
+    Prefer this over Path.absolute() (not public, doesn't normalize).
+    """
+    return Path(os.path.abspath(str(path)))
+
+
+def commonpath(path1: Path, path2: Path) -> Optional[Path]:
+    """Return the common part shared with the other path, or None if there is
+    no common part."""
+    try:
+        return Path(os.path.commonpath((str(path1), str(path2))))
+    except ValueError:
+        return None
+
+
+def bestrelpath(directory: Path, dest: Path) -> str:
+    """Return a string which is a relative path from directory to dest such
+    that directory/bestrelpath == dest.
+
+    If no such path can be determined, returns dest.
+    """
+    if dest == directory:
+        return os.curdir
+    # Find the longest common directory.
+    base = commonpath(directory, dest)
+    # Can be the case on Windows.
+    if not base:
+        return str(dest)
+    reldirectory = directory.relative_to(base)
+    reldest = dest.relative_to(base)
+    return os.path.join(
+        # Back from directory to base.
+        *([os.pardir] * len(reldirectory.parts)),
+        # Forward from base to dest.
+        *reldest.parts,
+    )
index 594abee9094b36026c1a272d9016179c504ff48c..d78062a86ce3b053f815aeb816ff65bef26532d5 100644 (file)
@@ -1,4 +1,4 @@
-"""(disabled by default) support for testing pytest and pytest plugins."""
+"""(Disabled by default) support for testing pytest and pytest plugins."""
 import collections.abc
 import gc
 import importlib
@@ -28,6 +28,8 @@ import pytest
 from _pytest import timing
 from _pytest._code import Source
 from _pytest.capture import _get_multicapture
+from _pytest.compat import final
+from _pytest.compat import overload
 from _pytest.compat import TYPE_CHECKING
 from _pytest.config import _PluggyPlugin
 from _pytest.config import Config
@@ -42,11 +44,13 @@ from _pytest.nodes import Item
 from _pytest.pathlib import make_numbered_dir
 from _pytest.pathlib import Path
 from _pytest.python import Module
+from _pytest.reports import CollectReport
 from _pytest.reports import TestReport
 from _pytest.tmpdir import TempdirFactory
 
 if TYPE_CHECKING:
     from typing import Type
+    from typing_extensions import Literal
 
     import pexpect
 
@@ -166,9 +170,7 @@ class LsofFdLeakChecker:
 def _pytest(request: FixtureRequest) -> "PytestArg":
     """Return a helper which offers a gethookrecorder(hook) method which
     returns a HookRecorder instance which helps to make assertions about called
-    hooks.
-
-    """
+    hooks."""
     return PytestArg(request)
 
 
@@ -182,25 +184,25 @@ class PytestArg:
         return hookrecorder
 
 
-def get_public_names(values):
+def get_public_names(values: Iterable[str]) -> List[str]:
     """Only return names from iterator values without a leading underscore."""
     return [x for x in values if x[0] != "_"]
 
 
 class ParsedCall:
-    def __init__(self, name, kwargs):
+    def __init__(self, name: str, kwargs) -> None:
         self.__dict__.update(kwargs)
         self._name = name
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         d = self.__dict__.copy()
         del d["_name"]
         return "<ParsedCall {!r}(**{!r})>".format(self._name, d)
 
     if TYPE_CHECKING:
         # The class has undetermined attributes, this tells mypy about it.
-        def __getattr__(self, key):
-            raise NotImplementedError()
+        def __getattr__(self, key: str):
+            ...
 
 
 class HookRecorder:
@@ -208,12 +210,12 @@ class HookRecorder:
 
     This wraps all the hook calls in the plugin manager, recording each call
     before propagating the normal calls.
-
     """
 
     def __init__(self, pluginmanager: PytestPluginManager) -> None:
         self._pluginmanager = pluginmanager
         self.calls = []  # type: List[ParsedCall]
+        self.ret = None  # type: Optional[Union[int, ExitCode]]
 
         def before(hook_name: str, hook_impls, kwargs) -> None:
             self.calls.append(ParsedCall(hook_name, kwargs))
@@ -231,7 +233,7 @@ class HookRecorder:
             names = names.split()
         return [call for call in self.calls if call._name in names]
 
-    def assert_contains(self, entries) -> None:
+    def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None:
         __tracebackhide__ = True
         i = 0
         entries = list(entries)
@@ -269,23 +271,47 @@ class HookRecorder:
 
     # functionality for test reports
 
+    @overload
     def getreports(
+        self, names: "Literal['pytest_collectreport']",
+    ) -> Sequence[CollectReport]:
+        ...
+
+    @overload  # noqa: F811
+    def getreports(  # noqa: F811
+        self, names: "Literal['pytest_runtest_logreport']",
+    ) -> Sequence[TestReport]:
+        ...
+
+    @overload  # noqa: F811
+    def getreports(  # noqa: F811
+        self,
+        names: Union[str, Iterable[str]] = (
+            "pytest_collectreport",
+            "pytest_runtest_logreport",
+        ),
+    ) -> Sequence[Union[CollectReport, TestReport]]:
+        ...
+
+    def getreports(  # noqa: F811
         self,
-        names: Union[
-            str, Iterable[str]
-        ] = "pytest_runtest_logreport pytest_collectreport",
-    ) -> List[TestReport]:
+        names: Union[str, Iterable[str]] = (
+            "pytest_collectreport",
+            "pytest_runtest_logreport",
+        ),
+    ) -> Sequence[Union[CollectReport, TestReport]]:
         return [x.report for x in self.getcalls(names)]
 
     def matchreport(
         self,
         inamepart: str = "",
-        names: Union[
-            str, Iterable[str]
-        ] = "pytest_runtest_logreport pytest_collectreport",
-        when=None,
-    ):
-        """return a testreport whose dotted import path matches"""
+        names: Union[str, Iterable[str]] = (
+            "pytest_runtest_logreport",
+            "pytest_collectreport",
+        ),
+        when: Optional[str] = None,
+    ) -> Union[CollectReport, TestReport]:
+        """Return a testreport whose dotted import path matches."""
         values = []
         for rep in self.getreports(names=names):
             if not when and rep.when != "call" and rep.passed:
@@ -308,26 +334,56 @@ class HookRecorder:
             )
         return values[0]
 
+    @overload
     def getfailures(
+        self, names: "Literal['pytest_collectreport']",
+    ) -> Sequence[CollectReport]:
+        ...
+
+    @overload  # noqa: F811
+    def getfailures(  # noqa: F811
+        self, names: "Literal['pytest_runtest_logreport']",
+    ) -> Sequence[TestReport]:
+        ...
+
+    @overload  # noqa: F811
+    def getfailures(  # noqa: F811
         self,
-        names: Union[
-            str, Iterable[str]
-        ] = "pytest_runtest_logreport pytest_collectreport",
-    ) -> List[TestReport]:
+        names: Union[str, Iterable[str]] = (
+            "pytest_collectreport",
+            "pytest_runtest_logreport",
+        ),
+    ) -> Sequence[Union[CollectReport, TestReport]]:
+        ...
+
+    def getfailures(  # noqa: F811
+        self,
+        names: Union[str, Iterable[str]] = (
+            "pytest_collectreport",
+            "pytest_runtest_logreport",
+        ),
+    ) -> Sequence[Union[CollectReport, TestReport]]:
         return [rep for rep in self.getreports(names) if rep.failed]
 
-    def getfailedcollections(self) -> List[TestReport]:
+    def getfailedcollections(self) -> Sequence[CollectReport]:
         return self.getfailures("pytest_collectreport")
 
     def listoutcomes(
         self,
-    ) -> Tuple[List[TestReport], List[TestReport], List[TestReport]]:
+    ) -> Tuple[
+        Sequence[TestReport],
+        Sequence[Union[CollectReport, TestReport]],
+        Sequence[Union[CollectReport, TestReport]],
+    ]:
         passed = []
         skipped = []
         failed = []
-        for rep in self.getreports("pytest_collectreport pytest_runtest_logreport"):
+        for rep in self.getreports(
+            ("pytest_collectreport", "pytest_runtest_logreport")
+        ):
             if rep.passed:
                 if rep.when == "call":
+                    assert isinstance(rep, TestReport)
                     passed.append(rep)
             elif rep.skipped:
                 skipped.append(rep)
@@ -358,17 +414,14 @@ class HookRecorder:
 
 @pytest.fixture
 def linecomp() -> "LineComp":
-    """
-    A :class: `LineComp` instance for checking that an input linearly
-    contains a sequence of strings.
-    """
+    """A :class: `LineComp` instance for checking that an input linearly
+    contains a sequence of strings."""
     return LineComp()
 
 
 @pytest.fixture(name="LineMatcher")
 def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]":
-    """
-    A reference to the :class: `LineMatcher`.
+    """A reference to the :class: `LineMatcher`.
 
     This is instantiable with a list of lines (without their trailing newlines).
     This is useful for testing large texts, such as the output of commands.
@@ -378,12 +431,10 @@ def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]":
 
 @pytest.fixture
 def testdir(request: FixtureRequest, tmpdir_factory: TempdirFactory) -> "Testdir":
-    """
-    A :class: `TestDir` instance, that can be used to run and test pytest itself.
+    """A :class: `TestDir` instance, that can be used to run and test pytest itself.
 
     It is particularly useful for testing plugins. It is similar to the `tmpdir` fixture
     but provides methods which aid in testing pytest itself.
-
     """
     return Testdir(request, tmpdir_factory)
 
@@ -406,9 +457,9 @@ def _config_for_test() -> Generator[Config, None, None]:
     config._ensure_unconfigure()  # cleanup, e.g. capman closing tmpfiles.
 
 
-# regex to match the session duration string in the summary: "74.34s"
+# Regex to match the session duration string in the summary: "74.34s".
 rex_session_duration = re.compile(r"\d+\.\d\ds")
-# regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped"
+# Regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped".
 rex_outcome = re.compile(r"(\d+) (\w+)")
 
 
@@ -424,13 +475,13 @@ class RunResult:
     ) -> None:
         try:
             self.ret = pytest.ExitCode(ret)  # type: Union[int, ExitCode]
-            """the return value"""
+            """The return value."""
         except ValueError:
             self.ret = ret
         self.outlines = outlines
-        """list of lines captured from stdout"""
+        """List of lines captured from stdout."""
         self.errlines = errlines
-        """list of lines captured from stderr"""
+        """List of lines captured from stderr."""
         self.stdout = LineMatcher(outlines)
         """:class:`LineMatcher` of stdout.
 
@@ -438,9 +489,9 @@ class RunResult:
         :func:`stdout.fnmatch_lines() <LineMatcher.fnmatch_lines()>` method.
         """
         self.stderr = LineMatcher(errlines)
-        """:class:`LineMatcher` of stderr"""
+        """:class:`LineMatcher` of stderr."""
         self.duration = duration
-        """duration in seconds"""
+        """Duration in seconds."""
 
     def __repr__(self) -> str:
         return (
@@ -456,19 +507,19 @@ class RunResult:
 
             ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
 
-        Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``
+        Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
         """
         return self.parse_summary_nouns(self.outlines)
 
     @classmethod
     def parse_summary_nouns(cls, lines) -> Dict[str, int]:
-        """Extracts the nouns from a pytest terminal summary line.
+        """Extract the nouns from a pytest terminal summary line.
 
         It always returns the plural noun for consistency::
 
             ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
 
-        Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``
+        Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
         """
         for line in reversed(lines):
             if rex_session_duration.search(line):
@@ -494,8 +545,7 @@ class RunResult:
         xfailed: int = 0,
     ) -> None:
         """Assert that the specified outcomes appear with the respective
-        numbers (0 means it didn't occur) in the text output from a test run.
-        """
+        numbers (0 means it didn't occur) in the text output from a test run."""
         __tracebackhide__ = True
 
         d = self.parseoutcomes()
@@ -548,10 +598,11 @@ class SysPathsSnapshot:
         sys.path[:], sys.meta_path[:] = self.__saved
 
 
+@final
 class Testdir:
     """Temporary test directory with tools to test/run pytest itself.
 
-    This is based on the ``tmpdir`` fixture but provides a number of methods
+    This is based on the :fixture:`tmpdir` fixture but provides a number of methods
     which aid with testing pytest itself.  Unless :py:meth:`chdir` is used all
     methods will use :py:attr:`tmpdir` as their current working directory.
 
@@ -559,11 +610,11 @@ class Testdir:
 
     :ivar tmpdir: The :py:class:`py.path.local` instance of the temporary directory.
 
-    :ivar plugins: A list of plugins to use with :py:meth:`parseconfig` and
+    :ivar plugins:
+       A list of plugins to use with :py:meth:`parseconfig` and
        :py:meth:`runpytest`.  Initially this is an empty list but plugins can
        be added to the list.  The type of items to add to the list depends on
        the method using them so refer to them for details.
-
     """
 
     __test__ = False
@@ -618,7 +669,6 @@ class Testdir:
         Some methods modify the global interpreter state and this tries to
         clean this up.  It does not remove the temporary directory however so
         it can be looked at after the test run has finished.
-
         """
         self._sys_modules_snapshot.restore()
         self._sys_path_snapshot.restore()
@@ -626,9 +676,9 @@ class Testdir:
         self.monkeypatch.undo()
 
     def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
-        # some zope modules used by twisted-related tests keep internal state
+        # Some zope modules used by twisted-related tests keep internal state
         # and can't be deleted; we had some trouble in the past with
-        # `zope.interface` for example
+        # `zope.interface` for example.
         def preserve_module(name):
             return name.startswith("zope")
 
@@ -644,7 +694,6 @@ class Testdir:
         """Cd into the temporary directory.
 
         This is done automatically upon instantiation.
-
         """
         self.tmpdir.chdir()
 
@@ -673,12 +722,15 @@ class Testdir:
     def makefile(self, ext: str, *args: str, **kwargs):
         r"""Create new file(s) in the testdir.
 
-        :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`.
-        :param list[str] args: All args will be treated as strings and joined using newlines.
-           The result will be written as contents to the file.  The name of the
-           file will be based on the test function requesting this fixture.
-        :param kwargs: Each keyword is the name of a file, while the value of it will
-           be written as contents of the file.
+        :param str ext:
+            The extension the file(s) should use, including the dot, e.g. `.py`.
+        :param args:
+            All args are treated as strings and joined using newlines.
+            The result is written as contents to the file.  The name of the
+            file is based on the test function requesting this fixture.
+        :param kwargs:
+            Each keyword is the name of a file, while the value of it will
+            be written as contents of the file.
 
         Examples:
 
@@ -713,6 +765,7 @@ class Testdir:
 
     def makepyfile(self, *args, **kwargs):
         r"""Shortcut for .makefile() with a .py extension.
+
         Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
         existing files.
 
@@ -721,17 +774,18 @@ class Testdir:
         .. code-block:: python
 
             def test_something(testdir):
-                # initial file is created test_something.py
+                # Initial file is created test_something.py.
                 testdir.makepyfile("foobar")
-                # to create multiple files, pass kwargs accordingly
+                # To create multiple files, pass kwargs accordingly.
                 testdir.makepyfile(custom="foobar")
-                # at this point, both 'test_something.py' & 'custom.py' exist in the test directory
+                # At this point, both 'test_something.py' & 'custom.py' exist in the test directory.
 
         """
         return self._makefile(".py", args, kwargs)
 
     def maketxtfile(self, *args, **kwargs):
         r"""Shortcut for .makefile() with a .txt extension.
+
         Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
         existing files.
 
@@ -740,11 +794,11 @@ class Testdir:
         .. code-block:: python
 
             def test_something(testdir):
-                # initial file is created test_something.txt
+                # Initial file is created test_something.txt.
                 testdir.maketxtfile("foobar")
-                # to create multiple files, pass kwargs accordingly
+                # To create multiple files, pass kwargs accordingly.
                 testdir.maketxtfile(custom="foobar")
-                # at this point, both 'test_something.txt' & 'custom.txt' exist in the test directory
+                # At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory.
 
         """
         return self._makefile(".txt", args, kwargs)
@@ -765,11 +819,10 @@ class Testdir:
         return self.tmpdir.mkdir(name)
 
     def mkpydir(self, name) -> py.path.local:
-        """Create a new python package.
+        """Create a new Python package.
 
         This creates a (sub)directory with an empty ``__init__.py`` file so it
-        gets recognised as a python package.
-
+        gets recognised as a Python package.
         """
         p = self.mkdir(name)
         p.ensure("__init__.py")
@@ -779,8 +832,7 @@ class Testdir:
         """Copy file from project's directory into the testdir.
 
         :param str name: The name of the file to copy.
-        :return: path to the copied directory (inside ``self.tmpdir``).
-
+        :returns: Path to the copied directory (inside ``self.tmpdir``).
         """
         import warnings
         from _pytest.warning_types import PYTESTER_COPY_EXAMPLE
@@ -830,12 +882,11 @@ class Testdir:
     def getnode(self, config: Config, arg):
         """Return the collection node of a file.
 
-        :param config: :py:class:`_pytest.config.Config` instance, see
-           :py:meth:`parseconfig` and :py:meth:`parseconfigure` to create the
-           configuration
-
-        :param arg: a :py:class:`py.path.local` instance of the file
-
+        :param _pytest.config.Config config:
+           A pytest config.
+           See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it.
+        :param py.path.local arg:
+            Path to the file.
         """
         session = Session.from_config(config)
         assert "::" not in str(arg)
@@ -851,8 +902,7 @@ class Testdir:
         This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
         create the (configured) pytest Config instance.
 
-        :param path: a :py:class:`py.path.local` instance of the file
-
+        :param py.path.local path: Path to the file.
         """
         config = self.parseconfigure(path)
         session = Session.from_config(config)
@@ -867,7 +917,6 @@ class Testdir:
 
         This recurses into the collection node and returns a list of all the
         test items contained within.
-
         """
         session = colitems[0].session
         result = []  # type: List[Item]
@@ -882,7 +931,6 @@ class Testdir:
         provide a ``.getrunner()`` method which should return a runner which
         can run the test protocol for a single item, e.g.
         :py:func:`_pytest.runner.runtestprotocol`.
-
         """
         # used from runner functional tests
         item = self.getitem(source)
@@ -891,37 +939,37 @@ class Testdir:
         runner = testclassinstance.getrunner()
         return runner(item)
 
-    def inline_runsource(self, source, *cmdlineargs):
+    def inline_runsource(self, source, *cmdlineargs) -> HookRecorder:
         """Run a test module in process using ``pytest.main()``.
 
         This run writes "source" into a temporary file and runs
         ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
         for the result.
 
-        :param source: the source code of the test module
+        :param source: The source code of the test module.
 
-        :param cmdlineargs: any extra command line arguments to use
-
-        :return: :py:class:`HookRecorder` instance of the result
+        :param cmdlineargs: Any extra command line arguments to use.
 
+        :returns: :py:class:`HookRecorder` instance of the result.
         """
         p = self.makepyfile(source)
         values = list(cmdlineargs) + [p]
         return self.inline_run(*values)
 
-    def inline_genitems(self, *args):
+    def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
         """Run ``pytest.main(['--collectonly'])`` in-process.
 
         Runs the :py:func:`pytest.main` function to run all of pytest inside
         the test process itself like :py:meth:`inline_run`, but returns a
         tuple of the collected items and a :py:class:`HookRecorder` instance.
-
         """
         rec = self.inline_run("--collect-only", *args)
         items = [x.item for x in rec.getcalls("pytest_itemcollected")]
         return items, rec
 
-    def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False):
+    def inline_run(
+        self, *args, plugins=(), no_reraise_ctrlc: bool = False
+    ) -> HookRecorder:
         """Run ``pytest.main()`` in-process, returning a HookRecorder.
 
         Runs the :py:func:`pytest.main` function to run all of pytest inside
@@ -930,14 +978,15 @@ class Testdir:
         from that run than can be done by matching stdout/stderr from
         :py:meth:`runpytest`.
 
-        :param args: command line arguments to pass to :py:func:`pytest.main`
-
-        :kwarg plugins: extra plugin instances the ``pytest.main()`` instance should use.
-
-        :kwarg no_reraise_ctrlc: typically we reraise keyboard interrupts from the child run. If
+        :param args:
+            Command line arguments to pass to :py:func:`pytest.main`.
+        :param plugins:
+            Extra plugin instances the ``pytest.main()`` instance should use.
+        :param no_reraise_ctrlc:
+            Typically we reraise keyboard interrupts from the child run. If
             True, the KeyboardInterrupt exception is captured.
 
-        :return: a :py:class:`HookRecorder` instance
+        :returns: A :py:class:`HookRecorder` instance.
         """
         # (maybe a cpython bug?) the importlib cache sometimes isn't updated
         # properly between file creation and inline_run (especially if imports
@@ -975,10 +1024,10 @@ class Testdir:
                 class reprec:  # type: ignore
                     pass
 
-            reprec.ret = ret  # type: ignore[attr-defined]
+            reprec.ret = ret
 
-            # typically we reraise keyboard interrupts from the child run
-            # because it's our user requesting interruption of the testing
+            # Typically we reraise keyboard interrupts from the child run
+            # because it's our user requesting interruption of the testing.
             if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc:
                 calls = reprec.getcalls("pytest_keyboard_interrupt")
                 if calls and calls[-1].excinfo.type == KeyboardInterrupt:
@@ -990,8 +1039,7 @@ class Testdir:
 
     def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
         """Return result of running pytest in-process, providing a similar
-        interface to what self.runpytest() provides.
-        """
+        interface to what self.runpytest() provides."""
         syspathinsert = kwargs.pop("syspathinsert", False)
 
         if syspathinsert:
@@ -1024,6 +1072,7 @@ class Testdir:
             sys.stdout.write(out)
             sys.stderr.write(err)
 
+        assert reprec.ret is not None
         res = RunResult(
             reprec.ret, out.splitlines(), err.splitlines(), timing.time() - now
         )
@@ -1032,9 +1081,7 @@ class Testdir:
 
     def runpytest(self, *args, **kwargs) -> RunResult:
         """Run pytest inline or in a subprocess, depending on the command line
-        option "--runpytest" and return a :py:class:`RunResult`.
-
-        """
+        option "--runpytest" and return a :py:class:`RunResult`."""
         args = self._ensure_basetemp(args)
         if self._method == "inprocess":
             return self.runpytest_inprocess(*args, **kwargs)
@@ -1061,7 +1108,6 @@ class Testdir:
 
         If :py:attr:`plugins` has been populated they should be plugin modules
         to be registered with the PluginManager.
-
         """
         args = self._ensure_basetemp(args)
 
@@ -1077,7 +1123,7 @@ class Testdir:
     def parseconfigure(self, *args) -> Config:
         """Return a new pytest configured Config instance.
 
-        This returns a new :py:class:`_pytest.config.Config` instance like
+        Returns a new :py:class:`_pytest.config.Config` instance like
         :py:meth:`parseconfig`, but also calls the pytest_configure hook.
         """
         config = self.parseconfig(*args)
@@ -1087,15 +1133,14 @@ class Testdir:
     def getitem(self, source, funcname: str = "test_func") -> Item:
         """Return the test item for a test function.
 
-        This writes the source to a python file and runs pytest's collection on
+        Writes the source to a python file and runs pytest's collection on
         the resulting module, returning the test item for the requested
         function name.
 
-        :param source: the module source
-
-        :param funcname: the name of the test function for which to return a
-            test item
-
+        :param source:
+            The module source.
+        :param funcname:
+            The name of the test function for which to return a test item.
         """
         items = self.getitems(source)
         for item in items:
@@ -1108,9 +1153,8 @@ class Testdir:
     def getitems(self, source) -> List[Item]:
         """Return all test items collected from the module.
 
-        This writes the source to a python file and runs pytest's collection on
+        Writes the source to a Python file and runs pytest's collection on
         the resulting module, returning all test items contained within.
-
         """
         modcol = self.getmodulecol(source)
         return self.genitems([modcol])
@@ -1118,18 +1162,19 @@ class Testdir:
     def getmodulecol(self, source, configargs=(), withinit: bool = False):
         """Return the module collection node for ``source``.
 
-        This writes ``source`` to a file using :py:meth:`makepyfile` and then
+        Writes ``source`` to a file using :py:meth:`makepyfile` and then
         runs the pytest collection on it, returning the collection node for the
         test module.
 
-        :param source: the source code of the module to collect
-
-        :param configargs: any extra arguments to pass to
-            :py:meth:`parseconfigure`
+        :param source:
+            The source code of the module to collect.
 
-        :param withinit: whether to also write an ``__init__.py`` file to the
-            same directory to ensure it is a package
+        :param configargs:
+            Any extra arguments to pass to :py:meth:`parseconfigure`.
 
+        :param withinit:
+            Whether to also write an ``__init__.py`` file to the same
+            directory to ensure it is a package.
         """
         if isinstance(source, Path):
             path = self.tmpdir.join(str(source))
@@ -1147,12 +1192,11 @@ class Testdir:
     ) -> Optional[Union[Item, Collector]]:
         """Return the collection node for name from the module collection.
 
-        This will search a module collection node for a collection node
-        matching the given name.
+        Searchs a module collection node for a collection node matching the
+        given name.
 
-        :param modcol: a module collection node; see :py:meth:`getmodulecol`
-
-        :param name: the name of the node to return
+        :param modcol: A module collection node; see :py:meth:`getmodulecol`.
+        :param name: The name of the node to return.
         """
         if modcol not in self._mod_collections:
             self._mod_collections[modcol] = list(modcol.collect())
@@ -1171,11 +1215,10 @@ class Testdir:
     ):
         """Invoke subprocess.Popen.
 
-        This calls subprocess.Popen making sure the current working directory
-        is in the PYTHONPATH.
+        Calls subprocess.Popen making sure the current working directory is
+        in the PYTHONPATH.
 
         You probably want to use :py:meth:`run` instead.
-
         """
         env = os.environ.copy()
         env["PYTHONPATH"] = os.pathsep.join(
@@ -1207,16 +1250,18 @@ class Testdir:
 
         Run a process using subprocess.Popen saving the stdout and stderr.
 
-        :param args: the sequence of arguments to pass to `subprocess.Popen()`
-        :kwarg timeout: the period in seconds after which to timeout and raise
-            :py:class:`Testdir.TimeoutExpired`
-        :kwarg stdin: optional standard input.  Bytes are being send, closing
+        :param args:
+            The sequence of arguments to pass to `subprocess.Popen()`.
+        :param timeout:
+            The period in seconds after which to timeout and raise
+            :py:class:`Testdir.TimeoutExpired`.
+        :param stdin:
+            Optional standard input.  Bytes are being send, closing
             the pipe, otherwise it is passed through to ``popen``.
             Defaults to ``CLOSE_STDIN``, which translates to using a pipe
             (``subprocess.PIPE``) that gets closed.
 
-        Returns a :py:class:`RunResult`.
-
+        :rtype: RunResult
         """
         __tracebackhide__ = True
 
@@ -1292,13 +1337,15 @@ class Testdir:
     def runpython(self, script) -> RunResult:
         """Run a python script using sys.executable as interpreter.
 
-        Returns a :py:class:`RunResult`.
-
+        :rtype: RunResult
         """
         return self.run(sys.executable, script)
 
     def runpython_c(self, command):
-        """Run python -c "command", return a :py:class:`RunResult`."""
+        """Run python -c "command".
+
+        :rtype: RunResult
+        """
         return self.run(sys.executable, "-c", command)
 
     def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunResult:
@@ -1310,11 +1357,13 @@ class Testdir:
         with "runpytest-" to not conflict with the normal numbered pytest
         location for temporary files and directories.
 
-        :param args: the sequence of arguments to pass to the pytest subprocess
-        :param timeout: the period in seconds after which to timeout and raise
-            :py:class:`Testdir.TimeoutExpired`
+        :param args:
+            The sequence of arguments to pass to the pytest subprocess.
+        :param timeout:
+            The period in seconds after which to timeout and raise
+            :py:class:`Testdir.TimeoutExpired`.
 
-        Returns a :py:class:`RunResult`.
+        :rtype: RunResult
         """
         __tracebackhide__ = True
         p = make_numbered_dir(root=Path(str(self.tmpdir)), prefix="runpytest-")
@@ -1334,7 +1383,6 @@ class Testdir:
         directory locations.
 
         The pexpect child is returned.
-
         """
         basetemp = self.tmpdir.mkdir("temp-pexpect")
         invoke = " ".join(map(str, self._getpytestargs()))
@@ -1345,7 +1393,6 @@ class Testdir:
         """Run a command using pexpect.
 
         The pexpect child is returned.
-
         """
         pexpect = pytest.importorskip("pexpect", "3.0")
         if hasattr(sys, "pypy_version_info") and "64" in platform.machine():
@@ -1400,14 +1447,12 @@ class LineMatcher:
         return lines2
 
     def fnmatch_lines_random(self, lines2: Sequence[str]) -> None:
-        """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`).
-        """
+        """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`)."""
         __tracebackhide__ = True
         self._match_lines_random(lines2, fnmatch)
 
     def re_match_lines_random(self, lines2: Sequence[str]) -> None:
-        """Check lines exist in the output in any order (using :func:`python:re.match`).
-        """
+        """Check lines exist in the output in any order (using :func:`python:re.match`)."""
         __tracebackhide__ = True
         self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name)))
 
@@ -1452,8 +1497,8 @@ class LineMatcher:
         wildcards.  If they do not match a pytest.fail() is called.  The
         matches and non-matches are also shown as part of the error message.
 
-        :param lines2: string patterns to match.
-        :param consecutive: match lines consecutive?
+        :param lines2: String patterns to match.
+        :param consecutive: Match lines consecutively?
         """
         __tracebackhide__ = True
         self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
@@ -1489,14 +1534,18 @@ class LineMatcher:
     ) -> None:
         """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
 
-        :param list[str] lines2: list of string patterns to match. The actual
-            format depends on ``match_func``
-        :param match_func: a callable ``match_func(line, pattern)`` where line
-            is the captured line from stdout/stderr and pattern is the matching
-            pattern
-        :param str match_nickname: the nickname for the match function that
-            will be logged to stdout when a match occurs
-        :param consecutive: match lines consecutively?
+        :param Sequence[str] lines2:
+            List of string patterns to match. The actual format depends on
+            ``match_func``.
+        :param match_func:
+            A callable ``match_func(line, pattern)`` where line is the
+            captured line from stdout/stderr and pattern is the matching
+            pattern.
+        :param str match_nickname:
+            The nickname for the match function that will be logged to stdout
+            when a match occurs.
+        :param consecutive:
+            Match lines consecutively?
         """
         if not isinstance(lines2, collections.abc.Sequence):
             raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__))
@@ -1546,7 +1595,7 @@ class LineMatcher:
     def no_fnmatch_line(self, pat: str) -> None:
         """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
 
-        :param str pat: the pattern to match lines.
+        :param str pat: The pattern to match lines.
         """
         __tracebackhide__ = True
         self._no_match_line(pat, fnmatch, "fnmatch")
@@ -1554,7 +1603,7 @@ class LineMatcher:
     def no_re_match_line(self, pat: str) -> None:
         """Ensure captured lines do not match the given pattern, using ``re.match``.
 
-        :param str pat: the regular expression to match lines.
+        :param str pat: The regular expression to match lines.
         """
         __tracebackhide__ = True
         self._no_match_line(
@@ -1564,9 +1613,9 @@ class LineMatcher:
     def _no_match_line(
         self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str
     ) -> None:
-        """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``
+        """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``.
 
-        :param str pat: the pattern to match lines
+        :param str pat: The pattern to match lines.
         """
         __tracebackhide__ = True
         nomatch_printed = False
index 28c2dc849c74bb566612576bacc50cf98e3a7b5c..7d3e301c076c43395b8c50190da6e4330a395736 100644 (file)
@@ -1,10 +1,11 @@
-""" Python test discovery, setup and run of test functions. """
+"""Python test discovery, setup and run of test functions."""
 import enum
 import fnmatch
 import inspect
 import itertools
 import os
 import sys
+import types
 import typing
 import warnings
 from collections import Counter
@@ -32,9 +33,11 @@ from _pytest import nodes
 from _pytest._code import filter_traceback
 from _pytest._code import getfslineno
 from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import TerminalRepr
 from _pytest._io import TerminalWriter
 from _pytest._io.saferepr import saferepr
 from _pytest.compat import ascii_escaped
+from _pytest.compat import final
 from _pytest.compat import get_default_arg_names
 from _pytest.compat import get_real_func
 from _pytest.compat import getimfunc
@@ -51,7 +54,7 @@ from _pytest.config import Config
 from _pytest.config import ExitCode
 from _pytest.config import hookimpl
 from _pytest.config.argparsing import Parser
-from _pytest.deprecated import FUNCARGNAMES
+from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
 from _pytest.fixtures import FuncFixtureInfo
 from _pytest.main import Session
 from _pytest.mark import MARK_GEN
@@ -65,7 +68,7 @@ from _pytest.outcomes import skip
 from _pytest.pathlib import import_path
 from _pytest.pathlib import ImportPathMismatchError
 from _pytest.pathlib import parts
-from _pytest.reports import TerminalRepr
+from _pytest.pathlib import visit
 from _pytest.warning_types import PytestCollectionWarning
 from _pytest.warning_types import PytestUnhandledCoroutineWarning
 
@@ -162,9 +165,10 @@ def async_warn_and_skip(nodeid: str) -> None:
     msg += (
         "You need to install a suitable plugin for your async framework, for example:\n"
     )
+    msg += "  - anyio\n"
     msg += "  - pytest-asyncio\n"
-    msg += "  - pytest-trio\n"
     msg += "  - pytest-tornasync\n"
+    msg += "  - pytest-trio\n"
     msg += "  - pytest-twisted"
     warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid)))
     skip(msg="async def function and no async plugin installed (see warnings)")
@@ -183,7 +187,9 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
     return True
 
 
-def pytest_collect_file(path: py.path.local, parent) -> Optional["Module"]:
+def pytest_collect_file(
+    path: py.path.local, parent: nodes.Collector
+) -> Optional["Module"]:
     ext = path.ext
     if ext == ".py":
         if not parent.session.isinitpath(path):
@@ -200,7 +206,7 @@ def pytest_collect_file(path: py.path.local, parent) -> Optional["Module"]:
 
 
 def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool:
-    """Returns True if path matches any of the patterns in the list of globs given."""
+    """Return whether path matches any of the patterns in the list of globs given."""
     return any(path.fnmatch(pattern) for pattern in patterns)
 
 
@@ -214,16 +220,16 @@ def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module":
 
 @hookimpl(trylast=True)
 def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object):
-    # nothing was collected elsewhere, let's do it here
+    # Nothing was collected elsewhere, let's do it here.
     if safe_isclass(obj):
         if collector.istestclass(obj, name):
             return Class.from_parent(collector, name=name, obj=obj)
     elif collector.istestfunction(obj, name):
-        # mock seems to store unbound methods (issue473), normalize it
+        # mock seems to store unbound methods (issue473), normalize it.
         obj = getattr(obj, "__func__", obj)
         # We need to try and unwrap the function if it's a functools.partial
         # or a functools.wrapped.
-        # We mustn't if it's been wrapped with mock.patch (python 2 only)
+        # We mustn't if it's been wrapped with mock.patch (python 2 only).
         if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))):
             filename, lineno = getfslineno(obj)
             warnings.warn_explicit(
@@ -297,14 +303,14 @@ class PyobjMixin:
         self._obj = value
 
     def _getobj(self):
-        """Gets the underlying Python object. May be overwritten by subclasses."""
+        """Get the underlying Python object. May be overwritten by subclasses."""
         # TODO: Improve the type of `parent` such that assert/ignore aren't needed.
         assert self.parent is not None
         obj = self.parent.obj  # type: ignore[attr-defined]
         return getattr(obj, self.name)
 
     def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
-        """ return python path relative to the containing module. """
+        """Return Python path relative to the containing module."""
         chain = self.listchain()
         chain.reverse()
         parts = []
@@ -340,13 +346,33 @@ class PyobjMixin:
         return fspath, lineno, modpath
 
 
+# As an optimization, these builtin attribute names are pre-ignored when
+# iterating over an object during collection -- the pytest_pycollect_makeitem
+# hook is not called for them.
+# fmt: off
+class _EmptyClass: pass  # noqa: E701
+IGNORED_ATTRIBUTES = frozenset.union(  # noqa: E305
+    frozenset(),
+    # Module.
+    dir(types.ModuleType("empty_module")),
+    # Some extra module attributes the above doesn't catch.
+    {"__builtins__", "__file__", "__cached__"},
+    # Class.
+    dir(_EmptyClass),
+    # Instance.
+    dir(_EmptyClass()),
+)
+del _EmptyClass
+# fmt: on
+
+
 class PyCollector(PyobjMixin, nodes.Collector):
     def funcnamefilter(self, name: str) -> bool:
         return self._matches_prefix_or_glob_option("python_functions", name)
 
     def isnosetest(self, obj: object) -> bool:
-        """ Look for the __test__ attribute, which is applied by the
-        @nose.tools.istest decorator
+        """Look for the __test__ attribute, which is applied by the
+        @nose.tools.istest decorator.
         """
         # We explicitly check for "is True" here to not mistakenly treat
         # classes with a custom __getattr__ returning something truthy (like a
@@ -359,7 +385,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
     def istestfunction(self, obj: object, name: str) -> bool:
         if self.funcnamefilter(name) or self.isnosetest(obj):
             if isinstance(obj, staticmethod):
-                # static methods need to be unwrapped
+                # staticmethods need to be unwrapped.
                 obj = safe_getattr(obj, "__func__", False)
             return (
                 safe_getattr(obj, "__call__", False)
@@ -372,16 +398,14 @@ class PyCollector(PyobjMixin, nodes.Collector):
         return self.classnamefilter(name) or self.isnosetest(obj)
 
     def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
-        """
-        checks if the given name matches the prefix or glob-pattern defined
-        in ini configuration.
-        """
+        """Check if the given name matches the prefix or glob-pattern defined
+        in ini configuration."""
         for option in self.config.getini(option_name):
             if name.startswith(option):
                 return True
-            # check that name looks like a glob-string before calling fnmatch
+            # Check that name looks like a glob-string before calling fnmatch
             # because this is called for every name in each collected module,
-            # and fnmatch is somewhat expensive to call
+            # and fnmatch is somewhat expensive to call.
             elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch(
                 name, option
             ):
@@ -399,19 +423,25 @@ class PyCollector(PyobjMixin, nodes.Collector):
             dicts.append(basecls.__dict__)
         seen = set()  # type: Set[str]
         values = []  # type: List[Union[nodes.Item, nodes.Collector]]
+        ihook = self.ihook
         for dic in dicts:
             # Note: seems like the dict can change during iteration -
             # be careful not to remove the list() without consideration.
             for name, obj in list(dic.items()):
+                if name in IGNORED_ATTRIBUTES:
+                    continue
                 if name in seen:
                     continue
                 seen.add(name)
-                res = self._makeitem(name, obj)
+                res = ihook.pytest_pycollect_makeitem(
+                    collector=self, name=name, obj=obj
+                )
                 if res is None:
                     continue
-                if not isinstance(res, list):
-                    res = [res]
-                values.extend(res)
+                elif isinstance(res, list):
+                    values.extend(res)
+                else:
+                    values.append(res)
 
         def sort_key(item):
             fspath, lineno, _ = item.reportinfo()
@@ -420,17 +450,6 @@ class PyCollector(PyobjMixin, nodes.Collector):
         values.sort(key=sort_key)
         return values
 
-    def _makeitem(
-        self, name: str, obj: object
-    ) -> Union[
-        None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]
-    ]:
-        # assert self.ihook.fspath == self.fspath, self
-        item = self.ihook.pytest_pycollect_makeitem(
-            collector=self, name=name, obj=obj
-        )  # type: Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]
-        return item
-
     def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
         modulecol = self.getparent(Module)
         assert modulecol is not None
@@ -456,10 +475,10 @@ class PyCollector(PyobjMixin, nodes.Collector):
         if not metafunc._calls:
             yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
         else:
-            # add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
+            # Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
             fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
 
-            # add_funcarg_pseudo_fixture_def may have shadowed some fixtures
+            # Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
             # with direct parametrization, so make sure we update what the
             # function really needs.
             fixtureinfo.prune_dependency_tree()
@@ -478,7 +497,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
 
 
 class Module(nodes.File, PyCollector):
-    """ Collector for test classes and functions. """
+    """Collector for test classes and functions."""
 
     def _getobj(self):
         return self._importtestmodule()
@@ -490,7 +509,7 @@ class Module(nodes.File, PyCollector):
         return super().collect()
 
     def _inject_setup_module_fixture(self) -> None:
-        """Injects a hidden autouse, module scoped fixture into the collected module object
+        """Inject a hidden autouse, module scoped fixture into the collected module object
         that invokes setUpModule/tearDownModule if either or both are available.
 
         Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@@ -517,7 +536,7 @@ class Module(nodes.File, PyCollector):
         self.obj.__pytest_setup_module = xunit_setup_module_fixture
 
     def _inject_setup_function_fixture(self) -> None:
-        """Injects a hidden autouse, function scoped fixture into the collected module object
+        """Inject a hidden autouse, function scoped fixture into the collected module object
         that invokes setup_function/teardown_function if either or both are available.
 
         Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@@ -546,7 +565,7 @@ class Module(nodes.File, PyCollector):
         self.obj.__pytest_setup_function = xunit_setup_function_fixture
 
     def _importtestmodule(self):
-        # we assume we are only called once per module
+        # We assume we are only called once per module.
         importmode = self.config.getoption("--import-mode")
         try:
             mod = import_path(self.fspath, mode=importmode)
@@ -580,7 +599,7 @@ class Module(nodes.File, PyCollector):
                 "Traceback:\n"
                 "{traceback}".format(fspath=self.fspath, traceback=formatted_tb)
             ) from e
-        except _pytest.runner.Skipped as e:
+        except skip.Exception as e:
             if e.allow_module_level:
                 raise
             raise self.CollectError(
@@ -603,7 +622,7 @@ class Package(Module):
         session=None,
         nodeid=None,
     ) -> None:
-        # NOTE: could be just the following, but kept as-is for compat.
+        # NOTE: Could be just the following, but kept as-is for compat.
         # nodes.FSCollector.__init__(self, fspath, parent=parent)
         session = parent.session
         nodes.FSCollector.__init__(
@@ -612,8 +631,8 @@ class Package(Module):
         self.name = os.path.basename(str(fspath.dirname))
 
     def setup(self) -> None:
-        # not using fixtures to call setup_module here because autouse fixtures
-        # from packages are not called automatically (#4085)
+        # Not using fixtures to call setup_module here because autouse fixtures
+        # from packages are not called automatically (#4085).
         setup_module = _get_first_non_fixture_func(
             self.obj, ("setUpModule", "setup_module")
         )
@@ -628,10 +647,48 @@ class Package(Module):
             self.addfinalizer(func)
 
     def gethookproxy(self, fspath: py.path.local):
-        return super()._gethookproxy(fspath)
+        warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
+        return self.session.gethookproxy(fspath)
 
     def isinitpath(self, path: py.path.local) -> bool:
-        return path in self.session._initialpaths
+        warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
+        return self.session.isinitpath(path)
+
+    def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
+        if direntry.name == "__pycache__":
+            return False
+        path = py.path.local(direntry.path)
+        ihook = self.session.gethookproxy(path.dirpath())
+        if ihook.pytest_ignore_collect(path=path, config=self.config):
+            return False
+        norecursepatterns = self.config.getini("norecursedirs")
+        if any(path.check(fnmatch=pat) for pat in norecursepatterns):
+            return False
+        return True
+
+    def _collectfile(
+        self, path: py.path.local, handle_dupes: bool = True
+    ) -> typing.Sequence[nodes.Collector]:
+        assert (
+            path.isfile()
+        ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
+            path, path.isdir(), path.exists(), path.islink()
+        )
+        ihook = self.session.gethookproxy(path)
+        if not self.session.isinitpath(path):
+            if ihook.pytest_ignore_collect(path=path, config=self.config):
+                return ()
+
+        if handle_dupes:
+            keepduplicates = self.config.getoption("keepduplicates")
+            if not keepduplicates:
+                duplicate_paths = self.config.pluginmanager._duplicatepaths
+                if path in duplicate_paths:
+                    return ()
+                else:
+                    duplicate_paths.add(path)
+
+        return ihook.pytest_collect_file(path=path, parent=self)  # type: ignore[no-any-return]
 
     def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
         this_path = self.fspath.dirpath()
@@ -641,23 +698,24 @@ class Package(Module):
         ):
             yield Module.from_parent(self, fspath=init_module)
         pkg_prefixes = set()  # type: Set[py.path.local]
-        for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
+        for direntry in visit(str(this_path), recurse=self._recurse):
+            path = py.path.local(direntry.path)
+
             # We will visit our own __init__.py file, in which case we skip it.
-            is_file = path.isfile()
-            if is_file:
-                if path.basename == "__init__.py" and path.dirpath() == this_path:
+            if direntry.is_file():
+                if direntry.name == "__init__.py" and path.dirpath() == this_path:
                     continue
 
-            parts_ = parts(path.strpath)
+            parts_ = parts(direntry.path)
             if any(
                 str(pkg_prefix) in parts_ and pkg_prefix.join("__init__.py") != path
                 for pkg_prefix in pkg_prefixes
             ):
                 continue
 
-            if is_file:
+            if direntry.is_file():
                 yield from self._collectfile(path)
-            elif not path.isdir():
+            elif not direntry.is_dir():
                 # Broken symlink or invalid/missing file.
                 continue
             elif path.join("__init__.py").check(file=1):
@@ -666,7 +724,7 @@ class Package(Module):
 
 def _call_with_optional_argument(func, arg) -> None:
     """Call the given function with the given argument if func accepts one argument, otherwise
-    calls func without arguments"""
+    calls func without arguments."""
     arg_count = func.__code__.co_argcount
     if inspect.ismethod(func):
         arg_count -= 1
@@ -678,9 +736,7 @@ def _call_with_optional_argument(func, arg) -> None:
 
 def _get_first_non_fixture_func(obj: object, names: Iterable[str]):
     """Return the attribute from the given object to be used as a setup/teardown
-    xunit-style function, but only if not marked as a fixture to
-    avoid calling it twice.
-    """
+    xunit-style function, but only if not marked as a fixture to avoid calling it twice."""
     for name in names:
         meth = getattr(obj, name, None)
         if meth is not None and fixtures.getfixturemarker(meth) is None:
@@ -688,13 +744,11 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]):
 
 
 class Class(PyCollector):
-    """ Collector for test methods. """
+    """Collector for test methods."""
 
     @classmethod
     def from_parent(cls, parent, *, name, obj=None):
-        """
-        The public constructor
-        """
+        """The public constructor."""
         return super().from_parent(name=name, parent=parent)
 
     def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
@@ -727,7 +781,7 @@ class Class(PyCollector):
         return [Instance.from_parent(self, name="()")]
 
     def _inject_setup_class_fixture(self) -> None:
-        """Injects a hidden autouse, class scoped fixture into the collected class object
+        """Inject a hidden autouse, class scoped fixture into the collected class object
         that invokes setup_class/teardown_class if either or both are available.
 
         Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@@ -751,7 +805,7 @@ class Class(PyCollector):
         self.obj.__pytest_setup_class = xunit_setup_class_fixture
 
     def _inject_setup_method_fixture(self) -> None:
-        """Injects a hidden autouse, function scoped fixture into the collected class object
+        """Inject a hidden autouse, function scoped fixture into the collected class object
         that invokes setup_method/teardown_method if either or both are available.
 
         Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@@ -778,9 +832,9 @@ class Class(PyCollector):
 
 class Instance(PyCollector):
     _ALLOW_MARKERS = False  # hack, destroy later
-    # instances share the object with their parents in a way
+    # Instances share the object with their parents in a way
     # that duplicates markers instances if not taken out
-    # can be removed at node structure reorganization time
+    # can be removed at node structure reorganization time.
 
     def _getobj(self):
         # TODO: Improve the type of `parent` such that assert/ignore aren't needed.
@@ -811,6 +865,7 @@ def hasnew(obj: object) -> bool:
     return False
 
 
+@final
 class CallSpec2:
     def __init__(self, metafunc: "Metafunc") -> None:
         self.metafunc = metafunc
@@ -871,9 +926,10 @@ class CallSpec2:
         self.marks.extend(normalize_mark_list(marks))
 
 
+@final
 class Metafunc:
-    """
-    Metafunc objects are passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
+    """Objects passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
+
     They help to inspect a test function and to generate tests according to
     test configuration or values specified in the class or module where a
     test function is defined.
@@ -889,30 +945,24 @@ class Metafunc:
     ) -> None:
         self.definition = definition
 
-        #: access to the :class:`_pytest.config.Config` object for the test session
+        #: Access to the :class:`_pytest.config.Config` object for the test session.
         self.config = config
 
-        #: the module object where the test function is defined in.
+        #: The module object where the test function is defined in.
         self.module = module
 
-        #: underlying python test function
+        #: Underlying Python test function.
         self.function = definition.obj
 
-        #: set of fixture names required by the test function
+        #: Set of fixture names required by the test function.
         self.fixturenames = fixtureinfo.names_closure
 
-        #: class object where the test function is defined in or ``None``.
+        #: Class object where the test function is defined in or ``None``.
         self.cls = cls
 
         self._calls = []  # type: List[CallSpec2]
         self._arg2fixturedefs = fixtureinfo.name2fixturedefs
 
-    @property
-    def funcargnames(self) -> List[str]:
-        """ alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
-        warnings.warn(FUNCARGNAMES, stacklevel=2)
-        return self.fixturenames
-
     def parametrize(
         self,
         argnames: Union[str, List[str], Tuple[str, ...]],
@@ -928,30 +978,35 @@ class Metafunc:
         *,
         _param_mark: Optional[Mark] = None
     ) -> None:
-        """ Add new invocations to the underlying test function using the list
+        """Add new invocations to the underlying test function using the list
         of argvalues for the given argnames.  Parametrization is performed
         during the collection phase.  If you need to setup expensive resources
         see about setting indirect to do it rather at test setup time.
 
-        :arg argnames: a comma-separated string denoting one or more argument
-                       names, or a list/tuple of argument strings.
+        :param argnames:
+            A comma-separated string denoting one or more argument names, or
+            a list/tuple of argument strings.
 
-        :arg argvalues: The list of argvalues determines how often a
-            test is invoked with different argument values.  If only one
-            argname was specified argvalues is a list of values.  If N
-            argnames were specified, argvalues must be a list of N-tuples,
-            where each tuple-element specifies a value for its respective
-            argname.
+        :param argvalues:
+            The list of argvalues determines how often a test is invoked with
+            different argument values.
 
-        :arg indirect: The list of argnames or boolean. A list of arguments'
-            names (subset of argnames). If True the list contains all names from
-            the argnames. Each argvalue corresponding to an argname in this list will
+            If only one argname was specified argvalues is a list of values.
+            If N argnames were specified, argvalues must be a list of
+            N-tuples, where each tuple-element specifies a value for its
+            respective argname.
+
+        :param indirect:
+            A list of arguments' names (subset of argnames) or a boolean.
+            If True the list contains all names from the argnames. Each
+            argvalue corresponding to an argname in this list will
             be passed as request.param to its respective argname fixture
             function so that it can perform more expensive setups during the
             setup phase of a test rather than at collection time.
 
-        :arg ids: sequence of (or generator for) ids for ``argvalues``,
-              or a callable to return part of the id for each argvalue.
+        :param ids:
+            Sequence of (or generator for) ids for ``argvalues``,
+            or a callable to return part of the id for each argvalue.
 
             With sequences (and generators like ``itertools.count()``) the
             returned ids should be of type ``string``, ``int``, ``float``,
@@ -969,7 +1024,8 @@ class Metafunc:
             If no ids are provided they will be generated automatically from
             the argvalues.
 
-        :arg scope: if specified it denotes the scope of the parameters.
+        :param scope:
+            If specified it denotes the scope of the parameters.
             The scope is used for grouping tests by parameter instances.
             It will also override any fixture-function defined scope, allowing
             to set a dynamic scope using test context or configuration.
@@ -1016,9 +1072,9 @@ class Metafunc:
             scope, descr="parametrize() call in {}".format(self.function.__name__)
         )
 
-        # create the new calls: if we are parametrize() multiple times (by applying the decorator
+        # Create the new calls: if we are parametrize() multiple times (by applying the decorator
         # more than once) then we accumulate those calls generating the cartesian product
-        # of all calls
+        # of all calls.
         newcalls = []
         for callspec in self._calls or [CallSpec2(self)]:
             for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
@@ -1047,15 +1103,15 @@ class Metafunc:
         parameters: typing.Sequence[ParameterSet],
         nodeid: str,
     ) -> List[str]:
-        """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given
+        """Resolve the actual ids for the given argnames, based on the ``ids`` parameter given
         to ``parametrize``.
 
-        :param List[str] argnames: list of argument names passed to ``parametrize()``.
-        :param ids: the ids parameter of the parametrized call (see docs).
-        :param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``.
-        :param str str: the nodeid of the item that generated this parametrized call.
+        :param List[str] argnames: List of argument names passed to ``parametrize()``.
+        :param ids: The ids parameter of the parametrized call (see docs).
+        :param List[ParameterSet] parameters: The list of parameter values, same size as ``argnames``.
+        :param str str: The nodeid of the item that generated this parametrized call.
         :rtype: List[str]
-        :return: the list of ids for each argname given
+        :returns: The list of ids for each argname given.
         """
         if ids is None:
             idfn = None
@@ -1095,7 +1151,10 @@ class Metafunc:
             elif isinstance(id_value, (float, int, bool)):
                 new_ids.append(str(id_value))
             else:
-                msg = "In {}: ids must be list of string/float/int/bool, found: {} (type: {!r}) at index {}"
+                msg = (  # type: ignore[unreachable]
+                    "In {}: ids must be list of string/float/int/bool, "
+                    "found: {} (type: {!r}) at index {}"
+                )
                 fail(
                     msg.format(func_name, saferepr(id_value), type(id_value), idx),
                     pytrace=False,
@@ -1107,11 +1166,12 @@ class Metafunc:
         argnames: typing.Sequence[str],
         indirect: Union[bool, typing.Sequence[str]],
     ) -> Dict[str, "Literal['params', 'funcargs']"]:
-        """Resolves if each parametrized argument must be considered a parameter to a fixture or a "funcarg"
-        to the function, based on the ``indirect`` parameter of the parametrized() call.
+        """Resolve if each parametrized argument must be considered a
+        parameter to a fixture or a "funcarg" to the function, based on the
+        ``indirect`` parameter of the parametrized() call.
 
-        :param List[str] argnames: list of argument names passed to ``parametrize()``.
-        :param indirect: same ``indirect`` parameter of ``parametrize()``.
+        :param List[str] argnames: List of argument names passed to ``parametrize()``.
+        :param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
         :rtype: Dict[str, str]
             A dict mapping each arg name to either:
             * "params" if the argname should be the parameter of a fixture of the same name.
@@ -1146,12 +1206,11 @@ class Metafunc:
         argnames: typing.Sequence[str],
         indirect: Union[bool, typing.Sequence[str]],
     ) -> None:
-        """
-        Check if all argnames are being used, by default values, or directly/indirectly.
+        """Check if all argnames are being used, by default values, or directly/indirectly.
 
-        :param List[str] argnames: list of argument names passed to ``parametrize()``.
-        :param indirect: same ``indirect`` parameter of ``parametrize()``.
-        :raise ValueError: if validation fails.
+        :param List[str] argnames: List of argument names passed to ``parametrize()``.
+        :param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
+        :raises ValueError: If validation fails.
         """
         default_arg_names = set(get_default_arg_names(self.function))
         func_name = self.function.__name__
@@ -1177,7 +1236,7 @@ class Metafunc:
 
 def _find_parametrized_scope(
     argnames: typing.Sequence[str],
-    arg2fixturedefs: Mapping[str, typing.Sequence[fixtures.FixtureDef]],
+    arg2fixturedefs: Mapping[str, typing.Sequence[fixtures.FixtureDef[object]]],
     indirect: Union[bool, typing.Sequence[str]],
 ) -> "fixtures._Scope":
     """Find the most appropriate scope for a parametrized call based on its arguments.
@@ -1202,7 +1261,7 @@ def _find_parametrized_scope(
             if name in argnames
         ]
         if used_scopes:
-            # Takes the most narrow scope from used fixtures
+            # Takes the most narrow scope from used fixtures.
             for scope in reversed(fixtures.scopes):
                 if scope in used_scopes:
                     return scope
@@ -1260,7 +1319,7 @@ def _idval(
     elif isinstance(val, enum.Enum):
         return str(val)
     elif isinstance(getattr(val, "__name__", None), str):
-        # name of a class, function, module, etc.
+        # Name of a class, function, module, etc.
         name = getattr(val, "__name__")  # type: str
         return name
     return str(argname) + str(idx)
@@ -1307,13 +1366,13 @@ def idmaker(
     unique_ids = set(resolved_ids)
     if len(unique_ids) != len(resolved_ids):
 
-        # Record the number of occurrences of each test ID
+        # Record the number of occurrences of each test ID.
         test_id_counts = Counter(resolved_ids)
 
-        # Map the test ID to its next suffix
+        # Map the test ID to its next suffix.
         test_id_suffixes = defaultdict(int)  # type: Dict[str, int]
 
-        # Suffix non-unique IDs to make them unique
+        # Suffix non-unique IDs to make them unique.
         for index, test_id in enumerate(resolved_ids):
             if test_id_counts[test_id] > 1:
                 resolved_ids[index] = "{}{}".format(test_id, test_id_suffixes[test_id])
@@ -1337,7 +1396,7 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
     verbose = config.getvalue("verbose")
 
     def get_best_relpath(func):
-        loc = getlocation(func, curdir)
+        loc = getlocation(func, str(curdir))
         return curdir.bestrelpath(py.path.local(loc))
 
     def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
@@ -1366,12 +1425,12 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
         tw.sep("-", "fixtures used by {}".format(item.name))
         # TODO: Fix this type ignore.
         tw.sep("-", "({})".format(get_best_relpath(item.function)))  # type: ignore[attr-defined]
-        # dict key not used in loop but needed for sorting
+        # dict key not used in loop but needed for sorting.
         for _, fixturedefs in sorted(info.name2fixturedefs.items()):
             assert fixturedefs is not None
             if not fixturedefs:
                 continue
-            # last item is expected to be the one used by the test item
+            # Last item is expected to be the one used by the test item.
             write_fixture(fixturedefs[-1])
 
     for session_item in session.items:
@@ -1402,7 +1461,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
         if not fixturedefs:
             continue
         for fixturedef in fixturedefs:
-            loc = getlocation(fixturedef.func, curdir)
+            loc = getlocation(fixturedef.func, str(curdir))
             if (fixturedef.argname, loc) in seen:
                 continue
             seen.add((fixturedef.argname, loc))
@@ -1432,7 +1491,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
         if verbose > 0:
             tw.write(" -- %s" % bestrel, yellow=True)
         tw.write("\n")
-        loc = getlocation(fixturedef.func, curdir)
+        loc = getlocation(fixturedef.func, str(curdir))
         doc = inspect.getdoc(fixturedef.func)
         if doc:
             write_docstring(tw, doc)
@@ -1447,11 +1506,35 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = "    ") -> None:
 
 
 class Function(PyobjMixin, nodes.Item):
-    """ a Function Item is responsible for setting up and executing a
-    Python test function.
+    """An Item responsible for setting up and executing a Python test function.
+
+    param name:
+        The full function name, including any decorations like those
+        added by parametrization (``my_func[my_param]``).
+    param parent:
+        The parent Node.
+    param config:
+        The pytest Config object.
+    param callspec:
+        If given, this is function has been parametrized and the callspec contains
+        meta information about the parametrization.
+    param callobj:
+        If given, the object which will be called when the Function is invoked,
+        otherwise the callobj will be obtained from ``parent`` using ``originalname``.
+    param keywords:
+        Keywords bound to the function object for "-k" matching.
+    param session:
+        The pytest Session object.
+    param fixtureinfo:
+        Fixture information already resolved at this fixture node..
+    param originalname:
+        The attribute name to use for accessing the underlying function object.
+        Defaults to ``name``. Set this if name is different from the original name,
+        for example when it contains decorations like those added by parametrization
+        (``my_func[my_param]``).
     """
 
-    # disable since functions handle it themselves
+    # Disable since functions handle it themselves.
     _ALLOW_MARKERS = False
 
     def __init__(
@@ -1466,24 +1549,6 @@ class Function(PyobjMixin, nodes.Item):
         fixtureinfo: Optional[FuncFixtureInfo] = None,
         originalname: Optional[str] = None,
     ) -> None:
-        """
-        param name: the full function name, including any decorations like those
-            added by parametrization (``my_func[my_param]``).
-        param parent: the parent Node.
-        param config: the pytest Config object
-        param callspec: if given, this is function has been parametrized and the callspec contains
-            meta information about the parametrization.
-        param callobj: if given, the object which will be called when the Function is invoked,
-            otherwise the callobj will be obtained from ``parent`` using ``originalname``
-        param keywords: keywords bound to the function object for "-k" matching.
-        param session: the pytest Session object
-        param fixtureinfo: fixture information already resolved at this fixture node.
-        param originalname:
-            The attribute name to use for accessing the underlying function object.
-            Defaults to ``name``. Set this if name is different from the original name,
-            for example when it contains decorations like those added by parametrization
-            (``my_func[my_param]``).
-        """
         super().__init__(name, parent, config=config, session=session)
 
         if callobj is not NOTSET:
@@ -1497,8 +1562,8 @@ class Function(PyobjMixin, nodes.Item):
         #: .. versionadded:: 3.0
         self.originalname = originalname or name
 
-        # note: when FunctionDefinition is introduced, we should change ``originalname``
-        # to a readonly property that returns FunctionDefinition.name
+        # Note: when FunctionDefinition is introduced, we should change ``originalname``
+        # to a readonly property that returns FunctionDefinition.name.
 
         self.keywords.update(self.obj.__dict__)
         self.own_markers.extend(get_unpacked_marks(self.obj))
@@ -1536,9 +1601,7 @@ class Function(PyobjMixin, nodes.Item):
 
     @classmethod
     def from_parent(cls, parent, **kw):  # todo: determine sound type limitations
-        """
-        The public  constructor
-        """
+        """The public constructor."""
         return super().from_parent(parent=parent, **kw)
 
     def _initrequest(self) -> None:
@@ -1547,7 +1610,7 @@ class Function(PyobjMixin, nodes.Item):
 
     @property
     def function(self):
-        "underlying python 'function' object"
+        """Underlying python 'function' object."""
         return getimfunc(self.obj)
 
     def _getobj(self):
@@ -1556,17 +1619,11 @@ class Function(PyobjMixin, nodes.Item):
 
     @property
     def _pyfuncitem(self):
-        "(compatonly) for code expecting pytest-2.2 style request objects"
+        """(compatonly) for code expecting pytest-2.2 style request objects."""
         return self
 
-    @property
-    def funcargnames(self) -> List[str]:
-        """ alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
-        warnings.warn(FUNCARGNAMES, stacklevel=2)
-        return self.fixturenames
-
     def runtest(self) -> None:
-        """ execute the underlying test function. """
+        """Execute the underlying test function."""
         self.ihook.pytest_pyfunc_call(pyfuncitem=self)
 
     def setup(self) -> None:
@@ -1575,7 +1632,7 @@ class Function(PyobjMixin, nodes.Item):
             self.obj = self._getobj()
         self._request._fillfixtures()
 
-    def _prunetraceback(self, excinfo: ExceptionInfo) -> None:
+    def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
         if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
             code = _pytest._code.Code(get_real_func(self.obj))
             path, firstlineno = code.path, code.firstlineno
@@ -1590,7 +1647,7 @@ class Function(PyobjMixin, nodes.Item):
 
             excinfo.traceback = ntraceback.filter()
             # issue364: mark all but first and last frames to
-            # only show a single-line message for each frame
+            # only show a single-line message for each frame.
             if self.config.getoption("tbstyle", "auto") == "auto":
                 if len(excinfo.traceback) > 2:
                     for entry in excinfo.traceback[1:-1]:
@@ -1607,10 +1664,8 @@ class Function(PyobjMixin, nodes.Item):
 
 
 class FunctionDefinition(Function):
-    """
-    internal hack until we get actual definition nodes instead of the
-    crappy metafunc hack
-    """
+    """Internal hack until we get actual definition nodes instead of the
+    crappy metafunc hack."""
 
     def runtest(self) -> None:
         raise RuntimeError("function definitions are not supposed to be used")
index e30471995e7eda3f5d8df5f8452da6a752bc8937..f5ad04a12c9dc46f3bc91aeb545d35d1ad75dbf1 100644 (file)
@@ -1,11 +1,9 @@
-import inspect
 import math
 import pprint
 from collections.abc import Iterable
 from collections.abc import Mapping
 from collections.abc import Sized
 from decimal import Decimal
-from itertools import filterfalse
 from numbers import Number
 from types import TracebackType
 from typing import Any
@@ -18,9 +16,8 @@ from typing import Tuple
 from typing import TypeVar
 from typing import Union
 
-from more_itertools.more import always_iterable
-
 import _pytest._code
+from _pytest.compat import final
 from _pytest.compat import overload
 from _pytest.compat import STRING_TYPES
 from _pytest.compat import TYPE_CHECKING
@@ -30,9 +27,6 @@ if TYPE_CHECKING:
     from typing import Type
 
 
-BASE_TYPE = (type, STRING_TYPES)
-
-
 def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
     at_str = " at {}".format(at) if at else ""
     return TypeError(
@@ -46,10 +40,8 @@ def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
 
 
 class ApproxBase:
-    """
-    Provide shared utilities for making approximate comparisons between numbers
-    or sequences of numbers.
-    """
+    """Provide shared utilities for making approximate comparisons between
+    numbers or sequences of numbers."""
 
     # Tell numpy to use our `__eq__` operator instead of its.
     __array_ufunc__ = None
@@ -81,16 +73,14 @@ class ApproxBase:
         return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
 
     def _yield_comparisons(self, actual):
-        """
-        Yield all the pairs of numbers to be compared.  This is used to
-        implement the `__eq__` method.
+        """Yield all the pairs of numbers to be compared.
+
+        This is used to implement the `__eq__` method.
         """
         raise NotImplementedError
 
     def _check_type(self) -> None:
-        """
-        Raise a TypeError if the expected value is not a valid type.
-        """
+        """Raise a TypeError if the expected value is not a valid type."""
         # This is only a concern if the expected value is a sequence.  In every
         # other case, the approx() function ensures that the expected value has
         # a numeric type.  For this reason, the default is to do nothing.  The
@@ -107,9 +97,7 @@ def _recursive_list_map(f, x):
 
 
 class ApproxNumpy(ApproxBase):
-    """
-    Perform approximate comparisons where the expected value is numpy array.
-    """
+    """Perform approximate comparisons where the expected value is numpy array."""
 
     def __repr__(self) -> str:
         list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist())
@@ -118,7 +106,7 @@ class ApproxNumpy(ApproxBase):
     def __eq__(self, actual) -> bool:
         import numpy as np
 
-        # self.expected is supposed to always be an array here
+        # self.expected is supposed to always be an array here.
 
         if not np.isscalar(actual):
             try:
@@ -149,10 +137,8 @@ class ApproxNumpy(ApproxBase):
 
 
 class ApproxMapping(ApproxBase):
-    """
-    Perform approximate comparisons where the expected value is a mapping with
-    numeric values (the keys can be anything).
-    """
+    """Perform approximate comparisons where the expected value is a mapping
+    with numeric values (the keys can be anything)."""
 
     def __repr__(self) -> str:
         return "approx({!r})".format(
@@ -180,10 +166,7 @@ class ApproxMapping(ApproxBase):
 
 
 class ApproxSequencelike(ApproxBase):
-    """
-    Perform approximate comparisons where the expected value is a sequence of
-    numbers.
-    """
+    """Perform approximate comparisons where the expected value is a sequence of numbers."""
 
     def __repr__(self) -> str:
         seq_type = type(self.expected)
@@ -214,9 +197,7 @@ class ApproxSequencelike(ApproxBase):
 
 
 class ApproxScalar(ApproxBase):
-    """
-    Perform approximate comparisons where the expected value is a single number.
-    """
+    """Perform approximate comparisons where the expected value is a single number."""
 
     # Using Real should be better than this Union, but not possible yet:
     # https://github.com/python/typeshed/pull/3108
@@ -224,13 +205,14 @@ class ApproxScalar(ApproxBase):
     DEFAULT_RELATIVE_TOLERANCE = 1e-6  # type: Union[float, Decimal]
 
     def __repr__(self) -> str:
-        """
-        Return a string communicating both the expected value and the tolerance
-        for the comparison being made, e.g. '1.0 ± 1e-6', '(3+4j) ± 5e-6 ∠ ±180°'.
+        """Return a string communicating both the expected value and the
+        tolerance for the comparison being made.
+
+        For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``.
         """
 
         # Infinities aren't compared using tolerances, so don't show a
-        # tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j)
+        # tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j).
         if math.isinf(abs(self.expected)):
             return str(self.expected)
 
@@ -246,10 +228,8 @@ class ApproxScalar(ApproxBase):
         return "{} ± {}".format(self.expected, vetted_tolerance)
 
     def __eq__(self, actual) -> bool:
-        """
-        Return true if the given value is equal to the expected value within
-        the pre-specified tolerance.
-        """
+        """Return whether the given value is equal to the expected value
+        within the pre-specified tolerance."""
         if _is_numpy_array(actual):
             # Call ``__eq__()`` manually to prevent infinite-recursion with
             # numpy<1.13.  See #3748.
@@ -283,10 +263,10 @@ class ApproxScalar(ApproxBase):
 
     @property
     def tolerance(self):
-        """
-        Return the tolerance for the comparison.  This could be either an
-        absolute tolerance or a relative tolerance, depending on what the user
-        specified or which would be larger.
+        """Return the tolerance for the comparison.
+
+        This could be either an absolute tolerance or a relative tolerance,
+        depending on what the user specified or which would be larger.
         """
 
         def set_default(x, default):
@@ -330,17 +310,14 @@ class ApproxScalar(ApproxBase):
 
 
 class ApproxDecimal(ApproxScalar):
-    """
-    Perform approximate comparisons where the expected value is a decimal.
-    """
+    """Perform approximate comparisons where the expected value is a Decimal."""
 
     DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")
     DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")
 
 
 def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
-    """
-    Assert that two numbers (or two sets of numbers) are equal to each other
+    """Assert that two numbers (or two sets of numbers) are equal to each other
     within some tolerance.
 
     Due to the `intricacies of floating-point arithmetic`__, numbers that we
@@ -519,7 +496,8 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
     elif (
         isinstance(expected, Iterable)
         and isinstance(expected, Sized)
-        and not isinstance(expected, STRING_TYPES)
+        # Type ignored because the error is wrong -- not unreachable.
+        and not isinstance(expected, STRING_TYPES)  # type: ignore[unreachable]
     ):
         cls = ApproxSequencelike
     else:
@@ -529,9 +507,9 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
 
 
 def _is_numpy_array(obj: object) -> bool:
-    """
-    Return true if the given object is a numpy array.  Make a special effort to
-    avoid importing numpy unless it's really necessary.
+    """Return true if the given object is a numpy array.
+
+    A special effort is made to avoid importing numpy unless it's really necessary.
     """
     import sys
 
@@ -550,19 +528,19 @@ _E = TypeVar("_E", bound=BaseException)
 def raises(
     expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
     *,
-    match: "Optional[Union[str, Pattern]]" = ...
+    match: "Optional[Union[str, Pattern[str]]]" = ...
 ) -> "RaisesContext[_E]":
-    ...  # pragma: no cover
+    ...
 
 
 @overload  # noqa: F811
 def raises(  # noqa: F811
     expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
-    func: Callable,
+    func: Callable[..., Any],
     *args: Any,
     **kwargs: Any
 ) -> _pytest._code.ExceptionInfo[_E]:
-    ...  # pragma: no cover
+    ...
 
 
 def raises(  # noqa: F811
@@ -570,11 +548,11 @@ def raises(  # noqa: F811
     *args: Any,
     **kwargs: Any
 ) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]:
-    r"""
-    Assert that a code block/function call raises ``expected_exception``
+    r"""Assert that a code block/function call raises ``expected_exception``
     or raise a failure exception otherwise.
 
-    :kwparam match: if specified, a string containing a regular expression,
+    :kwparam match:
+        If specified, a string containing a regular expression,
         or a regular expression object, that is tested against the string
         representation of the exception using ``re.search``. To match a literal
         string that may contain `special characters`__, the pattern can
@@ -680,16 +658,21 @@ def raises(  # noqa: F811
         documentation for :ref:`the try statement <python:try>`.
     """
     __tracebackhide__ = True
-    for exc in filterfalse(
-        inspect.isclass, always_iterable(expected_exception, BASE_TYPE)
-    ):
-        msg = "exceptions must be derived from BaseException, not %s"
-        raise TypeError(msg % type(exc))
+
+    if isinstance(expected_exception, type):
+        excepted_exceptions = (expected_exception,)  # type: Tuple[Type[_E], ...]
+    else:
+        excepted_exceptions = expected_exception
+    for exc in excepted_exceptions:
+        if not isinstance(exc, type) or not issubclass(exc, BaseException):  # type: ignore[unreachable]
+            msg = "expected exception must be a BaseException type, not {}"  # type: ignore[unreachable]
+            not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
+            raise TypeError(msg.format(not_a))
 
     message = "DID NOT RAISE {}".format(expected_exception)
 
     if not args:
-        match = kwargs.pop("match", None)
+        match = kwargs.pop("match", None)  # type: Optional[Union[str, Pattern[str]]]
         if kwargs:
             msg = "Unexpected keyword arguments passed to pytest.raises: "
             msg += ", ".join(sorted(kwargs))
@@ -717,12 +700,13 @@ def raises(  # noqa: F811
 raises.Exception = fail.Exception  # type: ignore
 
 
+@final
 class RaisesContext(Generic[_E]):
     def __init__(
         self,
         expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
         message: str,
-        match_expr: Optional[Union[str, "Pattern"]] = None,
+        match_expr: Optional[Union[str, "Pattern[str]"]] = None,
     ) -> None:
         self.expected_exception = expected_exception
         self.message = message
index 11ca571aadd7f85be13335a94adb76d4b413c883..39d6de914551ac1b45964f8cf1f15524d897357e 100644 (file)
@@ -1,4 +1,4 @@
-""" recording warnings during test function execution. """
+"""Record warnings during test function execution."""
 import re
 import warnings
 from types import TracebackType
@@ -13,6 +13,7 @@ from typing import Tuple
 from typing import TypeVar
 from typing import Union
 
+from _pytest.compat import final
 from _pytest.compat import overload
 from _pytest.compat import TYPE_CHECKING
 from _pytest.fixtures import fixture
@@ -40,20 +41,20 @@ def recwarn() -> Generator["WarningsRecorder", None, None]:
 
 @overload
 def deprecated_call(
-    *, match: Optional[Union[str, "Pattern"]] = ...
+    *, match: Optional[Union[str, "Pattern[str]"]] = ...
 ) -> "WarningsRecorder":
-    raise NotImplementedError()
+    ...
 
 
 @overload  # noqa: F811
 def deprecated_call(  # noqa: F811
     func: Callable[..., T], *args: Any, **kwargs: Any
 ) -> T:
-    raise NotImplementedError()
+    ...
 
 
 def deprecated_call(  # noqa: F811
-    func: Optional[Callable] = None, *args: Any, **kwargs: Any
+    func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any
 ) -> Union["WarningsRecorder", Any]:
     """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning``.
 
@@ -87,9 +88,9 @@ def deprecated_call(  # noqa: F811
 def warns(
     expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]],
     *,
-    match: "Optional[Union[str, Pattern]]" = ...
+    match: "Optional[Union[str, Pattern[str]]]" = ...
 ) -> "WarningsChecker":
-    raise NotImplementedError()
+    ...
 
 
 @overload  # noqa: F811
@@ -99,13 +100,13 @@ def warns(  # noqa: F811
     *args: Any,
     **kwargs: Any
 ) -> T:
-    raise NotImplementedError()
+    ...
 
 
 def warns(  # noqa: F811
     expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]],
     *args: Any,
-    match: Optional[Union[str, "Pattern"]] = None,
+    match: Optional[Union[str, "Pattern[str]"]] = None,
     **kwargs: Any
 ) -> Union["WarningsChecker", Any]:
     r"""Assert that code raises a particular class of warning.
@@ -228,13 +229,14 @@ class WarningsRecorder(warnings.catch_warnings):
         self._entered = False
 
 
+@final
 class WarningsChecker(WarningsRecorder):
     def __init__(
         self,
         expected_warning: Optional[
             Union["Type[Warning]", Tuple["Type[Warning]", ...]]
         ] = None,
-        match_expr: Optional[Union[str, "Pattern"]] = None,
+        match_expr: Optional[Union[str, "Pattern[str]"]] = None,
     ) -> None:
         super().__init__()
 
index cbd9ae1832a57d2b106dba4f895bfcfbc5353ed3..c42f778ec40bbc5a30c22121ea41644b35d47c8c 100644 (file)
@@ -1,6 +1,7 @@
 from io import StringIO
 from pprint import pprint
 from typing import Any
+from typing import cast
 from typing import Dict
 from typing import Iterable
 from typing import Iterator
@@ -15,6 +16,7 @@ import py
 
 from _pytest._code.code import ExceptionChainRepr
 from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import ExceptionRepr
 from _pytest._code.code import ReprEntry
 from _pytest._code.code import ReprEntryNative
 from _pytest._code.code import ReprExceptionInfo
@@ -24,6 +26,7 @@ from _pytest._code.code import ReprLocals
 from _pytest._code.code import ReprTraceback
 from _pytest._code.code import TerminalRepr
 from _pytest._io import TerminalWriter
+from _pytest.compat import final
 from _pytest.compat import TYPE_CHECKING
 from _pytest.config import Config
 from _pytest.nodes import Collector
@@ -57,8 +60,9 @@ _R = TypeVar("_R", bound="BaseReport")
 class BaseReport:
     when = None  # type: Optional[str]
     location = None  # type: Optional[Tuple[str, Optional[int], str]]
-    # TODO: Improve this Any.
-    longrepr = None  # type: Optional[Any]
+    longrepr = (
+        None
+    )  # type: Union[None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr]
     sections = []  # type: List[Tuple[str, str]]
     nodeid = None  # type: str
 
@@ -68,7 +72,7 @@ class BaseReport:
     if TYPE_CHECKING:
         # Can have arbitrary fields given to __init__().
         def __getattr__(self, key: str) -> Any:
-            raise NotImplementedError()
+            ...
 
     def toterminal(self, out: TerminalWriter) -> None:
         if hasattr(self, "node"):
@@ -79,7 +83,8 @@ class BaseReport:
             return
 
         if hasattr(longrepr, "toterminal"):
-            longrepr.toterminal(out)
+            longrepr_terminal = cast(TerminalRepr, longrepr)
+            longrepr_terminal.toterminal(out)
         else:
             try:
                 s = str(longrepr)
@@ -94,9 +99,8 @@ class BaseReport:
 
     @property
     def longreprtext(self) -> str:
-        """
-        Read-only property that returns the full string representation
-        of ``longrepr``.
+        """Read-only property that returns the full string representation of
+        ``longrepr``.
 
         .. versionadded:: 3.0
         """
@@ -109,7 +113,7 @@ class BaseReport:
 
     @property
     def caplog(self) -> str:
-        """Return captured log lines, if log capturing is enabled
+        """Return captured log lines, if log capturing is enabled.
 
         .. versionadded:: 3.5
         """
@@ -119,7 +123,7 @@ class BaseReport:
 
     @property
     def capstdout(self) -> str:
-        """Return captured text from stdout, if capturing is enabled
+        """Return captured text from stdout, if capturing is enabled.
 
         .. versionadded:: 3.0
         """
@@ -129,7 +133,7 @@ class BaseReport:
 
     @property
     def capstderr(self) -> str:
-        """Return captured text from stderr, if capturing is enabled
+        """Return captured text from stderr, if capturing is enabled.
 
         .. versionadded:: 3.0
         """
@@ -147,11 +151,8 @@ class BaseReport:
 
     @property
     def count_towards_summary(self) -> bool:
-        """
-        **Experimental**
-
-        ``True`` if this report should be counted towards the totals shown at the end of the
-        test session: "1 passed, 1 failure, etc".
+        """**Experimental** Whether this report should be counted towards the
+        totals shown at the end of the test session: "1 passed, 1 failure, etc".
 
         .. note::
 
@@ -162,11 +163,9 @@ class BaseReport:
 
     @property
     def head_line(self) -> Optional[str]:
-        """
-        **Experimental**
-
-        Returns the head line shown with longrepr output for this report, more commonly during
-        traceback representation during failures::
+        """**Experimental** The head line shown with longrepr output for this
+        report, more commonly during traceback representation during
+        failures::
 
             ________ Test.foo ________
 
@@ -190,11 +189,10 @@ class BaseReport:
         return verbose
 
     def _to_json(self) -> Dict[str, Any]:
-        """
-        This was originally the serialize_report() function from xdist (ca03269).
+        """Return the contents of this report as a dict of builtin entries,
+        suitable for serialization.
 
-        Returns the contents of this report as a dict of builtin entries, suitable for
-        serialization.
+        This was originally the serialize_report() function from xdist (ca03269).
 
         Experimental method.
         """
@@ -202,11 +200,11 @@ class BaseReport:
 
     @classmethod
     def _from_json(cls: "Type[_R]", reportdict: Dict[str, object]) -> _R:
-        """
-        This was originally the serialize_report() function from xdist (ca03269).
+        """Create either a TestReport or CollectReport, depending on the calling class.
 
-        Factory method that returns either a TestReport or CollectReport, depending on the calling
-        class. It's the callers responsibility to know which class to pass here.
+        It is the callers responsibility to know which class to pass here.
+
+        This was originally the serialize_report() function from xdist (ca03269).
 
         Experimental method.
         """
@@ -228,10 +226,10 @@ def _report_unserialization_failure(
     raise RuntimeError(stream.getvalue())
 
 
+@final
 class TestReport(BaseReport):
-    """ Basic test report object (also used for setup and teardown calls if
-    they fail).
-    """
+    """Basic test report object (also used for setup and teardown calls if
+    they fail)."""
 
     __test__ = False
 
@@ -241,45 +239,47 @@ class TestReport(BaseReport):
         location: Tuple[str, Optional[int], str],
         keywords,
         outcome: "Literal['passed', 'failed', 'skipped']",
-        longrepr,
+        longrepr: Union[
+            None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
+        ],
         when: "Literal['setup', 'call', 'teardown']",
         sections: Iterable[Tuple[str, str]] = (),
         duration: float = 0,
         user_properties: Optional[Iterable[Tuple[str, object]]] = None,
         **extra
     ) -> None:
-        #: normalized collection node id
+        #: Normalized collection nodeid.
         self.nodeid = nodeid
 
-        #: a (filesystempath, lineno, domaininfo) tuple indicating the
+        #: A (filesystempath, lineno, domaininfo) tuple indicating the
         #: actual location of a test item - it might be different from the
         #: collected one e.g. if a method is inherited from a different module.
         self.location = location  # type: Tuple[str, Optional[int], str]
 
-        #: a name -> value dictionary containing all keywords and
+        #: A name -> value dictionary containing all keywords and
         #: markers associated with a test invocation.
         self.keywords = keywords
 
-        #: test outcome, always one of "passed", "failed", "skipped".
+        #: Test outcome, always one of "passed", "failed", "skipped".
         self.outcome = outcome
 
         #: None or a failure representation.
         self.longrepr = longrepr
 
-        #: one of 'setup', 'call', 'teardown' to indicate runtest phase.
+        #: One of 'setup', 'call', 'teardown' to indicate runtest phase.
         self.when = when
 
-        #: user properties is a list of tuples (name, value) that holds user
-        #: defined properties of the test
+        #: User properties is a list of tuples (name, value) that holds user
+        #: defined properties of the test.
         self.user_properties = list(user_properties or [])
 
-        #: list of pairs ``(str, str)`` of extra information which needs to
+        #: List of pairs ``(str, str)`` of extra information which needs to
         #: marshallable. Used by pytest to add captured text
         #: from ``stdout`` and ``stderr``, but may be used by other plugins
         #: to add arbitrary information to reports.
         self.sections = list(sections)
 
-        #: time it took to run just the test
+        #: Time it took to run just the test.
         self.duration = duration
 
         self.__dict__.update(extra)
@@ -291,9 +291,7 @@ class TestReport(BaseReport):
 
     @classmethod
     def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
-        """
-        Factory method to create and fill a TestReport with standard item and call info.
-        """
+        """Create and fill a TestReport with standard item and call info."""
         when = call.when
         # Remove "collect" from the Literal type -- only for collection calls.
         assert when != "collect"
@@ -303,8 +301,9 @@ class TestReport(BaseReport):
         sections = []
         if not call.excinfo:
             outcome = "passed"  # type: Literal["passed", "failed", "skipped"]
-            # TODO: Improve this Any.
-            longrepr = None  # type: Optional[Any]
+            longrepr = (
+                None
+            )  # type: Union[None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr]
         else:
             if not isinstance(excinfo, ExceptionInfo):
                 outcome = "failed"
@@ -336,6 +335,7 @@ class TestReport(BaseReport):
         )
 
 
+@final
 class CollectReport(BaseReport):
     """Collection report object."""
 
@@ -350,10 +350,10 @@ class CollectReport(BaseReport):
         sections: Iterable[Tuple[str, str]] = (),
         **extra
     ) -> None:
-        #: normalized collection node id
+        #: Normalized collection nodeid.
         self.nodeid = nodeid
 
-        #: test outcome, always one of "passed", "failed", "skipped".
+        #: Test outcome, always one of "passed", "failed", "skipped".
         self.outcome = outcome
 
         #: None or a failure representation.
@@ -362,10 +362,11 @@ class CollectReport(BaseReport):
         #: The collected items and collection nodes.
         self.result = result or []
 
-        #: list of pairs ``(str, str)`` of extra information which needs to
-        #: marshallable. Used by pytest to add captured text
-        #: from ``stdout`` and ``stderr``, but may be used by other plugins
-        #: to add arbitrary information to reports.
+        #: List of pairs ``(str, str)`` of extra information which needs to
+        #: marshallable.
+        # Used by pytest to add captured text : from ``stdout`` and ``stderr``,
+        # but may be used by other plugins : to add arbitrary information to
+        # reports.
         self.sections = list(sections)
 
         self.__dict__.update(extra)
@@ -381,7 +382,7 @@ class CollectReport(BaseReport):
 
 
 class CollectErrorRepr(TerminalRepr):
-    def __init__(self, msg) -> None:
+    def __init__(self, msg: str) -> None:
         self.longrepr = msg
 
     def toterminal(self, out: TerminalWriter) -> None:
@@ -395,7 +396,8 @@ def pytest_report_to_serializable(
         data = report._to_json()
         data["$report_type"] = report.__class__.__name__
         return data
-    return None
+    # TODO: Check if this is actually reachable.
+    return None  # type: ignore[unreachable]
 
 
 def pytest_report_from_serializable(
@@ -413,11 +415,10 @@ def pytest_report_from_serializable(
 
 
 def _report_to_json(report: BaseReport) -> Dict[str, Any]:
-    """
-    This was originally the serialize_report() function from xdist (ca03269).
+    """Return the contents of this report as a dict of builtin entries,
+    suitable for serialization.
 
-    Returns the contents of this report as a dict of builtin entries, suitable for
-    serialization.
+    This was originally the serialize_report() function from xdist (ca03269).
     """
 
     def serialize_repr_entry(
@@ -445,16 +446,18 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
         else:
             return None
 
-    def serialize_longrepr(rep: BaseReport) -> Dict[str, Any]:
+    def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]:
         assert rep.longrepr is not None
+        # TODO: Investigate whether the duck typing is really necessary here.
+        longrepr = cast(ExceptionRepr, rep.longrepr)
         result = {
-            "reprcrash": serialize_repr_crash(rep.longrepr.reprcrash),
-            "reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback),
-            "sections": rep.longrepr.sections,
+            "reprcrash": serialize_repr_crash(longrepr.reprcrash),
+            "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback),
+            "sections": longrepr.sections,
         }  # type: Dict[str, Any]
-        if isinstance(rep.longrepr, ExceptionChainRepr):
+        if isinstance(longrepr, ExceptionChainRepr):
             result["chain"] = []
-            for repr_traceback, repr_crash, description in rep.longrepr.chain:
+            for repr_traceback, repr_crash, description in longrepr.chain:
                 result["chain"].append(
                     (
                         serialize_repr_traceback(repr_traceback),
@@ -471,7 +474,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
         if hasattr(report.longrepr, "reprtraceback") and hasattr(
             report.longrepr, "reprcrash"
         ):
-            d["longrepr"] = serialize_longrepr(report)
+            d["longrepr"] = serialize_exception_longrepr(report)
         else:
             d["longrepr"] = str(report.longrepr)
     else:
@@ -485,10 +488,10 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
 
 
 def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
-    """
-    This was originally the serialize_report() function from xdist (ca03269).
+    """Return **kwargs that can be used to construct a TestReport or
+    CollectReport instance.
 
-    Returns **kwargs that can be used to construct a TestReport or CollectReport instance.
+    This was originally the serialize_report() function from xdist (ca03269).
     """
 
     def deserialize_repr_entry(entry_data):
@@ -524,7 +527,7 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
         ]
         return ReprTraceback(**repr_traceback_dict)
 
-    def deserialize_repr_crash(repr_crash_dict: Optional[dict]):
+    def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]):
         if repr_crash_dict is not None:
             return ReprFileLocation(**repr_crash_dict)
         else:
diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py
deleted file mode 100644 (file)
index cd6824a..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-""" log machine-parseable test session result information in a plain
-text file.
-"""
-import os
-
-import py
-
-from _pytest._code.code import ExceptionRepr
-from _pytest.config import Config
-from _pytest.config.argparsing import Parser
-from _pytest.reports import CollectReport
-from _pytest.reports import TestReport
-from _pytest.store import StoreKey
-
-
-resultlog_key = StoreKey["ResultLog"]()
-
-
-def pytest_addoption(parser: Parser) -> None:
-    group = parser.getgroup("terminal reporting", "resultlog plugin options")
-    group.addoption(
-        "--resultlog",
-        "--result-log",
-        action="store",
-        metavar="path",
-        default=None,
-        help="DEPRECATED path for machine-readable result log.",
-    )
-
-
-def pytest_configure(config: Config) -> None:
-    resultlog = config.option.resultlog
-    # prevent opening resultlog on worker nodes (xdist)
-    if resultlog and not hasattr(config, "workerinput"):
-        dirname = os.path.dirname(os.path.abspath(resultlog))
-        if not os.path.isdir(dirname):
-            os.makedirs(dirname)
-        logfile = open(resultlog, "w", 1)  # line buffered
-        config._store[resultlog_key] = ResultLog(config, logfile)
-        config.pluginmanager.register(config._store[resultlog_key])
-
-        from _pytest.deprecated import RESULT_LOG
-        from _pytest.warnings import _issue_warning_captured
-
-        _issue_warning_captured(RESULT_LOG, config.hook, stacklevel=2)
-
-
-def pytest_unconfigure(config: Config) -> None:
-    resultlog = config._store.get(resultlog_key, None)
-    if resultlog:
-        resultlog.logfile.close()
-        del config._store[resultlog_key]
-        config.pluginmanager.unregister(resultlog)
-
-
-class ResultLog:
-    def __init__(self, config, logfile):
-        self.config = config
-        self.logfile = logfile  # preferably line buffered
-
-    def write_log_entry(self, testpath, lettercode, longrepr):
-        print("{} {}".format(lettercode, testpath), file=self.logfile)
-        for line in longrepr.splitlines():
-            print(" %s" % line, file=self.logfile)
-
-    def log_outcome(self, report, lettercode, longrepr):
-        testpath = getattr(report, "nodeid", None)
-        if testpath is None:
-            testpath = report.fspath
-        self.write_log_entry(testpath, lettercode, longrepr)
-
-    def pytest_runtest_logreport(self, report: TestReport) -> None:
-        if report.when != "call" and report.passed:
-            return
-        res = self.config.hook.pytest_report_teststatus(
-            report=report, config=self.config
-        )
-        code = res[1]
-        if code == "x":
-            longrepr = str(report.longrepr)
-        elif code == "X":
-            longrepr = ""
-        elif report.passed:
-            longrepr = ""
-        elif report.skipped:
-            assert report.longrepr is not None
-            longrepr = str(report.longrepr[2])
-        else:
-            longrepr = str(report.longrepr)
-        self.log_outcome(report, code, longrepr)
-
-    def pytest_collectreport(self, report: CollectReport) -> None:
-        if not report.passed:
-            if report.failed:
-                code = "F"
-                longrepr = str(report.longrepr)
-            else:
-                assert report.skipped
-                code = "S"
-                longrepr = "%s:%d: %s" % report.longrepr  # type: ignore
-            self.log_outcome(report, code, longrepr)
-
-    def pytest_internalerror(self, excrepr: ExceptionRepr) -> None:
-        if excrepr.reprcrash is not None:
-            path = excrepr.reprcrash.path
-        else:
-            path = "cwd:%s" % py.path.local()
-        self.write_log_entry(path, "!", str(excrepr))
index 69754ad5e105fe15e7d598aeeec657840993fcda..f29d356fe07e4bdecc7f566fbcfdfb57057048e8 100644 (file)
@@ -1,8 +1,7 @@
-""" basic collect and runtest protocol implementations """
+"""Basic collect and runtest protocol implementations."""
 import bdb
 import os
 import sys
-from typing import Any
 from typing import Callable
 from typing import cast
 from typing import Dict
@@ -22,6 +21,8 @@ from .reports import TestReport
 from _pytest import timing
 from _pytest._code.code import ExceptionChainRepr
 from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import TerminalRepr
+from _pytest.compat import final
 from _pytest.compat import TYPE_CHECKING
 from _pytest.config.argparsing import Parser
 from _pytest.nodes import Collector
@@ -39,7 +40,7 @@ if TYPE_CHECKING:
     from _pytest.terminal import TerminalReporter
 
 #
-# pytest plugin hooks
+# pytest plugin hooks.
 
 
 def pytest_addoption(parser: Parser) -> None:
@@ -52,10 +53,19 @@ def pytest_addoption(parser: Parser) -> None:
         metavar="N",
         help="show N slowest setup/test durations (N=0 for all).",
     )
+    group.addoption(
+        "--durations-min",
+        action="store",
+        type=float,
+        default=0.005,
+        metavar="N",
+        help="Minimal duration in seconds for inclusion in slowest list. Default 0.005",
+    )
 
 
 def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
     durations = terminalreporter.config.option.durations
+    durations_min = terminalreporter.config.option.durations_min
     verbose = terminalreporter.config.getvalue("verbose")
     if durations is None:
         return
@@ -76,11 +86,11 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
         dlist = dlist[:durations]
 
     for i, rep in enumerate(dlist):
-        if verbose < 2 and rep.duration < 0.005:
+        if verbose < 2 and rep.duration < durations_min:
             tr.write_line("")
             tr.write_line(
-                "(%s durations < 0.005s hidden.  Use -vv to show these durations.)"
-                % (len(dlist) - i)
+                "(%s durations < %gs hidden.  Use -vv to show these durations.)"
+                % (len(dlist) - i, durations_min)
             )
             break
         tr.write_line("{:02.2f}s {:<8} {}".format(rep.duration, rep.when, rep.nodeid))
@@ -116,8 +126,8 @@ def runtestprotocol(
         if not item.config.getoption("setuponly", False):
             reports.append(call_and_report(item, "call", log))
     reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
-    # after all teardown hooks have been called
-    # want funcargs and request info to go away
+    # After all teardown hooks have been called
+    # want funcargs and request info to go away.
     if hasrequest:
         item._request = False  # type: ignore[attr-defined]
         item.funcargs = None  # type: ignore[attr-defined]
@@ -170,8 +180,7 @@ def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None:
 def _update_current_test_var(
     item: Item, when: Optional["Literal['setup', 'call', 'teardown']"]
 ) -> None:
-    """
-    Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
+    """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
 
     If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment.
     """
@@ -214,7 +223,7 @@ def call_and_report(
     return report
 
 
-def check_interactive_exception(call: "CallInfo", report: BaseReport) -> bool:
+def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool:
     """Check whether the call raised an exception that should be reported as
     interactive."""
     if call.excinfo is None:
@@ -248,23 +257,30 @@ def call_runtest_hook(
     )
 
 
-_T = TypeVar("_T")
+TResult = TypeVar("TResult", covariant=True)
 
 
+@final
 @attr.s(repr=False)
-class CallInfo(Generic[_T]):
-    """ Result/Exception info a function invocation.
-
-    :param T result: The return value of the call, if it didn't raise. Can only be accessed
-        if excinfo is None.
-    :param Optional[ExceptionInfo] excinfo: The captured exception of the call, if it raised.
-    :param float start: The system time when the call started, in seconds since the epoch.
-    :param float stop: The system time when the call ended, in seconds since the epoch.
-    :param float duration: The call duration, in seconds.
-    :param str when: The context of invocation: "setup", "call", "teardown", ...
+class CallInfo(Generic[TResult]):
+    """Result/Exception info a function invocation.
+
+    :param T result:
+        The return value of the call, if it didn't raise. Can only be
+        accessed if excinfo is None.
+    :param Optional[ExceptionInfo] excinfo:
+        The captured exception of the call, if it raised.
+    :param float start:
+        The system time when the call started, in seconds since the epoch.
+    :param float stop:
+        The system time when the call ended, in seconds since the epoch.
+    :param float duration:
+        The call duration, in seconds.
+    :param str when:
+        The context of invocation: "setup", "call", "teardown", ...
     """
 
-    _result = attr.ib(type="Optional[_T]")
+    _result = attr.ib(type="Optional[TResult]")
     excinfo = attr.ib(type=Optional[ExceptionInfo[BaseException]])
     start = attr.ib(type=float)
     stop = attr.ib(type=float)
@@ -272,26 +288,26 @@ class CallInfo(Generic[_T]):
     when = attr.ib(type="Literal['collect', 'setup', 'call', 'teardown']")
 
     @property
-    def result(self) -> _T:
+    def result(self) -> TResult:
         if self.excinfo is not None:
             raise AttributeError("{!r} has no valid result".format(self))
         # The cast is safe because an exception wasn't raised, hence
         # _result has the expected function return type (which may be
         #  None, that's why a cast and not an assert).
-        return cast(_T, self._result)
+        return cast(TResult, self._result)
 
     @classmethod
     def from_call(
         cls,
-        func: "Callable[[], _T]",
+        func: "Callable[[], TResult]",
         when: "Literal['collect', 'setup', 'call', 'teardown']",
         reraise: "Optional[Union[Type[BaseException], Tuple[Type[BaseException], ...]]]" = None,
-    ) -> "CallInfo[_T]":
+    ) -> "CallInfo[TResult]":
         excinfo = None
         start = timing.time()
         precise_start = timing.perf_counter()
         try:
-            result = func()  # type: Optional[_T]
+            result = func()  # type: Optional[TResult]
         except BaseException:
             excinfo = ExceptionInfo.from_current()
             if reraise is not None and isinstance(excinfo.value, reraise):
@@ -322,8 +338,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport:
 
 def pytest_make_collect_report(collector: Collector) -> CollectReport:
     call = CallInfo.from_call(lambda: list(collector.collect()), "collect")
-    # TODO: Better typing for longrepr.
-    longrepr = None  # type: Optional[Any]
+    longrepr = None  # type: Union[None, Tuple[str, int, str], str, TerminalRepr]
     if not call.excinfo:
         outcome = "passed"  # type: Literal["passed", "skipped", "failed"]
     else:
@@ -343,6 +358,7 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
             outcome = "failed"
             errorinfo = collector.repr_failure(call.excinfo)
             if not hasattr(errorinfo, "toterminal"):
+                assert isinstance(errorinfo, str)
                 errorinfo = CollectErrorRepr(errorinfo)
             longrepr = errorinfo
     result = call.result if not call.excinfo else None
@@ -352,14 +368,14 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
 
 
 class SetupState:
-    """ shared state for setting up/tearing down test items or collectors. """
+    """Shared state for setting up/tearing down test items or collectors."""
 
     def __init__(self):
         self.stack = []  # type: List[Node]
         self._finalizers = {}  # type: Dict[Node, List[Callable[[], object]]]
 
     def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None:
-        """ attach a finalizer to the given colitem. """
+        """Attach a finalizer to the given colitem."""
         assert colitem and not isinstance(colitem, tuple)
         assert callable(finalizer)
         # assert colitem in self.stack  # some unit tests don't setup stack :/
@@ -419,7 +435,7 @@ class SetupState:
     def prepare(self, colitem) -> None:
         """Setup objects along the collector chain to the test-method."""
 
-        # check if the last collection node has raised an error
+        # Check if the last collection node has raised an error.
         for col in self.stack:
             if hasattr(col, "_prepare_exc"):
                 exc = col._prepare_exc  # type: ignore[attr-defined]
index dfd01cc76b2cb5964c9373291c231906cc1c4be2..44a1094c0d24faa3c0573ee24e1fd604e49266bc 100644 (file)
@@ -29,7 +29,7 @@ def pytest_addoption(parser: Parser) -> None:
 
 @pytest.hookimpl(hookwrapper=True)
 def pytest_fixture_setup(
-    fixturedef: FixtureDef, request: SubRequest
+    fixturedef: FixtureDef[object], request: SubRequest
 ) -> Generator[None, None, None]:
     yield
     if request.config.option.setupshow:
@@ -47,7 +47,7 @@ def pytest_fixture_setup(
         _show_fixture_action(fixturedef, "SETUP")
 
 
-def pytest_fixture_post_finalizer(fixturedef: FixtureDef) -> None:
+def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None:
     if fixturedef.cached_result is not None:
         config = fixturedef._fixturemanager.config
         if config.option.setupshow:
@@ -56,7 +56,7 @@ def pytest_fixture_post_finalizer(fixturedef: FixtureDef) -> None:
                 del fixturedef.cached_param  # type: ignore[attr-defined]
 
 
-def _show_fixture_action(fixturedef: FixtureDef, msg: str) -> None:
+def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None:
     config = fixturedef._fixturemanager.config
     capman = config.pluginmanager.getplugin("capturemanager")
     if capman:
index 0994ebbf20733ed32cdae068f1b0e415d92bb3d2..9ba81ccaf0a4f28be0da63bd5881f9cff14a4f1e 100644 (file)
@@ -22,7 +22,7 @@ def pytest_addoption(parser: Parser) -> None:
 
 @pytest.hookimpl(tryfirst=True)
 def pytest_fixture_setup(
-    fixturedef: FixtureDef, request: SubRequest
+    fixturedef: FixtureDef[object], request: SubRequest
 ) -> Optional[object]:
     # Will return a dummy fixture if the setuponly option is provided.
     if request.config.option.setupplan:
index e333e78df9b0f80158cafcc02d7101bae0af9c79..c5b4ff39e8538c7c1fb4d9a54d0c7b448b277113 100644 (file)
@@ -1,4 +1,4 @@
-""" support for skip/xfail functions and markers. """
+"""Support for skip/xfail functions and markers."""
 import os
 import platform
 import sys
@@ -298,9 +298,9 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
         and rep.skipped
         and type(rep.longrepr) is tuple
     ):
-        # skipped by mark.skipif; change the location of the failure
+        # Skipped by mark.skipif; change the location of the failure
         # to point to the item definition, otherwise it will display
-        # the location of where the skip exception was raised within pytest
+        # the location of where the skip exception was raised within pytest.
         _, _, reason = rep.longrepr
         filename, line = item.reportinfo()[:2]
         assert line is not None
index 2b46c4389361bf0e594a57cdf54d998887250acb..fbf3c588f36fa362356aa5cab2bd709f72463e39 100644 (file)
@@ -92,7 +92,7 @@ class Store:
     def __getitem__(self, key: StoreKey[T]) -> T:
         """Get the value for key.
 
-        Raises KeyError if the key wasn't set before.
+        Raises ``KeyError`` if the key wasn't set before.
         """
         return cast(T, self._store[key])
 
@@ -116,10 +116,10 @@ class Store:
     def __delitem__(self, key: StoreKey[T]) -> None:
         """Delete the value for key.
 
-        Raises KeyError if the key wasn't set before.
+        Raises ``KeyError`` if the key wasn't set before.
         """
         del self._store[key]
 
     def __contains__(self, key: StoreKey[T]) -> bool:
-        """Returns whether key was set."""
+        """Return whether key was set."""
         return key in self._store
index ef9da50f3f812dd281f73baf1006dde6288bf73a..e059612c212d8ff9ceb14ab784dbcb918ebc65d0 100644 (file)
@@ -1,4 +1,4 @@
-""" terminal reporting of the full testing process.
+"""Terminal reporting of the full testing process.
 
 This is a good source for looking at the various reporting hooks.
 """
@@ -25,24 +25,25 @@ from typing import Union
 import attr
 import pluggy
 import py
-from more_itertools import collapse
 
 import pytest
 from _pytest import nodes
 from _pytest import timing
 from _pytest._code import ExceptionInfo
 from _pytest._code.code import ExceptionRepr
-from _pytest._io import TerminalWriter
 from _pytest._io.wcwidth import wcswidth
+from _pytest.compat import final
 from _pytest.compat import order_preserving_dict
 from _pytest.compat import TYPE_CHECKING
 from _pytest.config import _PluggyPlugin
 from _pytest.config import Config
 from _pytest.config import ExitCode
 from _pytest.config.argparsing import Parser
-from _pytest.deprecated import TERMINALWRITER_WRITER
 from _pytest.nodes import Item
 from _pytest.nodes import Node
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import Path
 from _pytest.reports import BaseReport
 from _pytest.reports import CollectReport
 from _pytest.reports import TestReport
@@ -70,11 +71,10 @@ _REPORTCHARS_DEFAULT = "fE"
 
 
 class MoreQuietAction(argparse.Action):
-    """
-    a modified copy of the argparse count action which counts down and updates
-    the legacy quiet attribute at the same time
+    """A modified copy of the argparse count action which counts down and updates
+    the legacy quiet attribute at the same time.
 
-    used to unify verbosity handling
+    Used to unify verbosity handling.
     """
 
     def __init__(
@@ -277,13 +277,14 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
 
 @attr.s
 class WarningReport:
-    """
-    Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
+    """Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
 
-    :ivar str message: user friendly message about the warning
-    :ivar str|None nodeid: node id that generated the warning (see ``get_location``).
+    :ivar str message:
+        User friendly message about the warning.
+    :ivar str|None nodeid:
+        nodeid that generated the warning (see ``get_location``).
     :ivar tuple|py.path.local fslocation:
-        file system location of the source of the warning (see ``get_location``).
+        File system location of the source of the warning (see ``get_location``).
     """
 
     message = attr.ib(type=str)
@@ -294,24 +295,22 @@ class WarningReport:
     count_towards_summary = True
 
     def get_location(self, config: Config) -> Optional[str]:
-        """
-        Returns the more user-friendly information about the location
-        of a warning, or None.
-        """
+        """Return the more user-friendly information about the location of a warning, or None."""
         if self.nodeid:
             return self.nodeid
         if self.fslocation:
             if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2:
                 filename, linenum = self.fslocation[:2]
-                relpath = py.path.local(filename).relto(config.invocation_dir)
-                if not relpath:
-                    relpath = str(filename)
+                relpath = bestrelpath(
+                    config.invocation_params.dir, absolutepath(filename)
+                )
                 return "{}:{}".format(relpath, linenum)
             else:
                 return str(self.fslocation)
         return None
 
 
+@final
 class TerminalReporter:
     def __init__(self, config: Config, file: Optional[TextIO] = None) -> None:
         import _pytest.config
@@ -323,13 +322,14 @@ class TerminalReporter:
 
         self.stats = {}  # type: Dict[str, List[Any]]
         self._main_color = None  # type: Optional[str]
-        self._known_types = None  # type: Optional[List]
+        self._known_types = None  # type: Optional[List[str]]
         self.startdir = config.invocation_dir
+        self.startpath = config.invocation_params.dir
         if file is None:
             file = sys.stdout
         self._tw = _pytest.config.create_terminal_writer(config, file)
         self._screen_width = self._tw.fullwidth
-        self.currentfspath = None  # type: Any
+        self.currentfspath = None  # type: Union[None, Path, str, int]
         self.reportchars = getreportopt(config)
         self.hasmarkup = self._tw.hasmarkup
         self.isatty = file.isatty()
@@ -339,18 +339,8 @@ class TerminalReporter:
         self._already_displayed_warnings = None  # type: Optional[int]
         self._keyboardinterrupt_memo = None  # type: Optional[ExceptionRepr]
 
-    @property
-    def writer(self) -> TerminalWriter:
-        warnings.warn(TERMINALWRITER_WRITER, stacklevel=2)
-        return self._tw
-
-    @writer.setter
-    def writer(self, value: TerminalWriter) -> None:
-        warnings.warn(TERMINALWRITER_WRITER, stacklevel=2)
-        self._tw = value
-
     def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
-        """Return True if we should display progress information based on the current config"""
+        """Return whether we should display progress information based on the current config."""
         # do not show progress if we are not capturing output (#3038)
         if self.config.getoption("capture", "no") == "no":
             return False
@@ -401,19 +391,17 @@ class TerminalReporter:
         return char in self.reportchars
 
     def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None:
-        fspath = self.config.rootdir.join(nodeid.split("::")[0])
-        # NOTE: explicitly check for None to work around py bug, and for less
-        # overhead in general (https://github.com/pytest-dev/py/pull/207).
+        fspath = self.config.rootpath / nodeid.split("::")[0]
         if self.currentfspath is None or fspath != self.currentfspath:
             if self.currentfspath is not None and self._show_progress_info:
                 self._write_progress_information_filling_space()
             self.currentfspath = fspath
-            relfspath = self.startdir.bestrelpath(fspath)
+            relfspath = bestrelpath(self.startpath, fspath)
             self._tw.line()
             self._tw.write(relfspath + " ")
         self._tw.write(res, flush=True, **markup)
 
-    def write_ensure_prefix(self, prefix, extra: str = "", **kwargs) -> None:
+    def write_ensure_prefix(self, prefix: str, extra: str = "", **kwargs) -> None:
         if self.currentfspath != prefix:
             self._tw.line()
             self.currentfspath = prefix
@@ -440,10 +428,10 @@ class TerminalReporter:
         self._tw.line(line, **markup)
 
     def rewrite(self, line: str, **markup: bool) -> None:
-        """
-        Rewinds the terminal cursor to the beginning and writes the given line.
+        """Rewinds the terminal cursor to the beginning and writes the given line.
 
-        :kwarg erase: if True, will also add spaces until the full terminal width to ensure
+        :param erase:
+            If True, will also add spaces until the full terminal width to ensure
             previous lines are properly erased.
 
         The rest of the keyword arguments are markup instructions.
@@ -473,7 +461,7 @@ class TerminalReporter:
     def line(self, msg: str, **kw: bool) -> None:
         self._tw.line(msg, **kw)
 
-    def _add_stats(self, category: str, items: Sequence) -> None:
+    def _add_stats(self, category: str, items: Sequence[Any]) -> None:
         set_main_color = category not in self.stats
         self.stats.setdefault(category, []).extend(items)
         if set_main_color:
@@ -500,9 +488,9 @@ class TerminalReporter:
     def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
         if self.config.option.traceconfig:
             msg = "PLUGIN registered: {}".format(plugin)
-            # XXX this event may happen during setup/teardown time
+            # XXX This event may happen during setup/teardown time
             #     which unfortunately captures our output here
-            #     which garbles our output if we use self.write_line
+            #     which garbles our output if we use self.write_line.
             self.write_line(msg)
 
     def pytest_deselected(self, items: Sequence[Item]) -> None:
@@ -511,8 +499,8 @@ class TerminalReporter:
     def pytest_runtest_logstart(
         self, nodeid: str, location: Tuple[str, Optional[int], str]
     ) -> None:
-        # ensure that the path is printed before the
-        # 1st test of a module starts running
+        # Ensure that the path is printed before the
+        # 1st test of a module starts running.
         if self.showlongtestinfo:
             line = self._locationline(nodeid, *location)
             self.write_ensure_prefix(line, "")
@@ -526,15 +514,15 @@ class TerminalReporter:
         rep = report
         res = self.config.hook.pytest_report_teststatus(
             report=rep, config=self.config
-        )  # type: Tuple[str, str, str]
+        )  # type: Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]
         category, letter, word = res
-        if isinstance(word, tuple):
-            word, markup = word
-        else:
+        if not isinstance(word, tuple):
             markup = None
+        else:
+            word, markup = word
         self._add_stats(category, [rep])
         if not letter and not word:
-            # probably passed setup/teardown
+            # Probably passed setup/teardown.
             return
         running_xdist = hasattr(rep, "node")
         if markup is None:
@@ -624,7 +612,7 @@ class TerminalReporter:
 
     @property
     def _width_of_current_line(self) -> int:
-        """Return the width of current line, using the superior implementation of py-1.6 when available"""
+        """Return the width of the current line."""
         return self._tw.width_of_current_line
 
     def pytest_collection(self) -> None:
@@ -715,21 +703,24 @@ class TerminalReporter:
             self._write_report_lines_from_hooks(lines)
 
     def _write_report_lines_from_hooks(
-        self, lines: List[Union[str, List[str]]]
+        self, lines: Sequence[Union[str, Sequence[str]]]
     ) -> None:
-        lines.reverse()
-        for line in collapse(lines):
-            self.write_line(line)
+        for line_or_lines in reversed(lines):
+            if isinstance(line_or_lines, str):
+                self.write_line(line_or_lines)
+            else:
+                for line in line_or_lines:
+                    self.write_line(line)
 
     def pytest_report_header(self, config: Config) -> List[str]:
-        line = "rootdir: %s" % config.rootdir
+        line = "rootdir: %s" % config.rootpath
 
-        if config.inifile:
-            line += ", configfile: " + config.rootdir.bestrelpath(config.inifile)
+        if config.inipath:
+            line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
 
         testpaths = config.getini("testpaths")
         if testpaths and config.args == testpaths:
-            rel_paths = [config.rootdir.bestrelpath(x) for x in testpaths]
+            rel_paths = [bestrelpath(config.rootpath, x) for x in testpaths]
             line += ", testpaths: {}".format(", ".join(rel_paths))
         result = [line]
 
@@ -759,9 +750,9 @@ class TerminalReporter:
                     rep.toterminal(self._tw)
 
     def _printcollecteditems(self, items: Sequence[Item]) -> None:
-        # to print out items and their parent collectors
+        # To print out items and their parent collectors
         # we take care to leave out Instances aka ()
-        # because later versions are going to get rid of them anyway
+        # because later versions are going to get rid of them anyway.
         if self.config.option.verbose < 0:
             if self.config.option.verbose < -1:
                 counts = {}  # type: Dict[str, int]
@@ -866,14 +857,14 @@ class TerminalReporter:
                 line += "[".join(values)
             return line
 
-        # collect_fspath comes from testid which has a "/"-normalized path
+        # collect_fspath comes from testid which has a "/"-normalized path.
 
         if fspath:
             res = mkrel(nodeid)
             if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
                 "\\", nodes.SEP
             ):
-                res += " <- " + self.startdir.bestrelpath(fspath)
+                res += " <- " + bestrelpath(self.startpath, fspath)
         else:
             res = "[location]"
         return res + " "
@@ -894,7 +885,7 @@ class TerminalReporter:
                 return ""
 
     #
-    # summaries for sessionfinish
+    # Summaries for sessionfinish.
     #
     def getreports(self, name: str):
         values = []
@@ -1115,7 +1106,7 @@ class TerminalReporter:
 
         def show_skipped(lines: List[str]) -> None:
             skipped = self.stats.get("skipped", [])  # type: List[CollectReport]
-            fskips = _folded_skips(self.startdir, skipped) if skipped else []
+            fskips = _folded_skips(self.startpath, skipped) if skipped else []
             if not fskips:
                 return
             verbose_word = skipped[0]._get_verbose_word(self.config)
@@ -1243,19 +1234,20 @@ def _get_line_with_reprcrash_message(
 
 
 def _folded_skips(
-    startdir: py.path.local, skipped: Sequence[CollectReport],
+    startpath: Path, skipped: Sequence[CollectReport],
 ) -> List[Tuple[int, str, Optional[int], str]]:
     d = {}  # type: Dict[Tuple[str, Optional[int], str], List[CollectReport]]
     for event in skipped:
         assert event.longrepr is not None
+        assert isinstance(event.longrepr, tuple), (event, event.longrepr)
         assert len(event.longrepr) == 3, (event, event.longrepr)
         fspath, lineno, reason = event.longrepr
         # For consistency, report all fspaths in relative form.
-        fspath = startdir.bestrelpath(py.path.local(fspath))
+        fspath = bestrelpath(startpath, Path(fspath))
         keywords = getattr(event, "keywords", {})
-        # folding reports with global pytestmark variable
-        # this is workaround, because for now we cannot identify the scope of a skip marker
-        # TODO: revisit after marks scope would be fixed
+        # Folding reports with global pytestmark variable.
+        # This is a workaround, because for now we cannot identify the scope of a skip marker
+        # TODO: Revisit after marks scope would be fixed.
         if (
             event.when == "setup"
             and "skip" in keywords
@@ -1296,20 +1288,19 @@ def _make_plural(count: int, noun: str) -> Tuple[int, str]:
 def _plugin_nameversions(plugininfo) -> List[str]:
     values = []  # type: List[str]
     for plugin, dist in plugininfo:
-        # gets us name and version!
+        # Gets us name and version!
         name = "{dist.project_name}-{dist.version}".format(dist=dist)
-        # questionable convenience, but it keeps things short
+        # Questionable convenience, but it keeps things short.
         if name.startswith("pytest-"):
             name = name[7:]
-        # we decided to print python package names
-        # they can have more than one plugin
+        # We decided to print python package names they can have more than one plugin.
         if name not in values:
             values.append(name)
     return values
 
 
 def format_session_duration(seconds: float) -> str:
-    """Format the given seconds in a human readable manner to show in the final summary"""
+    """Format the given seconds in a human readable manner to show in the final summary."""
     if seconds < 60:
         return "{:.2f}s".format(seconds)
     else:
index ded917b35bdba612ce7052c65263e6fcb673a472..62442de75286c145c4e64752fa594178f9e1cbab 100644 (file)
@@ -1,5 +1,4 @@
-"""
-Indirection for time functions.
+"""Indirection for time functions.
 
 We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect
 pytest runtime information (issue #185).
index 58dd659087d54a0a4cb01c25cbdc7f7a9e96795a..eb8aa9f9104ba8cfc78a0ed6e0cad78b19fbf89f 100644 (file)
@@ -1,4 +1,4 @@
-""" support for providing temporary directories to test functions.  """
+"""Support for providing temporary directories to test functions."""
 import os
 import re
 import tempfile
@@ -13,22 +13,25 @@ from .pathlib import LOCK_TIMEOUT
 from .pathlib import make_numbered_dir
 from .pathlib import make_numbered_dir_with_cleanup
 from .pathlib import Path
+from _pytest.compat import final
 from _pytest.config import Config
 from _pytest.fixtures import FixtureRequest
 from _pytest.monkeypatch import MonkeyPatch
 
 
+@final
 @attr.s
 class TempPathFactory:
     """Factory for temporary directories under the common base temp directory.
 
-    The base directory can be configured using the ``--basetemp`` option."""
+    The base directory can be configured using the ``--basetemp`` option.
+    """
 
     _given_basetemp = attr.ib(
-        type=Path,
-        # using os.path.abspath() to get absolute path instead of resolve() as it
-        # does not work the same in all platforms (see #4427)
-        # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012)
+        type=Optional[Path],
+        # Use os.path.abspath() to get absolute path instead of resolve() as it
+        # does not work the same in all platforms (see #4427).
+        # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
         # Ignore type because of https://github.com/python/mypy/issues/6172.
         converter=attr.converters.optional(
             lambda p: Path(os.path.abspath(str(p)))  # type: ignore
@@ -38,10 +41,8 @@ class TempPathFactory:
     _basetemp = attr.ib(type=Optional[Path], default=None)
 
     @classmethod
-    def from_config(cls, config) -> "TempPathFactory":
-        """
-        :param config: a pytest configuration
-        """
+    def from_config(cls, config: Config) -> "TempPathFactory":
+        """Create a factory according to pytest configuration."""
         return cls(
             given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
         )
@@ -55,7 +56,7 @@ class TempPathFactory:
         return basename
 
     def mktemp(self, basename: str, numbered: bool = True) -> Path:
-        """Creates a new temporary directory managed by the factory.
+        """Create a new temporary directory managed by the factory.
 
         :param basename:
             Directory base name, must be a relative path.
@@ -66,7 +67,7 @@ class TempPathFactory:
             means that this function will create directories named ``"foo-0"``,
             ``"foo-1"``, ``"foo-2"`` and so on.
 
-        :return:
+        :returns:
             The path to the new directory.
         """
         basename = self._ensure_relative_to_basetemp(basename)
@@ -79,7 +80,7 @@ class TempPathFactory:
         return p
 
     def getbasetemp(self) -> Path:
-        """ return base temporary directory. """
+        """Return base temporary directory."""
         if self._basetemp is not None:
             return self._basetemp
 
@@ -104,30 +105,26 @@ class TempPathFactory:
         return t
 
 
+@final
 @attr.s
 class TempdirFactory:
-    """
-    backward comptibility wrapper that implements
-    :class:``py.path.local`` for :class:``TempPathFactory``
-    """
+    """Backward comptibility wrapper that implements :class:``py.path.local``
+    for :class:``TempPathFactory``."""
 
     _tmppath_factory = attr.ib(type=TempPathFactory)
 
     def mktemp(self, basename: str, numbered: bool = True) -> py.path.local:
-        """
-        Same as :meth:`TempPathFactory.mkdir`, but returns a ``py.path.local`` object.
-        """
+        """Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object."""
         return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve())
 
     def getbasetemp(self) -> py.path.local:
-        """backward compat wrapper for ``_tmppath_factory.getbasetemp``"""
+        """Backward compat wrapper for ``_tmppath_factory.getbasetemp``."""
         return py.path.local(self._tmppath_factory.getbasetemp().resolve())
 
 
 def get_user() -> Optional[str]:
     """Return the current user name, or None if getuser() does not work
-    in the current environment (see #1010).
-    """
+    in the current environment (see #1010)."""
     import getpass
 
     try:
@@ -153,16 +150,14 @@ def pytest_configure(config: Config) -> None:
 
 @pytest.fixture(scope="session")
 def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
-    """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
-    """
+    """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session."""
     # Set dynamically by pytest_configure() above.
     return request.config._tmpdirhandler  # type: ignore
 
 
 @pytest.fixture(scope="session")
 def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
-    """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
-    """
+    """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session."""
     # Set dynamically by pytest_configure() above.
     return request.config._tmp_path_factory  # type: ignore
 
@@ -177,11 +172,11 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
 
 @pytest.fixture
 def tmpdir(tmp_path: Path) -> py.path.local:
-    """Return a temporary directory path object
-    which is unique to each test function invocation,
-    created as a sub directory of the base temporary
-    directory.  The returned object is a `py.path.local`_
-    path object.
+    """Return a temporary directory path object which is unique to each test
+    function invocation, created as a sub directory of the base temporary
+    directory.
+
+    The returned object is a `py.path.local`_ path object.
 
     .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
     """
@@ -190,15 +185,15 @@ def tmpdir(tmp_path: Path) -> py.path.local:
 
 @pytest.fixture
 def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
-    """Return a temporary directory path object
-    which is unique to each test function invocation,
-    created as a sub directory of the base temporary
-    directory.  The returned object is a :class:`pathlib.Path`
-    object.
+    """Return a temporary directory path object which is unique to each test
+    function invocation, created as a sub directory of the base temporary
+    directory.
+
+    The returned object is a :class:`pathlib.Path` object.
 
     .. note::
 
-        in python < 3.6 this is a pathlib2.Path
+        In python < 3.6 this is a pathlib2.Path.
     """
 
     return _mk_tmp(request, tmp_path_factory)
index 782a5c36962e45883af9d6e8adf0b8bdd573fcb3..09aa014c58a4d9eacafd6ef31bd0d502fb5f0758 100644 (file)
@@ -1,4 +1,4 @@
-""" discovery and running of std-library "unittest" style tests. """
+"""Discover and run std-library "unittest" style tests."""
 import sys
 import traceback
 import types
@@ -46,7 +46,7 @@ if TYPE_CHECKING:
 def pytest_pycollect_makeitem(
     collector: PyCollector, name: str, obj: object
 ) -> Optional["UnitTestCase"]:
-    # has unittest been imported and is obj a subclass of its TestCase?
+    # Has unittest been imported and is obj a subclass of its TestCase?
     try:
         ut = sys.modules["unittest"]
         # Type ignored because `ut` is an opaque module.
@@ -54,14 +54,14 @@ def pytest_pycollect_makeitem(
             return None
     except Exception:
         return None
-    # yes, so let's collect it
+    # Yes, so let's collect it.
     item = UnitTestCase.from_parent(collector, name=name, obj=obj)  # type: UnitTestCase
     return item
 
 
 class UnitTestCase(Class):
-    # marker for fixturemanger.getfixtureinfo()
-    # to declare that our children do not support funcargs
+    # Marker for fixturemanger.getfixtureinfo()
+    # to declare that our children do not support funcargs.
     nofuncargs = True
 
     def collect(self) -> Iterable[Union[Item, Collector]]:
@@ -97,7 +97,7 @@ class UnitTestCase(Class):
 
     def _inject_setup_teardown_fixtures(self, cls: type) -> None:
         """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
-        teardown functions (#517)"""
+        teardown functions (#517)."""
         class_fixture = _make_xunit_fixture(
             cls, "setUpClass", "tearDownClass", scope="class", pass_self=False
         )
@@ -141,11 +141,11 @@ def _make_xunit_fixture(
 
 class TestCaseFunction(Function):
     nofuncargs = True
-    _excinfo = None  # type: Optional[List[_pytest._code.ExceptionInfo]]
+    _excinfo = None  # type: Optional[List[_pytest._code.ExceptionInfo[BaseException]]]
     _testcase = None  # type: Optional[unittest.TestCase]
 
     def setup(self) -> None:
-        # a bound method to be called during teardown() if set (see 'runtest()')
+        # A bound method to be called during teardown() if set (see 'runtest()').
         self._explicit_tearDown = None  # type: Optional[Callable[[], None]]
         assert self.parent is not None
         self._testcase = self.parent.obj(self.name)  # type: ignore[attr-defined]
@@ -164,12 +164,12 @@ class TestCaseFunction(Function):
         pass
 
     def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None:
-        # unwrap potential exception info (see twisted trial support below)
+        # Unwrap potential exception info (see twisted trial support below).
         rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
         try:
             excinfo = _pytest._code.ExceptionInfo(rawexcinfo)  # type: ignore[arg-type]
-            # invoke the attributes to trigger storing the traceback
-            # trial causes some issue there
+            # Invoke the attributes to trigger storing the traceback
+            # trial causes some issue there.
             excinfo.value
             excinfo.traceback
         except TypeError:
@@ -242,7 +242,7 @@ class TestCaseFunction(Function):
 
     def _expecting_failure(self, test_method) -> bool:
         """Return True if the given unittest method (or the entire class) is marked
-        with @expectedFailure"""
+        with @expectedFailure."""
         expecting_failure_method = getattr(
             test_method, "__unittest_expecting_failure__", False
         )
@@ -256,30 +256,32 @@ class TestCaseFunction(Function):
 
         maybe_wrap_pytest_function_for_tracing(self)
 
-        # let the unittest framework handle async functions
+        # Let the unittest framework handle async functions.
         if is_async_function(self.obj):
             # Type ignored because self acts as the TestResult, but is not actually one.
             self._testcase(result=self)  # type: ignore[arg-type]
         else:
-            # when --pdb is given, we want to postpone calling tearDown() otherwise
+            # When --pdb is given, we want to postpone calling tearDown() otherwise
             # when entering the pdb prompt, tearDown() would have probably cleaned up
-            # instance variables, which makes it difficult to debug
-            # arguably we could always postpone tearDown(), but this changes the moment where the
+            # instance variables, which makes it difficult to debug.
+            # Arguably we could always postpone tearDown(), but this changes the moment where the
             # TestCase instance interacts with the results object, so better to only do it
-            # when absolutely needed
+            # when absolutely needed.
             if self.config.getoption("usepdb") and not _is_skipped(self.obj):
                 self._explicit_tearDown = self._testcase.tearDown
                 setattr(self._testcase, "tearDown", lambda *args: None)
 
-            # we need to update the actual bound method with self.obj, because
-            # wrap_pytest_function_for_tracing replaces self.obj by a wrapper
+            # We need to update the actual bound method with self.obj, because
+            # wrap_pytest_function_for_tracing replaces self.obj by a wrapper.
             setattr(self._testcase, self.name, self.obj)
             try:
                 self._testcase(result=self)  # type: ignore[arg-type]
             finally:
                 delattr(self._testcase, self.name)
 
-    def _prunetraceback(self, excinfo: _pytest._code.ExceptionInfo) -> None:
+    def _prunetraceback(
+        self, excinfo: _pytest._code.ExceptionInfo[BaseException]
+    ) -> None:
         Function._prunetraceback(self, excinfo)
         traceback = excinfo.traceback.filter(
             lambda x: not x.frame.f_globals.get("__unittest")
@@ -305,14 +307,14 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
         and isinstance(call.excinfo.value, unittest.SkipTest)  # type: ignore[attr-defined]
     ):
         excinfo = call.excinfo
-        # let's substitute the excinfo with a pytest.skip one
+        # Let's substitute the excinfo with a pytest.skip one.
         call2 = CallInfo[None].from_call(
             lambda: pytest.skip(str(excinfo.value)), call.when
         )
         call.excinfo = call2.excinfo
 
 
-# twisted trial support
+# Twisted trial support.
 
 
 @hookimpl(hookwrapper=True)
@@ -356,5 +358,5 @@ def check_testcase_implements_trial_reporter(done: List[int] = []) -> None:
 
 
 def _is_skipped(obj) -> bool:
-    """Return True if the given object has been marked with @unittest.skip"""
+    """Return True if the given object has been marked with @unittest.skip."""
     return bool(getattr(obj, "__unittest_skip__", False))
index 6f3b88da8b87807ded8afccfc5958ec043febc81..52e4d2b14cb7719f7ec2fa54411f4fe27b8aaca4 100644 (file)
@@ -4,6 +4,7 @@ from typing import TypeVar
 
 import attr
 
+from _pytest.compat import final
 from _pytest.compat import TYPE_CHECKING
 
 if TYPE_CHECKING:
@@ -16,36 +17,42 @@ class PytestWarning(UserWarning):
     __module__ = "pytest"
 
 
+@final
 class PytestAssertRewriteWarning(PytestWarning):
     """Warning emitted by the pytest assert rewrite module."""
 
     __module__ = "pytest"
 
 
+@final
 class PytestCacheWarning(PytestWarning):
     """Warning emitted by the cache plugin in various situations."""
 
     __module__ = "pytest"
 
 
+@final
 class PytestConfigWarning(PytestWarning):
     """Warning emitted for configuration issues."""
 
     __module__ = "pytest"
 
 
+@final
 class PytestCollectionWarning(PytestWarning):
     """Warning emitted when pytest is not able to collect a file or symbol in a module."""
 
     __module__ = "pytest"
 
 
+@final
 class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
     """Warning class for features that will be removed in a future version."""
 
     __module__ = "pytest"
 
 
+@final
 class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
     """Warning category used to denote experiments in pytest.
 
@@ -64,6 +71,7 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
         )
 
 
+@final
 class PytestUnhandledCoroutineWarning(PytestWarning):
     """Warning emitted for an unhandled coroutine.
 
@@ -75,6 +83,7 @@ class PytestUnhandledCoroutineWarning(PytestWarning):
     __module__ = "pytest"
 
 
+@final
 class PytestUnknownMarkWarning(PytestWarning):
     """Warning emitted on use of unknown markers.
 
@@ -87,6 +96,7 @@ class PytestUnknownMarkWarning(PytestWarning):
 _W = TypeVar("_W", bound=PytestWarning)
 
 
+@final
 @attr.s
 class UnformattedWarning(Generic[_W]):
     """A warning meant to be formatted during runtime.
@@ -99,7 +109,7 @@ class UnformattedWarning(Generic[_W]):
     template = attr.ib(type=str)
 
     def format(self, **kwargs: Any) -> _W:
-        """Returns an instance of the warning category, formatted with given kwargs"""
+        """Return an instance of the warning category, formatted with given kwargs."""
         return self.category(self.template.format(**kwargs))
 
 
index 3a8f2d8b33ff114f4904a872d391439c7913ae4e..950d0bb385903a836cb82b7d88ab3aab2d925d33 100644 (file)
@@ -1,77 +1,22 @@
-import re
 import sys
 import warnings
 from contextlib import contextmanager
-from functools import lru_cache
 from typing import Generator
 from typing import Optional
-from typing import Tuple
 
 import pytest
 from _pytest.compat import TYPE_CHECKING
+from _pytest.config import apply_warning_filters
 from _pytest.config import Config
-from _pytest.config.argparsing import Parser
+from _pytest.config import parse_warning_filter
 from _pytest.main import Session
 from _pytest.nodes import Item
 from _pytest.terminal import TerminalReporter
 
 if TYPE_CHECKING:
-    from typing import Type
     from typing_extensions import Literal
 
 
-@lru_cache(maxsize=50)
-def _parse_filter(
-    arg: str, *, escape: bool
-) -> "Tuple[str, str, Type[Warning], str, int]":
-    """Parse a warnings filter string.
-
-    This is copied from warnings._setoption, but does not apply the filter,
-    only parses it, and makes the escaping optional.
-    """
-    parts = arg.split(":")
-    if len(parts) > 5:
-        raise warnings._OptionError("too many fields (max 5): {!r}".format(arg))
-    while len(parts) < 5:
-        parts.append("")
-    action_, message, category_, module, lineno_ = [s.strip() for s in parts]
-    action = warnings._getaction(action_)  # type: str # type: ignore[attr-defined]
-    category = warnings._getcategory(
-        category_
-    )  # type: Type[Warning] # type: ignore[attr-defined]
-    if message and escape:
-        message = re.escape(message)
-    if module and escape:
-        module = re.escape(module) + r"\Z"
-    if lineno_:
-        try:
-            lineno = int(lineno_)
-            if lineno < 0:
-                raise ValueError
-        except (ValueError, OverflowError) as e:
-            raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e
-    else:
-        lineno = 0
-    return (action, message, category, module, lineno)
-
-
-def pytest_addoption(parser: Parser) -> None:
-    group = parser.getgroup("pytest-warnings")
-    group.addoption(
-        "-W",
-        "--pythonwarnings",
-        action="append",
-        help="set which warnings to report, see -W option of python itself.",
-    )
-    parser.addini(
-        "filterwarnings",
-        type="linelist",
-        help="Each line specifies a pattern for "
-        "warnings.filterwarnings. "
-        "Processed after -W/--pythonwarnings.",
-    )
-
-
 def pytest_configure(config: Config) -> None:
     config.addinivalue_line(
         "markers",
@@ -87,39 +32,31 @@ def catch_warnings_for_item(
     when: "Literal['config', 'collect', 'runtest']",
     item: Optional[Item],
 ) -> Generator[None, None, None]:
-    """
-    Context manager that catches warnings generated in the contained execution block.
+    """Context manager that catches warnings generated in the contained execution block.
 
     ``item`` can be None if we are not in the context of an item execution.
 
     Each warning captured triggers the ``pytest_warning_recorded`` hook.
     """
-    cmdline_filters = config.getoption("pythonwarnings") or []
-    inifilters = config.getini("filterwarnings")
+    config_filters = config.getini("filterwarnings")
+    cmdline_filters = config.known_args_namespace.pythonwarnings or []
     with warnings.catch_warnings(record=True) as log:
         # mypy can't infer that record=True means log is not None; help it.
         assert log is not None
 
         if not sys.warnoptions:
-            # if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908)
+            # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908).
             warnings.filterwarnings("always", category=DeprecationWarning)
             warnings.filterwarnings("always", category=PendingDeprecationWarning)
 
-        warnings.filterwarnings("error", category=pytest.PytestDeprecationWarning)
-
-        # filters should have this precedence: mark, cmdline options, ini
-        # filters should be applied in the inverse order of precedence
-        for arg in inifilters:
-            warnings.filterwarnings(*_parse_filter(arg, escape=False))
-
-        for arg in cmdline_filters:
-            warnings.filterwarnings(*_parse_filter(arg, escape=True))
+        apply_warning_filters(config_filters, cmdline_filters)
 
+        # apply filters from "filterwarnings" marks
         nodeid = "" if item is None else item.nodeid
         if item is not None:
             for mark in item.iter_markers(name="filterwarnings"):
                 for arg in mark.args:
-                    warnings.filterwarnings(*_parse_filter(arg, escape=False))
+                    warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
 
         yield
 
@@ -192,28 +129,11 @@ def pytest_sessionfinish(session: Session) -> Generator[None, None, None]:
         yield
 
 
-def _issue_warning_captured(warning: Warning, hook, stacklevel: int) -> None:
-    """
-    This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
-    at this point the actual options might not have been set, so we manually trigger the pytest_warning_recorded
-    hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891.
-
-    :param warning: the warning instance.
-    :param hook: the hook caller
-    :param stacklevel: stacklevel forwarded to warnings.warn
-    """
-    with warnings.catch_warnings(record=True) as records:
-        warnings.simplefilter("always", type(warning))
-        warnings.warn(warning, stacklevel=stacklevel)
-    frame = sys._getframe(stacklevel - 1)
-    location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
-    hook.pytest_warning_captured.call_historic(
-        kwargs=dict(
-            warning_message=records[0], when="config", item=None, location=location
-        )
-    )
-    hook.pytest_warning_recorded.call_historic(
-        kwargs=dict(
-            warning_message=records[0], when="config", nodeid="", location=location
-        )
-    )
+@pytest.hookimpl(hookwrapper=True)
+def pytest_load_initial_conftests(
+    early_config: "Config",
+) -> Generator[None, None, None]:
+    with catch_warnings_for_item(
+        config=early_config, ihook=early_config.hook, when="config", item=None
+    ):
+        yield
index 64d6d1f23ee47d0d081fe4b57eb9cfcea49f3f95..c4c2819187722c02ed0a375c24c57e1f75979235 100644 (file)
@@ -1,7 +1,5 @@
 # PYTHON_ARGCOMPLETE_OK
-"""
-pytest: unit and functional testing with Python.
-"""
+"""pytest: unit and functional testing with Python."""
 from . import collect
 from _pytest import __version__
 from _pytest.assertion import register_assert_rewrite
index 25b1e45b89d593b8bb2fef941b3917f7ab8bea2c..b170152937b38cda35d4563241288eb3fc27edd0 100644 (file)
@@ -1,6 +1,4 @@
-"""
-pytest entry point
-"""
+"""The pytest entry point."""
 import pytest
 
 if __name__ == "__main__":
index 55b4b9b359cc8d40dae4ea1aa2006ba86d829bbc..2edf4470f4d50d5eeec67d6945c46be17a298e80 100644 (file)
@@ -1,10 +1,11 @@
 import sys
+import warnings
 from types import ModuleType
 from typing import Any
 from typing import List
 
 import pytest
-
+from _pytest.deprecated import PYTEST_COLLECT_MODULE
 
 COLLECT_FAKEMODULE_ATTRIBUTES = [
     "Collector",
@@ -31,8 +32,7 @@ class FakeCollectModule(ModuleType):
     def __getattr__(self, name: str) -> Any:
         if name not in self.__all__:
             raise AttributeError(name)
-        # Uncomment this after 6.0 release (#7361)
-        # warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2)
+        warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2)
         return getattr(pytest, name)
 
 
index 2a386e2c6e481a1518ab0123b3a30dd992f7d834..039d8dad969f276b7b097e8dac658a7631e6f19f 100644 (file)
@@ -70,7 +70,7 @@ class TestGeneralUsage:
     def test_file_not_found(self, testdir):
         result = testdir.runpytest("asd")
         assert result.ret != 0
-        result.stderr.fnmatch_lines(["ERROR: file not found*asd"])
+        result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"])
 
     def test_file_not_found_unconfigure_issue143(self, testdir):
         testdir.makeconftest(
@@ -83,7 +83,7 @@ class TestGeneralUsage:
         )
         result = testdir.runpytest("-s", "asd")
         assert result.ret == ExitCode.USAGE_ERROR
-        result.stderr.fnmatch_lines(["ERROR: file not found*asd"])
+        result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"])
         result.stdout.fnmatch_lines(["*---configure", "*---unconfigure"])
 
     def test_config_preparse_plugin_option(self, testdir):
@@ -223,13 +223,12 @@ class TestGeneralUsage:
             "E   {}: No module named 'qwerty'".format(exc_name),
         ]
 
-    @pytest.mark.filterwarnings("ignore::pytest.PytestDeprecationWarning")
     def test_early_skip(self, testdir):
         testdir.mkdir("xyz")
         testdir.makeconftest(
             """
             import pytest
-            def pytest_collect_directory():
+            def pytest_collect_file():
                 pytest.skip("early")
         """
         )
@@ -403,15 +402,12 @@ class TestGeneralUsage:
         result.stdout.fnmatch_lines(["pytest_sessionfinish_called"])
         assert result.ret == ExitCode.USAGE_ERROR
 
-    @pytest.mark.usefixtures("recwarn")
     def test_namespace_import_doesnt_confuse_import_hook(self, testdir):
-        """
-        Ref #383. Python 3.3's namespace package messed with our import hooks
+        """Ref #383.
+
+        Python 3.3's namespace package messed with our import hooks.
         Importing a module that didn't exist, even if the ImportError was
         gracefully handled, would make our test crash.
-
-        Use recwarn here to silence this warning in Python 2.7:
-            ImportWarning: Not importing directory '...\not_a_package': missing __init__.py
         """
         testdir.mkdir("not_a_package")
         p = testdir.makepyfile(
@@ -457,10 +453,8 @@ class TestGeneralUsage:
         )
 
     def test_plugins_given_as_strings(self, tmpdir, monkeypatch, _sys_snapshot):
-        """test that str values passed to main() as `plugins` arg
-        are interpreted as module names to be imported and registered.
-        #855.
-        """
+        """Test that str values passed to main() as `plugins` arg are
+        interpreted as module names to be imported and registered (#855)."""
         with pytest.raises(ImportError) as excinfo:
             pytest.main([str(tmpdir)], plugins=["invalid.module"])
         assert "invalid" in str(excinfo.value)
@@ -591,7 +585,7 @@ class TestInvocationVariants:
         ):
             pytest.main("-h")  # type: ignore[arg-type]
 
-    def test_invoke_with_path(self, tmpdir, capsys):
+    def test_invoke_with_path(self, tmpdir: py.path.local, capsys) -> None:
         retcode = pytest.main(tmpdir)
         assert retcode == ExitCode.NO_TESTS_COLLECTED
         out, err = capsys.readouterr()
@@ -664,8 +658,7 @@ class TestInvocationVariants:
         result.stderr.fnmatch_lines(["*not*found*test_missing*"])
 
     def test_cmdline_python_namespace_package(self, testdir, monkeypatch):
-        """
-        test --pyargs option with namespace packages (#1567)
+        """Test --pyargs option with namespace packages (#1567).
 
         Ref: https://packaging.python.org/guides/packaging-namespace-packages/
         """
@@ -797,7 +790,7 @@ class TestInvocationVariants:
     def test_cmdline_python_package_not_exists(self, testdir):
         result = testdir.runpytest("--pyargs", "tpkgwhatv")
         assert result.ret
-        result.stderr.fnmatch_lines(["ERROR*file*or*package*not*found*"])
+        result.stderr.fnmatch_lines(["ERROR*module*or*package*not*found*"])
 
     @pytest.mark.xfail(reason="decide: feature or bug")
     def test_noclass_discovery_if_not_testcase(self, testdir):
@@ -1011,9 +1004,7 @@ def test_pytest_plugins_as_module(testdir):
 
 
 def test_deferred_hook_checking(testdir):
-    """
-    Check hooks as late as possible (#1821).
-    """
+    """Check hooks as late as possible (#1821)."""
     testdir.syspathinsert()
     testdir.makepyfile(
         **{
@@ -1089,8 +1080,7 @@ def test_fixture_values_leak(testdir):
 
 
 def test_fixture_order_respects_scope(testdir):
-    """Ensure that fixtures are created according to scope order, regression test for #2405
-    """
+    """Ensure that fixtures are created according to scope order (#2405)."""
     testdir.makepyfile(
         """
         import pytest
@@ -1115,7 +1105,8 @@ def test_fixture_order_respects_scope(testdir):
 
 
 def test_frame_leak_on_failing_test(testdir):
-    """pytest would leak garbage referencing the frames of tests that failed that could never be reclaimed (#2798)
+    """Pytest would leak garbage referencing the frames of tests that failed
+    that could never be reclaimed (#2798).
 
     Unfortunately it was not possible to remove the actual circles because most of them
     are made of traceback objects which cannot be weakly referenced. Those objects at least
index 8eab319ead8e5671a452f33337e9b2663f7d82c1..5754977ddc7ed21c08d8ad99d2776082f9b7c3b1 100644 (file)
@@ -466,7 +466,7 @@ class TestFormattedExcinfo:
         assert lines[1] == "        pass"
 
     def test_repr_source_excinfo(self) -> None:
-        """ check if indentation is right """
+        """Check if indentation is right."""
         try:
 
             def f():
@@ -1352,6 +1352,19 @@ raise ValueError()
         )
         assert out == expected_out
 
+    def test_exec_type_error_filter(self, importasmod):
+        """See #7742"""
+        mod = importasmod(
+            """\
+            def f():
+                exec("a = 1", {}, [])
+            """
+        )
+        with pytest.raises(TypeError) as excinfo:
+            mod.f()
+        # previously crashed with `AttributeError: list has no attribute get`
+        excinfo.traceback.filter()
+
 
 @pytest.mark.parametrize("style", ["short", "long"])
 @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])
index 4222eb172f213aa8584495341205691fd8252e74..d12c55d935b4054872eccb4ba2ad3e6235999d6a 100644 (file)
@@ -238,7 +238,7 @@ def test_getline_finally() -> None:
             c(1)  # type: ignore
         finally:
             if teardown:
-                teardown()
+                teardown()  # type: ignore[unreachable]
     source = excinfo.traceback[-1].statement
     assert str(source).strip() == "c(1)  # type: ignore"
 
index f4de3b5d9c5b0303eb1317284594f36606724401..eb5d527f52bdc2a215d19e63544bff0c2a93097d 100644 (file)
@@ -1,34 +1,11 @@
-import copy
-import inspect
+import warnings
 from unittest import mock
 
 import pytest
 from _pytest import deprecated
-from _pytest import nodes
-from _pytest.config import Config
+from _pytest.pytester import Testdir
 
 
-@pytest.mark.filterwarnings("default")
-def test_resultlog_is_deprecated(testdir):
-    result = testdir.runpytest("--help")
-    result.stdout.fnmatch_lines(["*DEPRECATED path for machine-readable result log*"])
-
-    testdir.makepyfile(
-        """
-        def test():
-            pass
-    """
-    )
-    result = testdir.runpytest("--result-log=%s" % testdir.tmpdir.join("result.log"))
-    result.stdout.fnmatch_lines(
-        [
-            "*--result-log is deprecated, please try the new pytest-reportlog plugin.",
-            "*See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information*",
-        ]
-    )
-
-
-@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361")
 @pytest.mark.parametrize("attribute", pytest.collect.__all__)  # type: ignore
 # false positive due to dynamic attribute
 def test_pytest_collect_module_deprecated(attribute):
@@ -36,35 +13,6 @@ def test_pytest_collect_module_deprecated(attribute):
         getattr(pytest.collect, attribute)
 
 
-def test_terminal_reporter_writer_attr(pytestconfig: Config) -> None:
-    """Check that TerminalReporter._tw is also available as 'writer' (#2984)
-    This attribute has been deprecated in 5.4.
-    """
-    try:
-        import xdist  # noqa
-
-        pytest.skip("xdist workers disable the terminal reporter plugin")
-    except ImportError:
-        pass
-    terminal_reporter = pytestconfig.pluginmanager.get_plugin("terminalreporter")
-    original_tw = terminal_reporter._tw
-
-    with pytest.warns(pytest.PytestDeprecationWarning) as cw:
-        assert terminal_reporter.writer is original_tw
-    assert len(cw) == 1
-    assert cw[0].filename == __file__
-
-    new_tw = copy.copy(original_tw)
-    with pytest.warns(pytest.PytestDeprecationWarning) as cw:
-        terminal_reporter.writer = new_tw
-        try:
-            assert terminal_reporter._tw is new_tw
-        finally:
-            terminal_reporter.writer = original_tw
-    assert len(cw) == 2
-    assert cw[0].filename == cw[1].filename == __file__
-
-
 @pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS))
 @pytest.mark.filterwarnings("default")
 def test_external_plugins_integrated(testdir, plugin):
@@ -75,50 +23,6 @@ def test_external_plugins_integrated(testdir, plugin):
         testdir.parseconfig("-p", plugin)
 
 
-@pytest.mark.parametrize("junit_family", [None, "legacy", "xunit2"])
-def test_warn_about_imminent_junit_family_default_change(testdir, junit_family):
-    """Show a warning if junit_family is not defined and --junitxml is used (#6179)"""
-    testdir.makepyfile(
-        """
-        def test_foo():
-            pass
-    """
-    )
-    if junit_family:
-        testdir.makeini(
-            """
-            [pytest]
-            junit_family={junit_family}
-        """.format(
-                junit_family=junit_family
-            )
-        )
-
-    result = testdir.runpytest("--junit-xml=foo.xml")
-    warning_msg = (
-        "*PytestDeprecationWarning: The 'junit_family' default value will change*"
-    )
-    if junit_family:
-        result.stdout.no_fnmatch_line(warning_msg)
-    else:
-        result.stdout.fnmatch_lines([warning_msg])
-
-
-def test_node_direct_ctor_warning() -> None:
-    class MockConfig:
-        pass
-
-    ms = MockConfig()
-    with pytest.warns(
-        DeprecationWarning,
-        match="Direct construction of .* has been deprecated, please use .*.from_parent.*",
-    ) as w:
-        nodes.Node(name="test", config=ms, session=ms, nodeid="None")  # type: ignore
-    assert w[0].lineno == inspect.currentframe().f_lineno - 1  # type: ignore
-    assert w[0].filename == __file__
-
-
-@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361")
 def test_fillfuncargs_is_deprecated() -> None:
     with pytest.warns(
         pytest.PytestDeprecationWarning,
@@ -127,7 +31,6 @@ def test_fillfuncargs_is_deprecated() -> None:
         pytest._fillfuncargs(mock.Mock())
 
 
-@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361")
 def test_minus_k_dash_is_deprecated(testdir) -> None:
     threepass = testdir.makepyfile(
         test_threepass="""
@@ -140,7 +43,6 @@ def test_minus_k_dash_is_deprecated(testdir) -> None:
     result.stdout.fnmatch_lines(["*The `-k '-expr'` syntax*deprecated*"])
 
 
-@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361")
 def test_minus_k_colon_is_deprecated(testdir) -> None:
     threepass = testdir.makepyfile(
         test_threepass="""
@@ -151,3 +53,28 @@ def test_minus_k_colon_is_deprecated(testdir) -> None:
     )
     result = testdir.runpytest("-k", "test_two:", threepass)
     result.stdout.fnmatch_lines(["*The `-k 'expr:'` syntax*deprecated*"])
+
+
+def test_fscollector_gethookproxy_isinitpath(testdir: Testdir) -> None:
+    module = testdir.getmodulecol(
+        """
+        def test_foo(): pass
+        """,
+        withinit=True,
+    )
+    assert isinstance(module, pytest.Module)
+    package = module.parent
+    assert isinstance(package, pytest.Package)
+
+    with pytest.warns(pytest.PytestDeprecationWarning, match="gethookproxy"):
+        package.gethookproxy(testdir.tmpdir)
+
+    with pytest.warns(pytest.PytestDeprecationWarning, match="isinitpath"):
+        package.isinitpath(testdir.tmpdir)
+
+    # The methods on Session are *not* deprecated.
+    session = module.session
+    with warnings.catch_warnings(record=True) as rec:
+        session.gethookproxy(testdir.tmpdir)
+        session.isinitpath(testdir.tmpdir)
+    assert len(rec) == 0
diff --git a/testing/example_scripts/unittest/test_unittest_plain_async.py b/testing/example_scripts/unittest/test_unittest_plain_async.py
new file mode 100644 (file)
index 0000000..78dfece
--- /dev/null
@@ -0,0 +1,6 @@
+import unittest
+
+
+class Test(unittest.TestCase):
+    async def test_foo(self):
+        assert False
index b53eb09f53b0adaf25d800555c318e6b6257c242..998df7b1ca727833356fff725850f33c97d48a8a 100644 (file)
@@ -1,6 +1,4 @@
-"""
-Generates an executable with pytest runner embedded using PyInstaller.
-"""
+"""Generate an executable with pytest runner embedded using PyInstaller."""
 if __name__ == "__main__":
     import pytest
     import subprocess
index f18d7087dc570e06117f319a7406196f30b30a23..ffd51bcad7a089955253b7e628d3ebda8147366d 100644 (file)
@@ -269,10 +269,10 @@ def test_caplog_captures_despite_exception(testdir):
 
 
 def test_log_report_captures_according_to_config_option_upon_failure(testdir):
-    """ Test that upon failure:
-    (1) `caplog` succeeded to capture the DEBUG message and assert on it => No `Exception` is raised
-    (2) The `DEBUG` message does NOT appear in the `Captured log call` report
-    (3) The stdout, `INFO`, and `WARNING` messages DO appear in the test reports due to `--log-level=INFO`
+    """Test that upon failure:
+    (1) `caplog` succeeded to capture the DEBUG message and assert on it => No `Exception` is raised.
+    (2) The `DEBUG` message does NOT appear in the `Captured log call` report.
+    (3) The stdout, `INFO`, and `WARNING` messages DO appear in the test reports due to `--log-level=INFO`.
     """
     testdir.makepyfile(
         """
index 32224325884ea102e0b114da82ef3637f9b6862a..7590b57628935ead3bb8e92c2eb4ecbf4d8e6458 100644 (file)
@@ -899,7 +899,7 @@ def test_collection_collect_only_live_logging(testdir, verbose):
         expected_lines.extend(
             [
                 "*test_collection_collect_only_live_logging.py::test_simple*",
-                "no tests ran in [0-1].[0-9][0-9]s",
+                "no tests ran in [0-9].[0-9][0-9]s",
             ]
         )
     elif verbose == "-qq":
@@ -1066,10 +1066,8 @@ def test_log_set_path(testdir):
 
 
 def test_colored_captured_log(testdir):
-    """
-    Test that the level names of captured log messages of a failing test are
-    colored.
-    """
+    """Test that the level names of captured log messages of a failing test
+    are colored."""
     testdir.makepyfile(
         """
         import logging
@@ -1092,9 +1090,7 @@ def test_colored_captured_log(testdir):
 
 
 def test_colored_ansi_esc_caplogtext(testdir):
-    """
-    Make sure that caplog.text does not contain ANSI escape sequences.
-    """
+    """Make sure that caplog.text does not contain ANSI escape sequences."""
     testdir.makepyfile(
         """
         import logging
@@ -1111,8 +1107,7 @@ def test_colored_ansi_esc_caplogtext(testdir):
 
 
 def test_logging_emit_error(testdir: Testdir) -> None:
-    """
-    An exception raised during emit() should fail the test.
+    """An exception raised during emit() should fail the test.
 
     The default behavior of logging is to print "Logging error"
     to stderr with the call stack and some extra details.
@@ -1138,10 +1133,8 @@ def test_logging_emit_error(testdir: Testdir) -> None:
 
 
 def test_logging_emit_error_supressed(testdir: Testdir) -> None:
-    """
-    If logging is configured to silently ignore errors, pytest
-    doesn't propagate errors either.
-    """
+    """If logging is configured to silently ignore errors, pytest
+    doesn't propagate errors either."""
     testdir.makepyfile(
         """
         import logging
diff --git a/testing/plugins_integration/.gitignore b/testing/plugins_integration/.gitignore
new file mode 100644 (file)
index 0000000..d934447
--- /dev/null
@@ -0,0 +1,2 @@
+*.html
+assets/
diff --git a/testing/plugins_integration/README.rst b/testing/plugins_integration/README.rst
new file mode 100644 (file)
index 0000000..8f027c3
--- /dev/null
@@ -0,0 +1,13 @@
+This folder contains tests and support files for smoke testing popular plugins against the current pytest version.
+
+The objective is to gauge if any intentional or unintentional changes in pytest break plugins.
+
+As a rule of thumb, we should add plugins here:
+
+1. That are used at large. This might be subjective in some cases, but if answer is yes to
+   the question: *if a new release of pytest causes pytest-X to break, will this break a ton of test suites out there?*.
+2. That don't have large external dependencies: such as external services.
+
+Besides adding the plugin as dependency, we should also add a quick test which uses some
+minimal part of the plugin, a smoke test. Also consider reusing one of the existing tests if that's
+possible.
diff --git a/testing/plugins_integration/bdd_wallet.feature b/testing/plugins_integration/bdd_wallet.feature
new file mode 100644 (file)
index 0000000..e404c49
--- /dev/null
@@ -0,0 +1,9 @@
+Feature: Buy things with apple
+
+    Scenario: Buy fruits
+        Given A wallet with 50
+
+        When I buy some apples for 1
+        And I buy some bananas for 2
+
+        Then I have 47 left
diff --git a/testing/plugins_integration/bdd_wallet.py b/testing/plugins_integration/bdd_wallet.py
new file mode 100644 (file)
index 0000000..35927ea
--- /dev/null
@@ -0,0 +1,39 @@
+from pytest_bdd import given
+from pytest_bdd import scenario
+from pytest_bdd import then
+from pytest_bdd import when
+
+import pytest
+
+
+@scenario("bdd_wallet.feature", "Buy fruits")
+def test_publish():
+    pass
+
+
+@pytest.fixture
+def wallet():
+    class Wallet:
+        amount = 0
+
+    return Wallet()
+
+
+@given("A wallet with 50")
+def fill_wallet(wallet):
+    wallet.amount = 50
+
+
+@when("I buy some apples for 1")
+def buy_apples(wallet):
+    wallet.amount -= 1
+
+
+@when("I buy some bananas for 2")
+def buy_bananas(wallet):
+    wallet.amount -= 2
+
+
+@then("I have 47 left")
+def check(wallet):
+    assert wallet.amount == 47
diff --git a/testing/plugins_integration/django_settings.py b/testing/plugins_integration/django_settings.py
new file mode 100644 (file)
index 0000000..0715f47
--- /dev/null
@@ -0,0 +1 @@
+SECRET_KEY = "mysecret"
diff --git a/testing/plugins_integration/pytest.ini b/testing/plugins_integration/pytest.ini
new file mode 100644 (file)
index 0000000..f6c77b0
--- /dev/null
@@ -0,0 +1,4 @@
+[pytest]
+addopts = --strict-markers
+filterwarnings =
+    error::pytest.PytestWarning
diff --git a/testing/plugins_integration/pytest_anyio_integration.py b/testing/plugins_integration/pytest_anyio_integration.py
new file mode 100644 (file)
index 0000000..65c2f59
--- /dev/null
@@ -0,0 +1,8 @@
+import anyio
+
+import pytest
+
+
+@pytest.mark.anyio
+async def test_sleep():
+    await anyio.sleep(0)
diff --git a/testing/plugins_integration/pytest_asyncio_integration.py b/testing/plugins_integration/pytest_asyncio_integration.py
new file mode 100644 (file)
index 0000000..5d2a3fa
--- /dev/null
@@ -0,0 +1,8 @@
+import asyncio
+
+import pytest
+
+
+@pytest.mark.asyncio
+async def test_sleep():
+    await asyncio.sleep(0)
diff --git a/testing/plugins_integration/pytest_mock_integration.py b/testing/plugins_integration/pytest_mock_integration.py
new file mode 100644 (file)
index 0000000..740469d
--- /dev/null
@@ -0,0 +1,2 @@
+def test_mocker(mocker):
+    mocker.MagicMock()
diff --git a/testing/plugins_integration/pytest_trio_integration.py b/testing/plugins_integration/pytest_trio_integration.py
new file mode 100644 (file)
index 0000000..199f785
--- /dev/null
@@ -0,0 +1,8 @@
+import trio
+
+import pytest
+
+
+@pytest.mark.trio
+async def test_sleep():
+    await trio.sleep(0)
diff --git a/testing/plugins_integration/pytest_twisted_integration.py b/testing/plugins_integration/pytest_twisted_integration.py
new file mode 100644 (file)
index 0000000..94748d0
--- /dev/null
@@ -0,0 +1,18 @@
+import pytest_twisted
+from twisted.internet.task import deferLater
+
+
+def sleep():
+    import twisted.internet.reactor
+
+    return deferLater(clock=twisted.internet.reactor, delay=0)
+
+
+@pytest_twisted.inlineCallbacks
+def test_inlineCallbacks():
+    yield sleep()
+
+
+@pytest_twisted.ensureDeferred
+async def test_inlineCallbacks_async():
+    await sleep()
diff --git a/testing/plugins_integration/simple_integration.py b/testing/plugins_integration/simple_integration.py
new file mode 100644 (file)
index 0000000..20b2fc4
--- /dev/null
@@ -0,0 +1,10 @@
+import pytest
+
+
+def test_foo():
+    assert True
+
+
+@pytest.mark.parametrize("i", range(3))
+def test_bar(i):
+    assert True
index db67fe5aa7f59a5624acb93bd18b62496a6cd12b..194423dc3b05775efc6e4a28da5293ccd8d3dc25 100644 (file)
@@ -488,9 +488,7 @@ class TestApprox:
         ],
     )
     def test_comparison_operator_type_error(self, op):
-        """
-        pytest.approx should raise TypeError for operators other than == and != (#2003).
-        """
+        """pytest.approx should raise TypeError for operators other than == and != (#2003)."""
         with pytest.raises(TypeError):
             op(1, approx(1, rel=1e-6, abs=1e-12))
 
index f64a1462971b7d2d9a9a2e52ccf5412871bbae97..0129403986084ca8406e02f449e460ea38fe72d3 100644 (file)
@@ -843,15 +843,6 @@ class TestConftestCustomization:
         result = testdir.runpytest("--collect-only")
         result.stdout.fnmatch_lines(["*MyFunction*some*"])
 
-    def test_makeitem_non_underscore(self, testdir, monkeypatch):
-        modcol = testdir.getmodulecol("def _hello(): pass")
-        values = []
-        monkeypatch.setattr(
-            pytest.Module, "_makeitem", lambda self, name, obj: values.append(name)
-        )
-        values = modcol.collect()
-        assert "_hello" not in values
-
     def test_issue2369_collect_module_fileext(self, testdir):
         """Ensure we can collect files with weird file extensions as Python
         modules (#2369)"""
@@ -885,6 +876,34 @@ class TestConftestCustomization:
         result = testdir.runpytest_subprocess()
         result.stdout.fnmatch_lines(["*1 passed*"])
 
+    def test_early_ignored_attributes(self, testdir: Testdir) -> None:
+        """Builtin attributes should be ignored early on, even if
+        configuration would otherwise allow them.
+
+        This tests a performance optimization, not correctness, really,
+        although it tests PytestCollectionWarning is not raised, while
+        it would have been raised otherwise.
+        """
+        testdir.makeini(
+            """
+            [pytest]
+            python_classes=*
+            python_functions=*
+        """
+        )
+        testdir.makepyfile(
+            """
+            class TestEmpty:
+                pass
+            test_empty = TestEmpty()
+            def test_real():
+                pass
+        """
+        )
+        items, rec = testdir.inline_genitems()
+        assert rec.ret == 0
+        assert len(items) == 1
+
 
 def test_setup_only_available_in_subdir(testdir):
     sub1 = testdir.mkpydir("sub1")
@@ -984,8 +1003,7 @@ class TestTracebackCutting:
         result.stdout.fnmatch_lines([">*asd*", "E*NameError*"])
 
     def test_traceback_filter_error_during_fixture_collection(self, testdir):
-        """integration test for issue #995.
-        """
+        """Integration test for issue #995."""
         testdir.makepyfile(
             """
             import pytest
@@ -1011,14 +1029,15 @@ class TestTracebackCutting:
         result.stdout.fnmatch_lines(["*ValueError: fail me*", "* 1 error in *"])
 
     def test_filter_traceback_generated_code(self) -> None:
-        """test that filter_traceback() works with the fact that
+        """Test that filter_traceback() works with the fact that
         _pytest._code.code.Code.path attribute might return an str object.
+
         In this case, one of the entries on the traceback was produced by
         dynamically generated code.
         See: https://bitbucket.org/pytest-dev/py/issues/71
         This fixes #995.
         """
-        from _pytest.python import filter_traceback
+        from _pytest._code import filter_traceback
 
         try:
             ns = {}  # type: Dict[str, Any]
@@ -1033,12 +1052,13 @@ class TestTracebackCutting:
         assert not filter_traceback(traceback[-1])
 
     def test_filter_traceback_path_no_longer_valid(self, testdir) -> None:
-        """test that filter_traceback() works with the fact that
+        """Test that filter_traceback() works with the fact that
         _pytest._code.code.Code.path attribute might return an str object.
+
         In this case, one of the files in the traceback no longer exists.
         This fixes #1133.
         """
-        from _pytest.python import filter_traceback
+        from _pytest._code import filter_traceback
 
         testdir.syspathinsert()
         testdir.makepyfile(
@@ -1250,8 +1270,7 @@ def test_class_injection_does_not_break_collection(testdir):
 
 
 def test_syntax_error_with_non_ascii_chars(testdir):
-    """Fix decoding issue while formatting SyntaxErrors during collection (#578)
-    """
+    """Fix decoding issue while formatting SyntaxErrors during collection (#578)."""
     testdir.makepyfile("☃")
     result = testdir.runpytest()
     result.stdout.fnmatch_lines(["*ERROR collecting*", "*SyntaxError*", "*1 error in*"])
index ca3408ece30a2679ed79a8ed8b01035798ab1863..9ae5a91db43431522e13b94a179b2c3d962dc913 100644 (file)
@@ -3,6 +3,7 @@ import textwrap
 
 import pytest
 from _pytest import fixtures
+from _pytest.compat import getfuncargnames
 from _pytest.config import ExitCode
 from _pytest.fixtures import FixtureRequest
 from _pytest.pathlib import Path
@@ -15,22 +16,22 @@ def test_getfuncargnames_functions():
     def f():
         raise NotImplementedError()
 
-    assert not fixtures.getfuncargnames(f)
+    assert not getfuncargnames(f)
 
     def g(arg):
         raise NotImplementedError()
 
-    assert fixtures.getfuncargnames(g) == ("arg",)
+    assert getfuncargnames(g) == ("arg",)
 
     def h(arg1, arg2="hello"):
         raise NotImplementedError()
 
-    assert fixtures.getfuncargnames(h) == ("arg1",)
+    assert getfuncargnames(h) == ("arg1",)
 
     def j(arg1, arg2, arg3="hello"):
         raise NotImplementedError()
 
-    assert fixtures.getfuncargnames(j) == ("arg1", "arg2")
+    assert getfuncargnames(j) == ("arg1", "arg2")
 
 
 def test_getfuncargnames_methods():
@@ -40,7 +41,7 @@ def test_getfuncargnames_methods():
         def f(self, arg1, arg2="hello"):
             raise NotImplementedError()
 
-    assert fixtures.getfuncargnames(A().f) == ("arg1",)
+    assert getfuncargnames(A().f) == ("arg1",)
 
 
 def test_getfuncargnames_staticmethod():
@@ -51,7 +52,7 @@ def test_getfuncargnames_staticmethod():
         def static(arg1, arg2, x=1):
             raise NotImplementedError()
 
-    assert fixtures.getfuncargnames(A.static, cls=A) == ("arg1", "arg2")
+    assert getfuncargnames(A.static, cls=A) == ("arg1", "arg2")
 
 
 def test_getfuncargnames_partial():
@@ -64,7 +65,7 @@ def test_getfuncargnames_partial():
     class T:
         test_ok = functools.partial(check, i=2)
 
-    values = fixtures.getfuncargnames(T().test_ok, name="test_ok")
+    values = getfuncargnames(T().test_ok, name="test_ok")
     assert values == ("arg1", "arg2")
 
 
@@ -78,7 +79,7 @@ def test_getfuncargnames_staticmethod_partial():
     class T:
         test_ok = staticmethod(functools.partial(check, i=2))
 
-    values = fixtures.getfuncargnames(T().test_ok, name="test_ok")
+    values = getfuncargnames(T().test_ok, name="test_ok")
     assert values == ("arg1", "arg2")
 
 
@@ -142,14 +143,14 @@ class TestFillFixtures:
         p = testdir.copy_example()
         result = testdir.runpytest()
         result.stdout.fnmatch_lines(["*1 passed*"])
-        result = testdir.runpytest(next(p.visit("test_*.py")))
+        result = testdir.runpytest(str(next(Path(str(p)).rglob("test_*.py"))))
         result.stdout.fnmatch_lines(["*1 passed*"])
 
     def test_extend_fixture_conftest_conftest(self, testdir):
         p = testdir.copy_example()
         result = testdir.runpytest()
         result.stdout.fnmatch_lines(["*1 passed*"])
-        result = testdir.runpytest(next(p.visit("test_*.py")))
+        result = testdir.runpytest(str(next(Path(str(p)).rglob("test_*.py"))))
         result.stdout.fnmatch_lines(["*1 passed*"])
 
     def test_extend_fixture_conftest_plugin(self, testdir):
@@ -395,6 +396,132 @@ class TestFillFixtures:
         result = testdir.runpytest(testfile)
         result.stdout.fnmatch_lines(["*3 passed*"])
 
+    def test_override_fixture_reusing_super_fixture_parametrization(self, testdir):
+        """Override a fixture at a lower level, reusing the higher-level fixture that
+        is parametrized (#1953).
+        """
+        testdir.makeconftest(
+            """
+            import pytest
+
+            @pytest.fixture(params=[1, 2])
+            def foo(request):
+                return request.param
+            """
+        )
+        testdir.makepyfile(
+            """
+            import pytest
+
+            @pytest.fixture
+            def foo(foo):
+                return foo * 2
+
+            def test_spam(foo):
+                assert foo in (2, 4)
+            """
+        )
+        result = testdir.runpytest()
+        result.stdout.fnmatch_lines(["*2 passed*"])
+
+    def test_override_parametrize_fixture_and_indirect(self, testdir):
+        """Override a fixture at a lower level, reusing the higher-level fixture that
+        is parametrized, while also using indirect parametrization.
+        """
+        testdir.makeconftest(
+            """
+            import pytest
+
+            @pytest.fixture(params=[1, 2])
+            def foo(request):
+                return request.param
+            """
+        )
+        testdir.makepyfile(
+            """
+            import pytest
+
+            @pytest.fixture
+            def foo(foo):
+                return foo * 2
+
+            @pytest.fixture
+            def bar(request):
+                return request.param * 100
+
+            @pytest.mark.parametrize("bar", [42], indirect=True)
+            def test_spam(bar, foo):
+                assert bar == 4200
+                assert foo in (2, 4)
+            """
+        )
+        result = testdir.runpytest()
+        result.stdout.fnmatch_lines(["*2 passed*"])
+
+    def test_override_top_level_fixture_reusing_super_fixture_parametrization(
+        self, testdir
+    ):
+        """Same as the above test, but with another level of overwriting."""
+        testdir.makeconftest(
+            """
+            import pytest
+
+            @pytest.fixture(params=['unused', 'unused'])
+            def foo(request):
+                return request.param
+            """
+        )
+        testdir.makepyfile(
+            """
+            import pytest
+
+            @pytest.fixture(params=[1, 2])
+            def foo(request):
+                return request.param
+
+            class Test:
+
+                @pytest.fixture
+                def foo(self, foo):
+                    return foo * 2
+
+                def test_spam(self, foo):
+                    assert foo in (2, 4)
+            """
+        )
+        result = testdir.runpytest()
+        result.stdout.fnmatch_lines(["*2 passed*"])
+
+    def test_override_parametrized_fixture_with_new_parametrized_fixture(self, testdir):
+        """Overriding a parametrized fixture, while also parametrizing the new fixture and
+        simultaneously requesting the overwritten fixture as parameter, yields the same value
+        as ``request.param``.
+        """
+        testdir.makeconftest(
+            """
+            import pytest
+
+            @pytest.fixture(params=['ignored', 'ignored'])
+            def foo(request):
+                return request.param
+            """
+        )
+        testdir.makepyfile(
+            """
+            import pytest
+
+            @pytest.fixture(params=[10, 20])
+            def foo(foo, request):
+                assert request.param == foo
+                return foo * 2
+
+            def test_spam(foo):
+                assert foo in (20, 40)
+            """
+        )
+        result = testdir.runpytest()
+        result.stdout.fnmatch_lines(["*2 passed*"])
+
     def test_autouse_fixture_plugin(self, testdir):
         # A fixture from a plugin has no baseid set, which screwed up
         # the autouse fixture handling.
@@ -814,28 +941,6 @@ class TestRequestBasic:
         result = testdir.runpytest()
         result.stdout.fnmatch_lines(["*1 passed*"])
 
-    def test_funcargnames_compatattr(self, testdir):
-        testdir.makepyfile(
-            """
-            import pytest
-            def pytest_generate_tests(metafunc):
-                with pytest.warns(pytest.PytestDeprecationWarning):
-                    assert metafunc.funcargnames == metafunc.fixturenames
-            @pytest.fixture
-            def fn(request):
-                with pytest.warns(pytest.PytestDeprecationWarning):
-                    assert request._pyfuncitem.funcargnames == \
-                           request._pyfuncitem.fixturenames
-                with pytest.warns(pytest.PytestDeprecationWarning):
-                    return request.funcargnames, request.fixturenames
-
-            def test_hello(fn):
-                assert fn[0] == fn[1]
-        """
-        )
-        reprec = testdir.inline_run()
-        reprec.assertoutcome(passed=1)
-
     def test_setupdecorator_and_xunit(self, testdir):
         testdir.makepyfile(
             """
@@ -1683,10 +1788,8 @@ class TestAutouseDiscovery:
         reprec.assertoutcome(passed=2)
 
     def test_callables_nocode(self, testdir):
-        """
-        an imported mock.call would break setup/factory discovery
-        due to it being callable and __code__ not being a code object
-        """
+        """An imported mock.call would break setup/factory discovery due to
+        it being callable and __code__ not being a code object."""
         testdir.makepyfile(
             """
            class _call(tuple):
@@ -3332,9 +3435,7 @@ class TestShowFixtures:
         )
 
     def test_show_fixtures_different_files(self, testdir):
-        """
-        #833: --fixtures only shows fixtures from first file
-        """
+        """`--fixtures` only shows fixtures from first file (#833)."""
         testdir.makepyfile(
             test_a='''
             import pytest
@@ -4155,73 +4256,6 @@ def test_fixture_named_request(testdir):
     )
 
 
-def test_fixture_duplicated_arguments() -> None:
-    """Raise error if there are positional and keyword arguments for the same parameter (#1682)."""
-    with pytest.raises(TypeError) as excinfo:
-
-        @pytest.fixture("session", scope="session")  # type: ignore[call-overload]
-        def arg(arg):
-            pass
-
-    assert (
-        str(excinfo.value)
-        == "The fixture arguments are defined as positional and keyword: scope. "
-        "Use only keyword arguments."
-    )
-
-    with pytest.raises(TypeError) as excinfo:
-
-        @pytest.fixture(  # type: ignore[call-overload]
-            "function",
-            ["p1"],
-            True,
-            ["id1"],
-            "name",
-            scope="session",
-            params=["p1"],
-            autouse=True,
-            ids=["id1"],
-            name="name",
-        )
-        def arg2(request):
-            pass
-
-    assert (
-        str(excinfo.value)
-        == "The fixture arguments are defined as positional and keyword: scope, params, autouse, ids, name. "
-        "Use only keyword arguments."
-    )
-
-
-def test_fixture_with_positionals() -> None:
-    """Raise warning, but the positionals should still works (#1682)."""
-    from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
-
-    with pytest.warns(pytest.PytestDeprecationWarning) as warnings:
-
-        @pytest.fixture("function", [0], True)  # type: ignore[call-overload]
-        def fixture_with_positionals():
-            pass
-
-    assert str(warnings[0].message) == str(FIXTURE_POSITIONAL_ARGUMENTS)
-
-    assert fixture_with_positionals._pytestfixturefunction.scope == "function"
-    assert fixture_with_positionals._pytestfixturefunction.params == (0,)
-    assert fixture_with_positionals._pytestfixturefunction.autouse
-
-
-def test_fixture_with_too_many_positionals() -> None:
-    with pytest.raises(TypeError) as excinfo:
-
-        @pytest.fixture("function", [0], True, ["id"], "name", "extra")  # type: ignore[call-overload]
-        def fixture_with_positionals():
-            pass
-
-    assert (
-        str(excinfo.value) == "fixture() takes 5 positional arguments but 6 were given"
-    )
-
-
 def test_indirect_fixture_does_not_break_scope(testdir):
     """Ensure that fixture scope is respected when using indirect fixtures (#570)"""
     testdir.makepyfile(
index 537057484d0272b9cbba8e4ff208dc354bbfa773..854593a65c03683d090fa67c874fb57ea87633ef 100644 (file)
@@ -1,8 +1,8 @@
 from typing import Any
 
 import pytest
-from _pytest import python
 from _pytest import runner
+from _pytest._code import getfslineno
 
 
 class TestOEJSKITSpecials:
@@ -87,8 +87,8 @@ def test_wrapped_getfslineno() -> None:
     def wrapped_func(x, y, z):
         pass
 
-    fs, lineno = python.getfslineno(wrapped_func)
-    fs2, lineno2 = python.getfslineno(wrap)
+    fs, lineno = getfslineno(wrapped_func)
+    fs2, lineno2 = getfslineno(wrap)
     assert lineno > lineno2, "getfslineno does not unwrap correctly"
 
 
index 5d935153931f2b85a0471772af98556c1907b1c3..6b59104567ad02788f823f7428ac60dc8237b96c 100644 (file)
@@ -19,6 +19,8 @@ from hypothesis import strategies
 import pytest
 from _pytest import fixtures
 from _pytest import python
+from _pytest.compat import _format_args
+from _pytest.compat import getfuncargnames
 from _pytest.compat import NOTSET
 from _pytest.outcomes import fail
 from _pytest.pytester import Testdir
@@ -28,9 +30,9 @@ from _pytest.python import idmaker
 
 class TestMetafunc:
     def Metafunc(self, func, config=None) -> python.Metafunc:
-        # the unit tests of this class check if things work correctly
+        # The unit tests of this class check if things work correctly
         # on the funcarg level, so we don't need a full blown
-        # initialization
+        # initialization.
         class FuncFixtureInfoMock:
             name2fixturedefs = None
 
@@ -42,7 +44,7 @@ class TestMetafunc:
             obj = attr.ib()
             _nodeid = attr.ib()
 
-        names = fixtures.getfuncargnames(func)
+        names = getfuncargnames(func)
         fixtureinfo = FuncFixtureInfoMock(names)  # type: Any
         definition = DefinitionMock._create(func, "mock::nodeid")  # type: Any
         return python.Metafunc(definition, fixtureinfo, config)
@@ -135,7 +137,7 @@ class TestMetafunc:
             metafunc.parametrize("request", [1])
 
     def test_find_parametrized_scope(self) -> None:
-        """unittest for _find_parametrized_scope (#3941)"""
+        """Unit test for _find_parametrized_scope (#3941)."""
         from _pytest.python import _find_parametrized_scope
 
         @attr.s
@@ -143,7 +145,7 @@ class TestMetafunc:
             scope = attr.ib()
 
         fixtures_defs = cast(
-            Dict[str, Sequence[fixtures.FixtureDef]],
+            Dict[str, Sequence[fixtures.FixtureDef[object]]],
             dict(
                 session_fix=[DummyFixtureDef("session")],
                 package_fix=[DummyFixtureDef("package")],
@@ -284,30 +286,29 @@ class TestMetafunc:
         escaped.encode("ascii")
 
     def test_unicode_idval(self) -> None:
-        """This tests that Unicode strings outside the ASCII character set get
+        """Test that Unicode strings outside the ASCII character set get
         escaped, using byte escapes if they're in that range or unicode
         escapes if they're not.
 
         """
         values = [
-            ("", ""),
-            ("ascii", "ascii"),
-            ("ação", "a\\xe7\\xe3o"),
-            ("josé@blah.com", "jos\\xe9@blah.com"),
+            ("", r""),
+            ("ascii", r"ascii"),
+            ("ação", r"a\xe7\xe3o"),
+            ("josé@blah.com", r"jos\xe9@blah.com"),
             (
-                "δοκ.ιμή@παράδειγμα.δοκιμή",
-                "\\u03b4\\u03bf\\u03ba.\\u03b9\\u03bc\\u03ae@\\u03c0\\u03b1\\u03c1\\u03ac\\u03b4\\u03b5\\u03b9\\u03b3"
-                "\\u03bc\\u03b1.\\u03b4\\u03bf\\u03ba\\u03b9\\u03bc\\u03ae",
+                r"δοκ.ιμή@παράδειγμα.δοκιμή",
+                r"\u03b4\u03bf\u03ba.\u03b9\u03bc\u03ae@\u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3"
+                r"\u03bc\u03b1.\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae",
             ),
         ]
         for val, expected in values:
             assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
 
     def test_unicode_idval_with_config(self) -> None:
-        """unittest for expected behavior to obtain ids with
+        """Unit test for expected behavior to obtain ids with
         disable_test_id_escaping_and_forfeit_all_rights_to_community_support
-        option. (#5294)
-        """
+        option (#5294)."""
 
         class MockConfig:
             def __init__(self, config):
@@ -334,25 +335,20 @@ class TestMetafunc:
             assert actual == expected
 
     def test_bytes_idval(self) -> None:
-        """unittest for the expected behavior to obtain ids for parametrized
-        bytes values:
-        - python2: non-ascii strings are considered bytes and formatted using
-        "binary escape", where any byte < 127 is escaped into its hex form.
-        - python3: bytes objects are always escaped using "binary escape".
-        """
+        """Unit test for the expected behavior to obtain ids for parametrized
+        bytes values: bytes objects are always escaped using "binary escape"."""
         values = [
-            (b"", ""),
-            (b"\xc3\xb4\xff\xe4", "\\xc3\\xb4\\xff\\xe4"),
-            (b"ascii", "ascii"),
-            ("αρά".encode(), "\\xce\\xb1\\xcf\\x81\\xce\\xac"),
+            (b"", r""),
+            (b"\xc3\xb4\xff\xe4", r"\xc3\xb4\xff\xe4"),
+            (b"ascii", r"ascii"),
+            ("αρά".encode(), r"\xce\xb1\xcf\x81\xce\xac"),
         ]
         for val, expected in values:
             assert _idval(val, "a", 6, idfn=None, nodeid=None, config=None) == expected
 
     def test_class_or_function_idval(self) -> None:
-        """unittest for the expected behavior to obtain ids for parametrized
-        values that are classes or functions: their __name__.
-        """
+        """Unit test for the expected behavior to obtain ids for parametrized
+        values that are classes or functions: their __name__."""
 
         class TestClass:
             pass
@@ -491,9 +487,9 @@ class TestMetafunc:
         assert result == ["a-a0", "a-a1", "a-a2"]
 
     def test_idmaker_with_idfn_and_config(self) -> None:
-        """unittest for expected behavior to create ids with idfn and
+        """Unit test for expected behavior to create ids with idfn and
         disable_test_id_escaping_and_forfeit_all_rights_to_community_support
-        option. (#5294)
+        option (#5294).
         """
 
         class MockConfig:
@@ -523,9 +519,9 @@ class TestMetafunc:
             assert result == [expected]
 
     def test_idmaker_with_ids_and_config(self) -> None:
-        """unittest for expected behavior to create ids with ids and
+        """Unit test for expected behavior to create ids with ids and
         disable_test_id_escaping_and_forfeit_all_rights_to_community_support
-        option. (#5294)
+        option (#5294).
         """
 
         class MockConfig:
@@ -963,22 +959,22 @@ class TestMetafunc:
         def function1():
             pass
 
-        assert fixtures._format_args(function1) == "()"
+        assert _format_args(function1) == "()"
 
         def function2(arg1):
             pass
 
-        assert fixtures._format_args(function2) == "(arg1)"
+        assert _format_args(function2) == "(arg1)"
 
         def function3(arg1, arg2="qwe"):
             pass
 
-        assert fixtures._format_args(function3) == "(arg1, arg2='qwe')"
+        assert _format_args(function3) == "(arg1, arg2='qwe')"
 
         def function4(arg1, *args, **kwargs):
             pass
 
-        assert fixtures._format_args(function4) == "(arg1, *args, **kwargs)"
+        assert _format_args(function4) == "(arg1, *args, **kwargs)"
 
 
 class TestMetafuncFunctional:
@@ -1427,9 +1423,8 @@ class TestMetafuncFunctional:
 
 
 class TestMetafuncFunctionalAuto:
-    """
-    Tests related to automatically find out the correct scope for parametrized tests (#1832).
-    """
+    """Tests related to automatically find out the correct scope for
+    parametrized tests (#1832)."""
 
     def test_parametrize_auto_scope(self, testdir: Testdir) -> None:
         testdir.makepyfile(
index 12d44495c99b9b268e9876d44bfe44489ad6942c..26931a378445c83104ef9ec2ffad4f261d147858 100644 (file)
@@ -157,9 +157,7 @@ class TestRaises:
 
     @pytest.mark.parametrize("method", ["function", "function_match", "with"])
     def test_raises_cyclic_reference(self, method):
-        """
-        Ensure pytest.raises does not leave a reference cycle (#1965).
-        """
+        """Ensure pytest.raises does not leave a reference cycle (#1965)."""
         import gc
 
         class T:
@@ -283,3 +281,22 @@ class TestRaises:
             with pytest.raises(Exception, foo="bar"):  # type: ignore[call-overload]
                 pass
         assert "Unexpected keyword arguments" in str(excinfo.value)
+
+    def test_expected_exception_is_not_a_baseexception(self) -> None:
+        with pytest.raises(TypeError) as excinfo:
+            with pytest.raises("hello"):  # type: ignore[call-overload]
+                pass  # pragma: no cover
+        assert "must be a BaseException type, not str" in str(excinfo.value)
+
+        class NotAnException:
+            pass
+
+        with pytest.raises(TypeError) as excinfo:
+            with pytest.raises(NotAnException):  # type: ignore[type-var]
+                pass  # pragma: no cover
+        assert "must be a BaseException type, not NotAnException" in str(excinfo.value)
+
+        with pytest.raises(TypeError) as excinfo:
+            with pytest.raises(("hello", NotAnException)):  # type: ignore[arg-type]
+                pass  # pragma: no cover
+        assert "must be a BaseException type, not str" in str(excinfo.value)
index 08362c62a63706e64bc971760d2fa859b1cf6366..9cab242c0e062627cc3c57adf030a17c26a7b22f 100644 (file)
@@ -3,7 +3,7 @@ import sys
 
 import pytest
 
-# test for _argcomplete but not specific for any application
+# Test for _argcomplete but not specific for any application.
 
 
 def equal_with_bash(prefix, ffc, fc, out=None):
@@ -18,9 +18,9 @@ def equal_with_bash(prefix, ffc, fc, out=None):
     return retval
 
 
-# copied from argcomplete.completers as import from there
-# also pulls in argcomplete.__init__ which opens filedescriptor 9
-# this gives an OSError at the end of testrun
+# Copied from argcomplete.completers as import from there.
+# Also pulls in argcomplete.__init__ which opens filedescriptor 9.
+# This gives an OSError at the end of testrun.
 
 
 def _wrapcall(*args, **kargs):
@@ -31,7 +31,7 @@ def _wrapcall(*args, **kargs):
 
 
 class FilesCompleter:
-    "File completer class, optionally takes a list of allowed extensions"
+    """File completer class, optionally takes a list of allowed extensions."""
 
     def __init__(self, allowednames=(), directories=True):
         # Fix if someone passes in a string instead of a list
@@ -91,9 +91,7 @@ class TestArgComplete:
 
     @pytest.mark.skipif("sys.platform in ('win32', 'darwin')")
     def test_remove_dir_prefix(self):
-        """this is not compatible with compgen but it is with bash itself:
-        ls /usr/<TAB>
-        """
+        """This is not compatible with compgen but it is with bash itself: ls /usr/<TAB>."""
         from _pytest._argcomplete import FastFilesCompleter
 
         ffc = FastFilesCompleter()
index 6723a707e198d72a81b9ee0bba0cf4a3d125f71b..1cb63a329f6b8d6a24c584d1bc4a1219254df36d 100644 (file)
@@ -29,9 +29,7 @@ class TestImportHookInstallation:
     @pytest.mark.parametrize("initial_conftest", [True, False])
     @pytest.mark.parametrize("mode", ["plain", "rewrite"])
     def test_conftest_assertion_rewrite(self, testdir, initial_conftest, mode):
-        """Test that conftest files are using assertion rewrite on import.
-        (#1619)
-        """
+        """Test that conftest files are using assertion rewrite on import (#1619)."""
         testdir.tmpdir.join("foo/tests").ensure(dir=1)
         conftest_path = "conftest.py" if initial_conftest else "foo/conftest.py"
         contents = {
@@ -569,7 +567,7 @@ class TestAssert_reprcompare:
             assert "b" not in line
 
     def test_dict_omitting_with_verbosity_1(self) -> None:
-        """ Ensure differing items are visible for verbosity=1 (#1512) """
+        """Ensure differing items are visible for verbosity=1 (#1512)."""
         lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=1)
         assert lines is not None
         assert lines[1].startswith("Omitting 1 identical item")
@@ -640,7 +638,8 @@ class TestAssert_reprcompare:
 
     def test_Sequence(self) -> None:
         # Test comparing with a Sequence subclass.
-        class TestSequence(collections.abc.MutableSequence):
+        # TODO(py36): Inherit from typing.MutableSequence[int].
+        class TestSequence(collections.abc.MutableSequence):  # type: ignore[type-arg]
             def __init__(self, iterable):
                 self.elements = list(iterable)
 
@@ -718,10 +717,7 @@ class TestAssert_reprcompare:
         ]
 
     def test_one_repr_empty(self):
-        """
-        the faulty empty string repr did trigger
-        an unbound local error in _diff_text
-        """
+        """The faulty empty string repr did trigger an unbound local error in _diff_text."""
 
         class A(str):
             def __repr__(self):
@@ -1134,7 +1130,7 @@ class TestTruncateExplanation:
         assert last_line_before_trunc_msg.endswith("...")
 
     def test_full_output_truncated(self, monkeypatch, testdir):
-        """ Test against full runpytest() output. """
+        """Test against full runpytest() output."""
 
         line_count = 7
         line_len = 100
@@ -1369,9 +1365,7 @@ def test_traceback_failure(testdir):
 
 
 def test_exception_handling_no_traceback(testdir):
-    """
-    Handle chain exceptions in tasks submitted by the multiprocess module (#1984).
-    """
+    """Handle chain exceptions in tasks submitted by the multiprocess module (#1984)."""
     p1 = testdir.makepyfile(
         """
         from multiprocessing import Pool
index e403bb2ec9b6db6e87f97df858dc534a50be9c31..ad3089a23c035d094b4fb398fa83a7775d05b72b 100644 (file)
@@ -233,7 +233,7 @@ class TestAssertionRewrite:
 
     def test_dont_rewrite_if_hasattr_fails(self, request) -> None:
         class Y:
-            """ A class whos getattr fails, but not with `AttributeError` """
+            """A class whose getattr fails, but not with `AttributeError`."""
 
             def __getattr__(self, attribute_name):
                 raise KeyError()
@@ -381,7 +381,7 @@ class TestAssertionRewrite:
         )
 
         def f7() -> None:
-            assert False or x()
+            assert False or x()  # type: ignore[unreachable]
 
         assert (
             getmsg(f7, {"x": x})
@@ -416,7 +416,7 @@ class TestAssertionRewrite:
 
     def test_short_circuit_evaluation(self) -> None:
         def f1() -> None:
-            assert True or explode  # type: ignore[name-defined] # noqa: F821
+            assert True or explode  # type: ignore[name-defined,unreachable] # noqa: F821
 
         getmsg(f1, must_pass=True)
 
@@ -471,7 +471,7 @@ class TestAssertionRewrite:
         assert getmsg(f1) == "assert ((3 % 2) and False)"
 
         def f2() -> None:
-            assert False or 4 % 2
+            assert False or 4 % 2  # type: ignore[unreachable]
 
         assert getmsg(f2) == "assert (False or (4 % 2))"
 
@@ -911,10 +911,8 @@ def test_rewritten():
         assert testdir.runpytest_subprocess().ret == 0
 
     def test_remember_rewritten_modules(self, pytestconfig, testdir, monkeypatch):
-        """
-        AssertionRewriteHook should remember rewritten modules so it
-        doesn't give false positives (#2005).
-        """
+        """`AssertionRewriteHook` should remember rewritten modules so it
+        doesn't give false positives (#2005)."""
         monkeypatch.syspath_prepend(testdir.tmpdir)
         testdir.makepyfile(test_remember_rewritten_modules="")
         warnings = []
@@ -992,7 +990,7 @@ class TestAssertionRewriteHookDetails:
                 e = OSError()
                 e.errno = 10
                 raise e
-                yield
+                yield  # type:ignore[unreachable]
 
             monkeypatch.setattr(
                 _pytest.assertion.rewrite, "atomic_write", atomic_write_failed
@@ -1091,8 +1089,7 @@ class TestAssertionRewriteHookDetails:
         result.stdout.fnmatch_lines(["* 1 passed*"])
 
     def test_get_data_support(self, testdir):
-        """Implement optional PEP302 api (#808).
-        """
+        """Implement optional PEP302 api (#808)."""
         path = testdir.mkpydir("foo")
         path.join("test_foo.py").write(
             textwrap.dedent(
@@ -1600,7 +1597,7 @@ class TestPyCacheDir:
         if prefix:
             if sys.version_info < (3, 8):
                 pytest.skip("pycache_prefix not available in py<38")
-            monkeypatch.setattr(sys, "pycache_prefix", prefix)
+            monkeypatch.setattr(sys, "pycache_prefix", prefix)  # type:ignore
 
         assert get_cache_dir(Path(source)) == Path(expected)
 
index c133663ea1bcb5608788c49830188690383a72f4..a911257ce24ac0a805d9013fcd0e743307f63046 100644 (file)
@@ -643,9 +643,7 @@ class TestLastFailed:
         return sorted(config.cache.get("cache/lastfailed", {}))
 
     def test_cache_cumulative(self, testdir):
-        """
-        Test workflow where user fixes errors gradually file by file using --lf.
-        """
+        """Test workflow where user fixes errors gradually file by file using --lf."""
         # 1. initial run
         test_bar = testdir.makepyfile(
             test_bar="""
index 678bc1863c101d22e82e234428cfd4e50b1a63b6..5f820d8465b41976d3aa8c0b20d5171e2bd4d5c5 100644 (file)
@@ -14,6 +14,7 @@ import pytest
 from _pytest import capture
 from _pytest.capture import _get_multicapture
 from _pytest.capture import CaptureManager
+from _pytest.capture import CaptureResult
 from _pytest.capture import MultiCapture
 from _pytest.config import ExitCode
 from _pytest.pytester import Testdir
@@ -22,7 +23,9 @@ from _pytest.pytester import Testdir
 # pylib 1.4.20.dev2 (rev 13d9af95547e)
 
 
-def StdCaptureFD(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture:
+def StdCaptureFD(
+    out: bool = True, err: bool = True, in_: bool = True
+) -> MultiCapture[str]:
     return capture.MultiCapture(
         in_=capture.FDCapture(0) if in_ else None,
         out=capture.FDCapture(1) if out else None,
@@ -30,7 +33,9 @@ def StdCaptureFD(out: bool = True, err: bool = True, in_: bool = True) -> MultiC
     )
 
 
-def StdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture:
+def StdCapture(
+    out: bool = True, err: bool = True, in_: bool = True
+) -> MultiCapture[str]:
     return capture.MultiCapture(
         in_=capture.SysCapture(0) if in_ else None,
         out=capture.SysCapture(1) if out else None,
@@ -38,7 +43,9 @@ def StdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCap
     )
 
 
-def TeeStdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture:
+def TeeStdCapture(
+    out: bool = True, err: bool = True, in_: bool = True
+) -> MultiCapture[str]:
     return capture.MultiCapture(
         in_=capture.SysCapture(0, tee=True) if in_ else None,
         out=capture.SysCapture(1, tee=True) if out else None,
@@ -664,9 +671,8 @@ class TestCaptureFixture:
 
     @pytest.mark.parametrize("fixture", ["capsys", "capfd"])
     def test_fixture_use_by_other_fixtures(self, testdir, fixture):
-        """
-        Ensure that capsys and capfd can be used by other fixtures during setup and teardown.
-        """
+        """Ensure that capsys and capfd can be used by other fixtures during
+        setup and teardown."""
         testdir.makepyfile(
             """\
             import sys
@@ -886,6 +892,36 @@ def test_dontreadfrominput():
     f.close()  # just for completeness
 
 
+def test_captureresult() -> None:
+    cr = CaptureResult("out", "err")
+    assert len(cr) == 2
+    assert cr.out == "out"
+    assert cr.err == "err"
+    out, err = cr
+    assert out == "out"
+    assert err == "err"
+    assert cr[0] == "out"
+    assert cr[1] == "err"
+    assert cr == cr
+    assert cr == CaptureResult("out", "err")
+    assert cr != CaptureResult("wrong", "err")
+    assert cr == ("out", "err")
+    assert cr != ("out", "wrong")
+    assert hash(cr) == hash(CaptureResult("out", "err"))
+    assert hash(cr) == hash(("out", "err"))
+    assert hash(cr) != hash(("out", "wrong"))
+    assert cr < ("z",)
+    assert cr < ("z", "b")
+    assert cr < ("z", "b", "c")
+    assert cr.count("err") == 1
+    assert cr.count("wrong") == 0
+    assert cr.index("err") == 1
+    with pytest.raises(ValueError):
+        assert cr.index("wrong") == 0
+    assert next(iter(cr)) == "out"
+    assert cr._replace(err="replaced") == ("out", "replaced")
+
+
 @pytest.fixture
 def tmpfile(testdir) -> Generator[BinaryIO, None, None]:
     f = testdir.makepyfile("").open("wb+")
@@ -1138,8 +1174,8 @@ class TestTeeStdCapture(TestStdCapture):
     captureclass = staticmethod(TeeStdCapture)
 
     def test_capturing_error_recursive(self):
-        """ for TeeStdCapture since we passthrough stderr/stdout, cap1
-        should get all output, while cap2 should only get "cap2\n" """
+        r"""For TeeStdCapture since we passthrough stderr/stdout, cap1
+        should get all output, while cap2 should only get "cap2\n"."""
 
         with self.getcapture() as cap1:
             print("cap1")
index 3e01e296b58fa0976df451bfdba706544dee4428..3e1b816b79e0d2380dffb43499fee69dc49d18ce 100644 (file)
@@ -7,6 +7,7 @@ import pytest
 from _pytest.config import ExitCode
 from _pytest.main import _in_venv
 from _pytest.main import Session
+from _pytest.pathlib import Path
 from _pytest.pathlib import symlink_or_skip
 from _pytest.pytester import Testdir
 
@@ -115,8 +116,8 @@ class TestCollectFS:
         tmpdir.ensure(".whatever", "test_notfound.py")
         tmpdir.ensure(".bzr", "test_notfound.py")
         tmpdir.ensure("normal", "test_found.py")
-        for x in tmpdir.visit("test_*.py"):
-            x.write("def test_hello(): pass")
+        for x in Path(str(tmpdir)).rglob("test_*.py"):
+            x.write_text("def test_hello(): pass", "utf-8")
 
         result = testdir.runpytest("--collect-only")
         s = result.stdout.str()
@@ -256,20 +257,6 @@ class TestCollectPluginHookRelay:
         assert len(wascalled) == 1
         assert wascalled[0].ext == ".abc"
 
-    @pytest.mark.filterwarnings("ignore:.*pytest_collect_directory.*")
-    def test_pytest_collect_directory(self, testdir):
-        wascalled = []
-
-        class Plugin:
-            def pytest_collect_directory(self, path):
-                wascalled.append(path.basename)
-
-        testdir.mkdir("hello")
-        testdir.mkdir("world")
-        pytest.main(testdir.tmpdir, plugins=[Plugin()])
-        assert "hello" in wascalled
-        assert "world" in wascalled
-
 
 class TestPrunetraceback:
     def test_custom_repr_failure(self, testdir):
@@ -442,25 +429,6 @@ class TestCustomConftests:
 
 
 class TestSession:
-    def test_parsearg(self, testdir) -> None:
-        p = testdir.makepyfile("def test_func(): pass")
-        subdir = testdir.mkdir("sub")
-        subdir.ensure("__init__.py")
-        target = subdir.join(p.basename)
-        p.move(target)
-        subdir.chdir()
-        config = testdir.parseconfig(p.basename)
-        rcol = Session.from_config(config)
-        assert rcol.fspath == subdir
-        fspath, parts = rcol._parsearg(p.basename)
-
-        assert fspath == target
-        assert len(parts) == 0
-        fspath, parts = rcol._parsearg(p.basename + "::test_func")
-        assert fspath == target
-        assert parts[0] == "test_func"
-        assert len(parts) == 1
-
     def test_collect_topdir(self, testdir):
         p = testdir.makepyfile("def test_func(): pass")
         id = "::".join([p.basename, "test_func"])
@@ -724,10 +692,8 @@ class Test_genitems:
         print(s)
 
     def test_class_and_functions_discovery_using_glob(self, testdir):
-        """
-        tests that python_classes and python_functions config options work
-        as prefixes and glob-like patterns (issue #600).
-        """
+        """Test that Python_classes and Python_functions config options work
+        as prefixes and glob-like patterns (#600)."""
         testdir.makeini(
             """
             [pytest]
@@ -1427,3 +1393,17 @@ class TestImportModeImportlib:
                 "* 1 failed in *",
             ]
         )
+
+
+def test_does_not_crash_on_error_from_decorated_function(testdir: Testdir) -> None:
+    """Regression test for an issue around bad exception formatting due to
+    assertion rewriting mangling lineno's (#4984)."""
+    testdir.makepyfile(
+        """
+        @pytest.fixture
+        def a(): return 4
+        """
+    )
+    result = testdir.runpytest()
+    # Not INTERNAL_ERROR
+    assert result.ret == ExitCode.INTERRUPTED
index 9b1c11d5edcc16cd3dd141f1fdc8d3eab752bf03..0cfd11fd525fd3d6636871a3229ddb4965112119 100644 (file)
@@ -5,6 +5,7 @@ import textwrap
 from typing import Dict
 from typing import List
 from typing import Sequence
+from typing import Tuple
 
 import attr
 import py.path
@@ -12,26 +13,42 @@ import py.path
 import _pytest._code
 import pytest
 from _pytest.compat import importlib_metadata
+from _pytest.compat import TYPE_CHECKING
 from _pytest.config import _get_plugin_specs_as_list
 from _pytest.config import _iter_rewritable_modules
+from _pytest.config import _strtobool
 from _pytest.config import Config
 from _pytest.config import ConftestImportFailure
 from _pytest.config import ExitCode
+from _pytest.config import parse_warning_filter
 from _pytest.config.exceptions import UsageError
 from _pytest.config.findpaths import determine_setup
 from _pytest.config.findpaths import get_common_ancestor
 from _pytest.config.findpaths import locate_config
+from _pytest.monkeypatch import MonkeyPatch
 from _pytest.pathlib import Path
+from _pytest.pytester import Testdir
+
+if TYPE_CHECKING:
+    from typing import Type
 
 
 class TestParseIni:
     @pytest.mark.parametrize(
         "section, filename", [("pytest", "pytest.ini"), ("tool:pytest", "setup.cfg")]
     )
-    def test_getcfg_and_config(self, testdir, tmpdir, section, filename):
-        sub = tmpdir.mkdir("sub")
-        sub.chdir()
-        tmpdir.join(filename).write(
+    def test_getcfg_and_config(
+        self,
+        testdir: Testdir,
+        tmp_path: Path,
+        section: str,
+        filename: str,
+        monkeypatch: MonkeyPatch,
+    ) -> None:
+        sub = tmp_path / "sub"
+        sub.mkdir()
+        monkeypatch.chdir(sub)
+        (tmp_path / filename).write_text(
             textwrap.dedent(
                 """\
                 [{section}]
@@ -39,17 +56,14 @@ class TestParseIni:
                 """.format(
                     section=section
                 )
-            )
+            ),
+            encoding="utf-8",
         )
         _, _, cfg = locate_config([sub])
         assert cfg["name"] == "value"
-        config = testdir.parseconfigure(sub)
+        config = testdir.parseconfigure(str(sub))
         assert config.inicfg["name"] == "value"
 
-    def test_getcfg_empty_path(self):
-        """correctly handle zero length arguments (a la pytest '')"""
-        locate_config([""])
-
     def test_setupcfg_uses_toolpytest_with_pytest(self, testdir):
         p1 = testdir.makepyfile("def test(): pass")
         testdir.makefile(
@@ -167,74 +181,81 @@ class TestParseIni:
     @pytest.mark.parametrize(
         "ini_file_text, invalid_keys, warning_output, exception_text",
         [
-            (
+            pytest.param(
                 """
-          [pytest]
-          unknown_ini = value1
-          another_unknown_ini = value2
-          """,
+                [pytest]
+                unknown_ini = value1
+                another_unknown_ini = value2
+                """,
                 ["unknown_ini", "another_unknown_ini"],
                 [
                     "=*= warnings summary =*=",
-                    "*PytestConfigWarning:*Unknown config ini key: another_unknown_ini",
-                    "*PytestConfigWarning:*Unknown config ini key: unknown_ini",
+                    "*PytestConfigWarning:*Unknown config option: another_unknown_ini",
+                    "*PytestConfigWarning:*Unknown config option: unknown_ini",
                 ],
-                "Unknown config ini key: another_unknown_ini",
+                "Unknown config option: another_unknown_ini",
+                id="2-unknowns",
             ),
-            (
+            pytest.param(
                 """
-          [pytest]
-          unknown_ini = value1
-          minversion = 5.0.0
-          """,
+                [pytest]
+                unknown_ini = value1
+                minversion = 5.0.0
+                """,
                 ["unknown_ini"],
                 [
                     "=*= warnings summary =*=",
-                    "*PytestConfigWarning:*Unknown config ini key: unknown_ini",
+                    "*PytestConfigWarning:*Unknown config option: unknown_ini",
                 ],
-                "Unknown config ini key: unknown_ini",
+                "Unknown config option: unknown_ini",
+                id="1-unknown",
             ),
-            (
+            pytest.param(
                 """
-          [some_other_header]
-          unknown_ini = value1
-          [pytest]
-          minversion = 5.0.0
-          """,
+                [some_other_header]
+                unknown_ini = value1
+                [pytest]
+                minversion = 5.0.0
+                """,
                 [],
                 [],
                 "",
+                id="unknown-in-other-header",
             ),
-            (
+            pytest.param(
                 """
-          [pytest]
-          minversion = 5.0.0
-          """,
+                [pytest]
+                minversion = 5.0.0
+                """,
                 [],
                 [],
                 "",
+                id="no-unknowns",
             ),
-            (
+            pytest.param(
                 """
-          [pytest]
-          conftest_ini_key = 1
-          """,
+                [pytest]
+                conftest_ini_key = 1
+                """,
                 [],
                 [],
                 "",
+                id="1-known",
             ),
         ],
     )
-    def test_invalid_ini_keys(
+    @pytest.mark.filterwarnings("default")
+    def test_invalid_config_options(
         self, testdir, ini_file_text, invalid_keys, warning_output, exception_text
     ):
         testdir.makeconftest(
             """
             def pytest_addoption(parser):
                 parser.addini("conftest_ini_key", "")
-        """
+            """
         )
-        testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text))
+        testdir.makepyfile("def test(): pass")
+        testdir.makeini(ini_file_text)
 
         config = testdir.parseconfig()
         assert sorted(config._get_unknown_ini_keys()) == sorted(invalid_keys)
@@ -242,11 +263,45 @@ class TestParseIni:
         result = testdir.runpytest()
         result.stdout.fnmatch_lines(warning_output)
 
+        result = testdir.runpytest("--strict-config")
         if exception_text:
-            with pytest.raises(pytest.fail.Exception, match=exception_text):
-                testdir.runpytest("--strict-config")
+            result.stderr.fnmatch_lines("ERROR: " + exception_text)
+            assert result.ret == pytest.ExitCode.USAGE_ERROR
         else:
-            testdir.runpytest("--strict-config")
+            result.stderr.no_fnmatch_line(exception_text)
+            assert result.ret == pytest.ExitCode.OK
+
+    @pytest.mark.filterwarnings("default")
+    def test_silence_unknown_key_warning(self, testdir: Testdir) -> None:
+        """Unknown config key warnings can be silenced using filterwarnings (#7620)"""
+        testdir.makeini(
+            """
+            [pytest]
+            filterwarnings =
+                ignore:Unknown config option:pytest.PytestConfigWarning
+            foobar=1
+        """
+        )
+        result = testdir.runpytest()
+        result.stdout.no_fnmatch_line("*PytestConfigWarning*")
+
+    @pytest.mark.filterwarnings("default")
+    def test_disable_warnings_plugin_disables_config_warnings(
+        self, testdir: Testdir
+    ) -> None:
+        """Disabling 'warnings' plugin also disables config time warnings"""
+        testdir.makeconftest(
+            """
+            import pytest
+            def pytest_configure(config):
+                config.issue_config_time_warning(
+                    pytest.PytestConfigWarning("custom config warning"),
+                    stacklevel=2,
+                )
+        """
+        )
+        result = testdir.runpytest("-pno:warnings")
+        result.stdout.no_fnmatch_line("*PytestConfigWarning*")
 
     @pytest.mark.parametrize(
         "ini_file_text, exception_text",
@@ -362,11 +417,30 @@ class TestParseIni:
         testdir.makeini(ini_file_text)
 
         if exception_text:
-            with pytest.raises(pytest.fail.Exception, match=exception_text):
+            with pytest.raises(pytest.UsageError, match=exception_text):
                 testdir.parseconfig()
         else:
             testdir.parseconfig()
 
+    def test_early_config_cmdline(self, testdir, monkeypatch):
+        """early_config contains options registered by third-party plugins.
+
+        This is a regression involving pytest-cov (and possibly others) introduced in #7700.
+        """
+        testdir.makepyfile(
+            myplugin="""
+            def pytest_addoption(parser):
+                parser.addoption('--foo', default=None, dest='foo')
+
+            def pytest_load_initial_conftests(early_config, parser, args):
+                assert early_config.known_args_namespace.foo == "1"
+            """
+        )
+        monkeypatch.setenv("PYTEST_PLUGINS", "myplugin")
+        testdir.syspathinsert()
+        result = testdir.runpytest("--foo=1")
+        result.stdout.fnmatch_lines("* no tests ran in *")
+
 
 class TestConfigCmdlineParsing:
     def test_parsing_again_fails(self, testdir):
@@ -1006,8 +1080,8 @@ def test_cmdline_processargs_simple(testdir):
 
 
 def test_invalid_options_show_extra_information(testdir):
-    """display extra information when pytest exits due to unrecognized
-    options in the command-line"""
+    """Display extra information when pytest exits due to unrecognized
+    options in the command-line."""
     testdir.makeini(
         """
         [pytest]
@@ -1125,7 +1199,7 @@ def test_load_initial_conftest_last_ordering(_config_for_test):
     pm.register(m)
     hc = pm.hook.pytest_load_initial_conftests
     values = hc._nonwrappers + hc._wrappers
-    expected = ["_pytest.config", m.__module__, "_pytest.capture"]
+    expected = ["_pytest.config", m.__module__, "_pytest.capture", "_pytest.warnings"]
     assert [x.function.__module__ for x in values] == expected
 
 
@@ -1168,16 +1242,17 @@ def test_collect_pytest_prefix_bug(pytestconfig):
 
 
 class TestRootdir:
-    def test_simple_noini(self, tmpdir):
-        assert get_common_ancestor([tmpdir]) == tmpdir
-        a = tmpdir.mkdir("a")
-        assert get_common_ancestor([a, tmpdir]) == tmpdir
-        assert get_common_ancestor([tmpdir, a]) == tmpdir
-        with tmpdir.as_cwd():
-            assert get_common_ancestor([]) == tmpdir
-            no_path = tmpdir.join("does-not-exist")
-            assert get_common_ancestor([no_path]) == tmpdir
-            assert get_common_ancestor([no_path.join("a")]) == tmpdir
+    def test_simple_noini(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
+        assert get_common_ancestor([tmp_path]) == tmp_path
+        a = tmp_path / "a"
+        a.mkdir()
+        assert get_common_ancestor([a, tmp_path]) == tmp_path
+        assert get_common_ancestor([tmp_path, a]) == tmp_path
+        monkeypatch.chdir(tmp_path)
+        assert get_common_ancestor([]) == tmp_path
+        no_path = tmp_path / "does-not-exist"
+        assert get_common_ancestor([no_path]) == tmp_path
+        assert get_common_ancestor([no_path / "a"]) == tmp_path
 
     @pytest.mark.parametrize(
         "name, contents",
@@ -1190,44 +1265,49 @@ class TestRootdir:
             pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"),
         ],
     )
-    def test_with_ini(self, tmpdir: py.path.local, name: str, contents: str) -> None:
-        inifile = tmpdir.join(name)
-        inifile.write(contents)
-
-        a = tmpdir.mkdir("a")
-        b = a.mkdir("b")
-        for args in ([str(tmpdir)], [str(a)], [str(b)]):
-            rootdir, parsed_inifile, _ = determine_setup(None, args)
-            assert rootdir == tmpdir
-            assert parsed_inifile == inifile
-        rootdir, parsed_inifile, ini_config = determine_setup(None, [str(b), str(a)])
-        assert rootdir == tmpdir
-        assert parsed_inifile == inifile
+    def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None:
+        inipath = tmp_path / name
+        inipath.write_text(contents, "utf-8")
+
+        a = tmp_path / "a"
+        a.mkdir()
+        b = a / "b"
+        b.mkdir()
+        for args in ([str(tmp_path)], [str(a)], [str(b)]):
+            rootpath, parsed_inipath, _ = determine_setup(None, args)
+            assert rootpath == tmp_path
+            assert parsed_inipath == inipath
+        rootpath, parsed_inipath, ini_config = determine_setup(None, [str(b), str(a)])
+        assert rootpath == tmp_path
+        assert parsed_inipath == inipath
         assert ini_config == {"x": "10"}
 
-    @pytest.mark.parametrize("name", "setup.cfg tox.ini".split())
-    def test_pytestini_overrides_empty_other(self, tmpdir: py.path.local, name) -> None:
-        inifile = tmpdir.ensure("pytest.ini")
-        a = tmpdir.mkdir("a")
-        a.ensure(name)
-        rootdir, parsed_inifile, _ = determine_setup(None, [str(a)])
-        assert rootdir == tmpdir
-        assert parsed_inifile == inifile
-
-    def test_setuppy_fallback(self, tmpdir: py.path.local) -> None:
-        a = tmpdir.mkdir("a")
-        a.ensure("setup.cfg")
-        tmpdir.ensure("setup.py")
-        rootdir, inifile, inicfg = determine_setup(None, [str(a)])
-        assert rootdir == tmpdir
-        assert inifile is None
+    @pytest.mark.parametrize("name", ["setup.cfg", "tox.ini"])
+    def test_pytestini_overrides_empty_other(self, tmp_path: Path, name: str) -> None:
+        inipath = tmp_path / "pytest.ini"
+        inipath.touch()
+        a = tmp_path / "a"
+        a.mkdir()
+        (a / name).touch()
+        rootpath, parsed_inipath, _ = determine_setup(None, [str(a)])
+        assert rootpath == tmp_path
+        assert parsed_inipath == inipath
+
+    def test_setuppy_fallback(self, tmp_path: Path) -> None:
+        a = tmp_path / "a"
+        a.mkdir()
+        (a / "setup.cfg").touch()
+        (tmp_path / "setup.py").touch()
+        rootpath, inipath, inicfg = determine_setup(None, [str(a)])
+        assert rootpath == tmp_path
+        assert inipath is None
         assert inicfg == {}
 
-    def test_nothing(self, tmpdir: py.path.local, monkeypatch) -> None:
-        monkeypatch.chdir(str(tmpdir))
-        rootdir, inifile, inicfg = determine_setup(None, [str(tmpdir)])
-        assert rootdir == tmpdir
-        assert inifile is None
+    def test_nothing(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
+        monkeypatch.chdir(tmp_path)
+        rootpath, inipath, inicfg = determine_setup(None, [str(tmp_path)])
+        assert rootpath == tmp_path
+        assert inipath is None
         assert inicfg == {}
 
     @pytest.mark.parametrize(
@@ -1242,45 +1322,58 @@ class TestRootdir:
         ],
     )
     def test_with_specific_inifile(
-        self, tmpdir: py.path.local, name: str, contents: str
+        self, tmp_path: Path, name: str, contents: str
     ) -> None:
-        p = tmpdir.ensure(name)
-        p.write(contents)
-        rootdir, inifile, ini_config = determine_setup(str(p), [str(tmpdir)])
-        assert rootdir == tmpdir
-        assert inifile == p
+        p = tmp_path / name
+        p.touch()
+        p.write_text(contents, "utf-8")
+        rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)])
+        assert rootpath == tmp_path
+        assert inipath == p
         assert ini_config == {"x": "10"}
 
-    def test_with_arg_outside_cwd_without_inifile(self, tmpdir, monkeypatch) -> None:
-        monkeypatch.chdir(str(tmpdir))
-        a = tmpdir.mkdir("a")
-        b = tmpdir.mkdir("b")
-        rootdir, inifile, _ = determine_setup(None, [str(a), str(b)])
-        assert rootdir == tmpdir
+    def test_with_arg_outside_cwd_without_inifile(
+        self, tmp_path: Path, monkeypatch: MonkeyPatch
+    ) -> None:
+        monkeypatch.chdir(tmp_path)
+        a = tmp_path / "a"
+        a.mkdir()
+        b = tmp_path / "b"
+        b.mkdir()
+        rootpath, inifile, _ = determine_setup(None, [str(a), str(b)])
+        assert rootpath == tmp_path
         assert inifile is None
 
-    def test_with_arg_outside_cwd_with_inifile(self, tmpdir) -> None:
-        a = tmpdir.mkdir("a")
-        b = tmpdir.mkdir("b")
-        inifile = a.ensure("pytest.ini")
-        rootdir, parsed_inifile, _ = determine_setup(None, [str(a), str(b)])
-        assert rootdir == a
-        assert inifile == parsed_inifile
+    def test_with_arg_outside_cwd_with_inifile(self, tmp_path: Path) -> None:
+        a = tmp_path / "a"
+        a.mkdir()
+        b = tmp_path / "b"
+        b.mkdir()
+        inipath = a / "pytest.ini"
+        inipath.touch()
+        rootpath, parsed_inipath, _ = determine_setup(None, [str(a), str(b)])
+        assert rootpath == a
+        assert inipath == parsed_inipath
 
     @pytest.mark.parametrize("dirs", ([], ["does-not-exist"], ["a/does-not-exist"]))
-    def test_with_non_dir_arg(self, dirs, tmpdir) -> None:
-        with tmpdir.ensure(dir=True).as_cwd():
-            rootdir, inifile, _ = determine_setup(None, dirs)
-            assert rootdir == tmpdir
-            assert inifile is None
+    def test_with_non_dir_arg(
+        self, dirs: Sequence[str], tmp_path: Path, monkeypatch: MonkeyPatch
+    ) -> None:
+        monkeypatch.chdir(tmp_path)
+        rootpath, inipath, _ = determine_setup(None, dirs)
+        assert rootpath == tmp_path
+        assert inipath is None
 
-    def test_with_existing_file_in_subdir(self, tmpdir) -> None:
-        a = tmpdir.mkdir("a")
-        a.ensure("exist")
-        with tmpdir.as_cwd():
-            rootdir, inifile, _ = determine_setup(None, ["a/exist"])
-            assert rootdir == tmpdir
-            assert inifile is None
+    def test_with_existing_file_in_subdir(
+        self, tmp_path: Path, monkeypatch: MonkeyPatch
+    ) -> None:
+        a = tmp_path / "a"
+        a.mkdir()
+        (a / "exists").touch()
+        monkeypatch.chdir(tmp_path)
+        rootpath, inipath, _ = determine_setup(None, ["a/exist"])
+        assert rootpath == tmp_path
+        assert inipath is None
 
 
 class TestOverrideIniArgs:
@@ -1441,7 +1534,7 @@ class TestOverrideIniArgs:
         )
 
     def test_addopts_from_ini_not_concatenated(self, testdir):
-        """addopts from ini should not take values from normal args (#4265)."""
+        """`addopts` from ini should not take values from normal args (#4265)."""
         testdir.makeini(
             """
             [pytest]
@@ -1777,10 +1870,8 @@ class TestPytestPluginsVariable:
 
 
 def test_conftest_import_error_repr(tmpdir):
-    """
-    ConftestImportFailure should use a short error message and readable path to the failed
-    conftest.py file
-    """
+    """`ConftestImportFailure` should use a short error message and readable
+    path to the failed conftest.py file."""
     path = tmpdir.join("foo/conftest.py")
     with pytest.raises(
         ConftestImportFailure,
@@ -1792,3 +1883,52 @@ def test_conftest_import_error_repr(tmpdir):
             assert exc.__traceback__ is not None
             exc_info = (type(exc), exc, exc.__traceback__)
             raise ConftestImportFailure(path, exc_info) from exc
+
+
+def test_strtobool():
+    assert _strtobool("YES")
+    assert not _strtobool("NO")
+    with pytest.raises(ValueError):
+        _strtobool("unknown")
+
+
+@pytest.mark.parametrize(
+    "arg, escape, expected",
+    [
+        ("ignore", False, ("ignore", "", Warning, "", 0)),
+        (
+            "ignore::DeprecationWarning",
+            False,
+            ("ignore", "", DeprecationWarning, "", 0),
+        ),
+        (
+            "ignore:some msg:DeprecationWarning",
+            False,
+            ("ignore", "some msg", DeprecationWarning, "", 0),
+        ),
+        (
+            "ignore::DeprecationWarning:mod",
+            False,
+            ("ignore", "", DeprecationWarning, "mod", 0),
+        ),
+        (
+            "ignore::DeprecationWarning:mod:42",
+            False,
+            ("ignore", "", DeprecationWarning, "mod", 42),
+        ),
+        ("error:some\\msg:::", True, ("error", "some\\\\msg", Warning, "", 0)),
+        ("error:::mod\\foo:", True, ("error", "", Warning, "mod\\\\foo\\Z", 0)),
+    ],
+)
+def test_parse_warning_filter(
+    arg: str, escape: bool, expected: "Tuple[str, str, Type[Warning], str, int]"
+) -> None:
+    assert parse_warning_filter(arg, escape=escape) == expected
+
+
+@pytest.mark.parametrize("arg", [":" * 5, "::::-1", "::::not-a-number"])
+def test_parse_warning_filter_failure(arg: str) -> None:
+    import warnings
+
+    with pytest.raises(warnings._OptionError):
+        parse_warning_filter(arg, escape=True)
index 724e6f464cb0ca4edf889885013431d4d85bb3b1..5a4764080133a56dddf58cb9ccfdad9e42d455f6 100644 (file)
@@ -196,9 +196,7 @@ def test_conftest_confcutdir(testdir):
 
 
 def test_conftest_symlink(testdir):
-    """
-    conftest.py discovery follows normal path resolution and does not resolve symlinks.
-    """
+    """`conftest.py` discovery follows normal path resolution and does not resolve symlinks."""
     # Structure:
     # /real
     # /real/conftest.py
@@ -308,21 +306,6 @@ def test_no_conftest(testdir):
     assert result.ret == ExitCode.USAGE_ERROR
 
 
-def test_conftest_existing_resultlog(testdir):
-    x = testdir.mkdir("tests")
-    x.join("conftest.py").write(
-        textwrap.dedent(
-            """\
-            def pytest_addoption(parser):
-                parser.addoption("--xyz", action="store_true")
-            """
-        )
-    )
-    testdir.makefile(ext=".log", result="")  # Writes result.log
-    result = testdir.runpytest("-h", "--resultlog", "result.log")
-    result.stdout.fnmatch_lines(["*--xyz*"])
-
-
 def test_conftest_existing_junitxml(testdir):
     x = testdir.mkdir("tests")
     x.join("conftest.py").write(
@@ -477,8 +460,9 @@ class TestConftestVisibility:
             )
         )
         print("created directory structure:")
-        for x in testdir.tmpdir.visit():
-            print("   " + x.relto(testdir.tmpdir))
+        tmppath = Path(str(testdir.tmpdir))
+        for x in tmppath.rglob(""):
+            print("   " + str(x.relative_to(tmppath)))
 
         return {"runner": runner, "package": package, "swc": swc, "snc": snc}
 
index 965dba6c1799ee570c7b10cec1d8a8dab1a026c5..0b32ad32203cbf93aff218f17051e4ec12ce2841 100644 (file)
@@ -115,8 +115,7 @@ class TestDoctests:
         reprec.assertoutcome(failed=1)
 
     def test_multiple_patterns(self, testdir):
-        """Test support for multiple --doctest-glob arguments (#1255).
-        """
+        """Test support for multiple --doctest-glob arguments (#1255)."""
         testdir.maketxtfile(
             xdoc="""
             >>> 1
@@ -149,8 +148,7 @@ class TestDoctests:
         [("foo", "ascii"), ("öäü", "latin1"), ("öäü", "utf-8")],
     )
     def test_encoding(self, testdir, test_string, encoding):
-        """Test support for doctest_encoding ini option.
-        """
+        """Test support for doctest_encoding ini option."""
         testdir.makeini(
             """
             [pytest]
@@ -667,8 +665,7 @@ class TestDoctests:
         reprec.assertoutcome(failed=1, passed=0)
 
     def test_contains_unicode(self, testdir):
-        """Fix internal error with docstrings containing non-ascii characters.
-        """
+        """Fix internal error with docstrings containing non-ascii characters."""
         testdir.makepyfile(
             '''\
             def foo():
@@ -701,9 +698,7 @@ class TestDoctests:
         reprec.assertoutcome(skipped=1, failed=1, passed=0)
 
     def test_junit_report_for_doctest(self, testdir):
-        """
-        #713: Fix --junit-xml option when used with --doctest-modules.
-        """
+        """#713: Fix --junit-xml option when used with --doctest-modules."""
         p = testdir.makepyfile(
             """
             def foo():
@@ -775,9 +770,7 @@ class TestDoctests:
         result.stdout.fnmatch_lines(["* 1 passed *"])
 
     def test_reportinfo(self, testdir):
-        """
-        Test case to make sure that DoctestItem.reportinfo() returns lineno.
-        """
+        """Make sure that DoctestItem.reportinfo() returns lineno."""
         p = testdir.makepyfile(
             test_reportinfo="""
             def foo(x):
@@ -1167,8 +1160,7 @@ class TestDoctestAutoUseFixtures:
     SCOPES = ["module", "session", "class", "function"]
 
     def test_doctest_module_session_fixture(self, testdir):
-        """Test that session fixtures are initialized for doctest modules (#768)
-        """
+        """Test that session fixtures are initialized for doctest modules (#768)."""
         # session fixture which changes some global data, which will
         # be accessed by doctests in a module
         testdir.makeconftest(
index 46adccd21593cc32e831fa88f44bed63b272a48b..87a195bf807c68d9eff224592f1921ddbfb7182d 100644 (file)
@@ -19,8 +19,7 @@ def test_enabled(testdir):
 
 def test_crash_near_exit(testdir):
     """Test that fault handler displays crashes that happen even after
-    pytest is exiting (for example, when the interpreter is shutting down).
-    """
+    pytest is exiting (for example, when the interpreter is shutting down)."""
     testdir.makepyfile(
         """
     import faulthandler
@@ -35,8 +34,7 @@ def test_crash_near_exit(testdir):
 
 
 def test_disabled(testdir):
-    """Test option to disable fault handler in the command line.
-    """
+    """Test option to disable fault handler in the command line."""
     testdir.makepyfile(
         """
     import faulthandler
@@ -60,6 +58,7 @@ def test_disabled(testdir):
 )
 def test_timeout(testdir, enabled: bool) -> None:
     """Test option to dump tracebacks after a certain timeout.
+
     If faulthandler is disabled, no traceback will be dumped.
     """
     testdir.makepyfile(
@@ -90,9 +89,8 @@ def test_timeout(testdir, enabled: bool) -> None:
 @pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"])
 def test_cancel_timeout_on_hook(monkeypatch, hook_name):
     """Make sure that we are cancelling any scheduled traceback dumping due
-    to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any other interactive
-    exception (pytest-dev/pytest-faulthandler#14).
-    """
+    to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any
+    other interactive exception (pytest-dev/pytest-faulthandler#14)."""
     import faulthandler
     from _pytest.faulthandler import FaultHandlerHooks
 
@@ -111,7 +109,7 @@ def test_cancel_timeout_on_hook(monkeypatch, hook_name):
 
 @pytest.mark.parametrize("faulthandler_timeout", [0, 2])
 def test_already_initialized(faulthandler_timeout, testdir):
-    """Test for faulthandler being initialized earlier than pytest (#6575)"""
+    """Test for faulthandler being initialized earlier than pytest (#6575)."""
     testdir.makepyfile(
         """
         def test():
index 3de2ea21828954d85fbe4ea5944f76888bd3eb66..974dcf8f3cda649e2198cd7265e09fd15a0d1833 100644 (file)
@@ -1,74 +1,75 @@
 from textwrap import dedent
 
-import py
-
 import pytest
 from _pytest.config.findpaths import get_common_ancestor
+from _pytest.config.findpaths import get_dirs_from_args
 from _pytest.config.findpaths import load_config_dict_from_file
+from _pytest.pathlib import Path
 
 
 class TestLoadConfigDictFromFile:
-    def test_empty_pytest_ini(self, tmpdir):
+    def test_empty_pytest_ini(self, tmp_path: Path) -> None:
         """pytest.ini files are always considered for configuration, even if empty"""
-        fn = tmpdir.join("pytest.ini")
-        fn.write("")
+        fn = tmp_path / "pytest.ini"
+        fn.write_text("", encoding="utf-8")
         assert load_config_dict_from_file(fn) == {}
 
-    def test_pytest_ini(self, tmpdir):
+    def test_pytest_ini(self, tmp_path: Path) -> None:
         """[pytest] section in pytest.ini files is read correctly"""
-        fn = tmpdir.join("pytest.ini")
-        fn.write("[pytest]\nx=1")
+        fn = tmp_path / "pytest.ini"
+        fn.write_text("[pytest]\nx=1", encoding="utf-8")
         assert load_config_dict_from_file(fn) == {"x": "1"}
 
-    def test_custom_ini(self, tmpdir):
+    def test_custom_ini(self, tmp_path: Path) -> None:
         """[pytest] section in any .ini file is read correctly"""
-        fn = tmpdir.join("custom.ini")
-        fn.write("[pytest]\nx=1")
+        fn = tmp_path / "custom.ini"
+        fn.write_text("[pytest]\nx=1", encoding="utf-8")
         assert load_config_dict_from_file(fn) == {"x": "1"}
 
-    def test_custom_ini_without_section(self, tmpdir):
+    def test_custom_ini_without_section(self, tmp_path: Path) -> None:
         """Custom .ini files without [pytest] section are not considered for configuration"""
-        fn = tmpdir.join("custom.ini")
-        fn.write("[custom]")
+        fn = tmp_path / "custom.ini"
+        fn.write_text("[custom]", encoding="utf-8")
         assert load_config_dict_from_file(fn) is None
 
-    def test_custom_cfg_file(self, tmpdir):
+    def test_custom_cfg_file(self, tmp_path: Path) -> None:
         """Custom .cfg files without [tool:pytest] section are not considered for configuration"""
-        fn = tmpdir.join("custom.cfg")
-        fn.write("[custom]")
+        fn = tmp_path / "custom.cfg"
+        fn.write_text("[custom]", encoding="utf-8")
         assert load_config_dict_from_file(fn) is None
 
-    def test_valid_cfg_file(self, tmpdir):
+    def test_valid_cfg_file(self, tmp_path: Path) -> None:
         """Custom .cfg files with [tool:pytest] section are read correctly"""
-        fn = tmpdir.join("custom.cfg")
-        fn.write("[tool:pytest]\nx=1")
+        fn = tmp_path / "custom.cfg"
+        fn.write_text("[tool:pytest]\nx=1", encoding="utf-8")
         assert load_config_dict_from_file(fn) == {"x": "1"}
 
-    def test_unsupported_pytest_section_in_cfg_file(self, tmpdir):
+    def test_unsupported_pytest_section_in_cfg_file(self, tmp_path: Path) -> None:
         """.cfg files with [pytest] section are no longer supported and should fail to alert users"""
-        fn = tmpdir.join("custom.cfg")
-        fn.write("[pytest]")
+        fn = tmp_path / "custom.cfg"
+        fn.write_text("[pytest]", encoding="utf-8")
         with pytest.raises(pytest.fail.Exception):
             load_config_dict_from_file(fn)
 
-    def test_invalid_toml_file(self, tmpdir):
+    def test_invalid_toml_file(self, tmp_path: Path) -> None:
         """.toml files without [tool.pytest.ini_options] are not considered for configuration."""
-        fn = tmpdir.join("myconfig.toml")
-        fn.write(
+        fn = tmp_path / "myconfig.toml"
+        fn.write_text(
             dedent(
                 """
             [build_system]
             x = 1
             """
-            )
+            ),
+            encoding="utf-8",
         )
         assert load_config_dict_from_file(fn) is None
 
-    def test_valid_toml_file(self, tmpdir):
+    def test_valid_toml_file(self, tmp_path: Path) -> None:
         """.toml files with [tool.pytest.ini_options] are read correctly, including changing
         data types to str/list for compatibility with other configuration options."""
-        fn = tmpdir.join("myconfig.toml")
-        fn.write(
+        fn = tmp_path / "myconfig.toml"
+        fn.write_text(
             dedent(
                 """
             [tool.pytest.ini_options]
@@ -77,7 +78,8 @@ class TestLoadConfigDictFromFile:
             values = ["tests", "integration"]
             name = "foo"
             """
-            )
+            ),
+            encoding="utf-8",
         )
         assert load_config_dict_from_file(fn) == {
             "x": "1",
@@ -88,23 +90,36 @@ class TestLoadConfigDictFromFile:
 
 
 class TestCommonAncestor:
-    def test_has_ancestor(self, tmpdir):
-        fn1 = tmpdir.join("foo/bar/test_1.py").ensure(file=1)
-        fn2 = tmpdir.join("foo/zaz/test_2.py").ensure(file=1)
-        assert get_common_ancestor([fn1, fn2]) == tmpdir.join("foo")
-        assert get_common_ancestor([py.path.local(fn1.dirname), fn2]) == tmpdir.join(
-            "foo"
-        )
-        assert get_common_ancestor(
-            [py.path.local(fn1.dirname), py.path.local(fn2.dirname)]
-        ) == tmpdir.join("foo")
-        assert get_common_ancestor([fn1, py.path.local(fn2.dirname)]) == tmpdir.join(
-            "foo"
-        )
-
-    def test_single_dir(self, tmpdir):
-        assert get_common_ancestor([tmpdir]) == tmpdir
-
-    def test_single_file(self, tmpdir):
-        fn = tmpdir.join("foo.py").ensure(file=1)
-        assert get_common_ancestor([fn]) == tmpdir
+    def test_has_ancestor(self, tmp_path: Path) -> None:
+        fn1 = tmp_path / "foo" / "bar" / "test_1.py"
+        fn1.parent.mkdir(parents=True)
+        fn1.touch()
+        fn2 = tmp_path / "foo" / "zaz" / "test_2.py"
+        fn2.parent.mkdir(parents=True)
+        fn2.touch()
+        assert get_common_ancestor([fn1, fn2]) == tmp_path / "foo"
+        assert get_common_ancestor([fn1.parent, fn2]) == tmp_path / "foo"
+        assert get_common_ancestor([fn1.parent, fn2.parent]) == tmp_path / "foo"
+        assert get_common_ancestor([fn1, fn2.parent]) == tmp_path / "foo"
+
+    def test_single_dir(self, tmp_path: Path) -> None:
+        assert get_common_ancestor([tmp_path]) == tmp_path
+
+    def test_single_file(self, tmp_path: Path) -> None:
+        fn = tmp_path / "foo.py"
+        fn.touch()
+        assert get_common_ancestor([fn]) == tmp_path
+
+
+def test_get_dirs_from_args(tmp_path):
+    """get_dirs_from_args() skips over non-existing directories and files"""
+    fn = tmp_path / "foo.py"
+    fn.touch()
+    d = tmp_path / "tests"
+    d.mkdir()
+    option = "--foobar=/foo.txt"
+    # xdist uses options in this format for its rsync feature (#7638)
+    xdist_rsync_option = "popen=c:/dest"
+    assert get_dirs_from_args(
+        [str(fn), str(tmp_path / "does_not_exist"), str(d), option, xdist_rsync_option]
+    ) == [fn.parent, d]
index a33273a2c1daf4c5e6fa6e99f652eda69a388fe5..6116242ec0d7b0a1259fea3da4f182b917bb0a5a 100644 (file)
@@ -39,8 +39,7 @@ def test_help(testdir):
 
 
 def test_none_help_param_raises_exception(testdir):
-    """Tests a None help param raises a TypeError.
-    """
+    """Test that a None help param raises a TypeError."""
     testdir.makeconftest(
         """
         def pytest_addoption(parser):
@@ -54,8 +53,7 @@ def test_none_help_param_raises_exception(testdir):
 
 
 def test_empty_help_param(testdir):
-    """Tests an empty help param is displayed correctly.
-    """
+    """Test that an empty help param is displayed correctly."""
     testdir.makeconftest(
         """
         def pytest_addoption(parser):
index 01eeccdcd9282a9718104e03ef4776e96f33d9b9..3cc93a398055dafb0622bc8e23355c6dd45c5725 100644 (file)
@@ -22,7 +22,7 @@ from _pytest.store import Store
 
 @pytest.fixture(scope="session")
 def schema():
-    """Returns a xmlschema.XMLSchema object for the junit-10.xsd file"""
+    """Return an xmlschema.XMLSchema object for the junit-10.xsd file."""
     fn = Path(__file__).parent / "example_scripts/junit-10.xsd"
     with fn.open() as f:
         return xmlschema.XMLSchema(f)
@@ -30,9 +30,8 @@ def schema():
 
 @pytest.fixture
 def run_and_parse(testdir, schema):
-    """
-    Fixture that returns a function that can be used to execute pytest and return
-    the parsed ``DomNode`` of the root xml node.
+    """Fixture that returns a function that can be used to execute pytest and
+    return the parsed ``DomNode`` of the root xml node.
 
     The ``family`` parameter is used to configure the ``junit_family`` of the written report.
     "xunit2" is also automatically validated against the schema.
@@ -323,8 +322,9 @@ class TestPython:
         node = dom.find_first_by_tag("testsuite")
         node.assert_attr(errors=1, failures=1, tests=1)
         first, second = dom.find_by_tag("testcase")
-        if not first or not second or first == second:
-            assert 0
+        assert first
+        assert second
+        assert first != second
         fnode = first.find_first_by_tag("failure")
         fnode.assert_attr(message="Exception: Call Exception")
         snode = second.find_first_by_tag("error")
@@ -535,7 +535,7 @@ class TestPython:
         node = dom.find_first_by_tag("testsuite")
         tnode = node.find_first_by_tag("testcase")
         fnode = tnode.find_first_by_tag("failure")
-        fnode.assert_attr(message="AssertionError: An error assert 0")
+        fnode.assert_attr(message="AssertionError: An error\nassert 0")
 
     @parametrize_families
     def test_failure_escape(self, testdir, run_and_parse, xunit_family):
@@ -719,7 +719,7 @@ class TestPython:
         assert "hx" in fnode.toxml()
 
     def test_assertion_binchars(self, testdir, run_and_parse):
-        """this test did fail when the escaping wasnt strict"""
+        """This test did fail when the escaping wasn't strict."""
         testdir.makepyfile(
             """
 
@@ -995,14 +995,14 @@ def test_invalid_xml_escape():
     # 0xD, 0xD7FF, 0xE000, 0xFFFD, 0x10000, 0x10FFFF)
 
     for i in invalid:
-        got = bin_xml_escape(chr(i)).uniobj
+        got = bin_xml_escape(chr(i))
         if i <= 0xFF:
             expected = "#x%02X" % i
         else:
             expected = "#x%04X" % i
         assert got == expected
     for i in valid:
-        assert chr(i) == bin_xml_escape(chr(i)).uniobj
+        assert chr(i) == bin_xml_escape(chr(i))
 
 
 def test_logxml_path_expansion(tmpdir, monkeypatch):
@@ -1211,8 +1211,7 @@ def test_record_attribute(testdir, run_and_parse):
 @pytest.mark.filterwarnings("default")
 @pytest.mark.parametrize("fixture_name", ["record_xml_attribute", "record_property"])
 def test_record_fixtures_xunit2(testdir, fixture_name, run_and_parse):
-    """Ensure record_xml_attribute and record_property drop values when outside of legacy family
-    """
+    """Ensure record_xml_attribute and record_property drop values when outside of legacy family."""
     testdir.makeini(
         """
         [pytest]
@@ -1249,10 +1248,9 @@ def test_record_fixtures_xunit2(testdir, fixture_name, run_and_parse):
 
 
 def test_random_report_log_xdist(testdir, monkeypatch, run_and_parse):
-    """xdist calls pytest_runtest_logreport as they are executed by the workers,
+    """`xdist` calls pytest_runtest_logreport as they are executed by the workers,
     with nodes from several nodes overlapping, so junitxml must cope with that
-    to produce correct reports. #1064
-    """
+    to produce correct reports (#1064)."""
     pytest.importorskip("xdist")
     monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
     testdir.makepyfile(
index 3e9199dff563d7a1578b0de4c50bdb6472bab22e..f43f7ded5671e6123d5f116a97aaacc39790d298 100644 (file)
@@ -50,9 +50,7 @@ def subst_path_linux(filename):
 
 
 def test_link_resolve(testdir: pytester.Testdir) -> None:
-    """
-    See: https://github.com/pytest-dev/pytest/issues/5965
-    """
+    """See: https://github.com/pytest-dev/pytest/issues/5965."""
     sub1 = testdir.mkpydir("sub1")
     p = sub1.join("test_foo.py")
     p.write(
index ee8349a9f33880e7035091de3f73c32aaf639046..5b45ec6b5bd178cf759a9923be2d1232f6540830 100644 (file)
@@ -1,9 +1,16 @@
 import argparse
+import os
+import re
 from typing import Optional
 
+import py.path
+
 import pytest
 from _pytest.config import ExitCode
+from _pytest.config import UsageError
+from _pytest.main import resolve_collection_argument
 from _pytest.main import validate_basetemp
+from _pytest.pathlib import Path
 from _pytest.pytester import Testdir
 
 
@@ -98,3 +105,144 @@ def test_validate_basetemp_fails(tmp_path, basetemp, monkeypatch):
 def test_validate_basetemp_integration(testdir):
     result = testdir.runpytest("--basetemp=.")
     result.stderr.fnmatch_lines("*basetemp must not be*")
+
+
+class TestResolveCollectionArgument:
+    @pytest.fixture
+    def invocation_dir(self, testdir: Testdir) -> py.path.local:
+        testdir.syspathinsert(str(testdir.tmpdir / "src"))
+        testdir.chdir()
+
+        pkg = testdir.tmpdir.join("src/pkg").ensure_dir()
+        pkg.join("__init__.py").ensure()
+        pkg.join("test.py").ensure()
+        return testdir.tmpdir
+
+    @pytest.fixture
+    def invocation_path(self, invocation_dir: py.path.local) -> Path:
+        return Path(str(invocation_dir))
+
+    def test_file(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
+        """File and parts."""
+        assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == (
+            invocation_dir / "src/pkg/test.py",
+            [],
+        )
+        assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == (
+            invocation_dir / "src/pkg/test.py",
+            [""],
+        )
+        assert resolve_collection_argument(
+            invocation_path, "src/pkg/test.py::foo::bar"
+        ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"])
+        assert resolve_collection_argument(
+            invocation_path, "src/pkg/test.py::foo::bar::"
+        ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar", ""])
+
+    def test_dir(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
+        """Directory and parts."""
+        assert resolve_collection_argument(invocation_path, "src/pkg") == (
+            invocation_dir / "src/pkg",
+            [],
+        )
+
+        with pytest.raises(
+            UsageError, match=r"directory argument cannot contain :: selection parts"
+        ):
+            resolve_collection_argument(invocation_path, "src/pkg::")
+
+        with pytest.raises(
+            UsageError, match=r"directory argument cannot contain :: selection parts"
+        ):
+            resolve_collection_argument(invocation_path, "src/pkg::foo::bar")
+
+    def test_pypath(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
+        """Dotted name and parts."""
+        assert resolve_collection_argument(
+            invocation_path, "pkg.test", as_pypath=True
+        ) == (invocation_dir / "src/pkg/test.py", [])
+        assert resolve_collection_argument(
+            invocation_path, "pkg.test::foo::bar", as_pypath=True
+        ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"])
+        assert resolve_collection_argument(invocation_path, "pkg", as_pypath=True) == (
+            invocation_dir / "src/pkg",
+            [],
+        )
+
+        with pytest.raises(
+            UsageError, match=r"package argument cannot contain :: selection parts"
+        ):
+            resolve_collection_argument(
+                invocation_path, "pkg::foo::bar", as_pypath=True
+            )
+
+    def test_does_not_exist(self, invocation_path: Path) -> None:
+        """Given a file/module that does not exist raises UsageError."""
+        with pytest.raises(
+            UsageError, match=re.escape("file or directory not found: foobar")
+        ):
+            resolve_collection_argument(invocation_path, "foobar")
+
+        with pytest.raises(
+            UsageError,
+            match=re.escape(
+                "module or package not found: foobar (missing __init__.py?)"
+            ),
+        ):
+            resolve_collection_argument(invocation_path, "foobar", as_pypath=True)
+
+    def test_absolute_paths_are_resolved_correctly(
+        self, invocation_dir: py.path.local, invocation_path: Path
+    ) -> None:
+        """Absolute paths resolve back to absolute paths."""
+        full_path = str(invocation_dir / "src")
+        assert resolve_collection_argument(invocation_path, full_path) == (
+            py.path.local(os.path.abspath("src")),
+            [],
+        )
+
+        # ensure full paths given in the command-line without the drive letter resolve
+        # to the full path correctly (#7628)
+        drive, full_path_without_drive = os.path.splitdrive(full_path)
+        assert resolve_collection_argument(
+            invocation_path, full_path_without_drive
+        ) == (py.path.local(os.path.abspath("src")), [])
+
+
+def test_module_full_path_without_drive(testdir):
+    """Collect and run test using full path except for the drive letter (#7628).
+
+    Passing a full path without a drive letter would trigger a bug in py.path.local
+    where it would keep the full path without the drive letter around, instead of resolving
+    to the full path, resulting in fixtures node ids not matching against test node ids correctly.
+    """
+    testdir.makepyfile(
+        **{
+            "project/conftest.py": """
+                import pytest
+                @pytest.fixture
+                def fix(): return 1
+            """,
+        }
+    )
+
+    testdir.makepyfile(
+        **{
+            "project/tests/dummy_test.py": """
+                def test(fix):
+                    assert fix == 1
+            """
+        }
+    )
+    fn = testdir.tmpdir.join("project/tests/dummy_test.py")
+    assert fn.isfile()
+
+    drive, path = os.path.splitdrive(str(fn))
+
+    result = testdir.runpytest(path, "-v")
+    result.stdout.fnmatch_lines(
+        [
+            os.path.join("project", "tests", "dummy_test.py") + "::test PASSED *",
+            "* 1 passed in *",
+        ]
+    )
index f35660093e7eca9939ccfe5be8301a073f0437ec..5d5e0cf42f7e75fcf1ffe9499c33d26b99b19cee 100644 (file)
@@ -4,8 +4,8 @@ from unittest import mock
 
 import pytest
 from _pytest.config import ExitCode
-from _pytest.mark import EMPTY_PARAMETERSET_OPTION
 from _pytest.mark import MarkGenerator as Mark
+from _pytest.mark.structures import EMPTY_PARAMETERSET_OPTION
 from _pytest.nodes import Collector
 from _pytest.nodes import Node
 
@@ -370,9 +370,8 @@ def test_keyword_option_wrong_arguments(
 
 
 def test_parametrized_collected_from_command_line(testdir):
-    """Parametrized test not collected if test named specified
-       in command line issue#649.
-    """
+    """Parametrized test not collected if test named specified in command
+    line issue#649."""
     py_file = testdir.makepyfile(
         """
         import pytest
@@ -430,7 +429,7 @@ def test_parametrized_with_kwargs(testdir):
 
 
 def test_parametrize_iterator(testdir):
-    """parametrize should work with generators (#5354)."""
+    """`parametrize` should work with generators (#5354)."""
     py_file = testdir.makepyfile(
         """\
         import pytest
@@ -669,13 +668,12 @@ class TestFunctional:
         reprec.assertoutcome(passed=1)
 
     def assert_markers(self, items, **expected):
-        """assert that given items have expected marker names applied to them.
-        expected should be a dict of (item name -> seq of expected marker names)
+        """Assert that given items have expected marker names applied to them.
+        expected should be a dict of (item name -> seq of expected marker names).
 
-        .. note:: this could be moved to ``testdir`` if proven to be useful
+        Note: this could be moved to ``testdir`` if proven to be useful
         to other modules.
         """
-
         items = {x.name: x for x in items}
         for name, expected_markers in expected.items():
             markers = {m.name for m in items[name].iter_markers()}
@@ -866,9 +864,7 @@ class TestKeywordSelection:
         assert len(deselected_tests) == 1
 
     def test_no_match_directories_outside_the_suite(self, testdir):
-        """
-        -k should not match against directories containing the test suite (#7040).
-        """
+        """`-k` should not match against directories containing the test suite (#7040)."""
         test_contents = """
             def test_aaa(): pass
             def test_ddd(): pass
index 7ab8951a01558a84434992e853b3ec77797585fb..1acf6d09f5908037b59f24cfb90ed65b12a25c77 100644 (file)
@@ -1,5 +1,4 @@
-"""
-Test importing of all internal packages and modules.
+"""Test importing of all internal packages and modules.
 
 This ensures all internal packages can be imported without needing the pytest
 namespace being set, which is critical for the initialization of xdist.
index 509e72599c7aab3a22adda892f3ed29d558ee033..fea8a28fba8203857ef90945aa59b68ed2b3743c 100644 (file)
@@ -360,7 +360,7 @@ def test_issue156_undo_staticmethod(Sample: "Type[Sample]") -> None:
     monkeypatch.setattr(Sample, "hello", None)
     assert Sample.hello is None
 
-    monkeypatch.undo()
+    monkeypatch.undo()  # type: ignore[unreachable]
     assert Sample.hello()
 
 
index 0701641f8057f2e27afc574123ab66f75244d47f..7f88a13eb43cfab4b7336bd5071b69ae88d945ce 100644 (file)
@@ -87,9 +87,7 @@ class TestPaste:
 
     @pytest.fixture
     def mocked_urlopen_fail(self, monkeypatch):
-        """
-        monkeypatch the actual urlopen call to emulate a HTTP Error 400
-        """
+        """Monkeypatch the actual urlopen call to emulate a HTTP Error 400."""
         calls = []
 
         import urllib.error
@@ -104,11 +102,9 @@ class TestPaste:
 
     @pytest.fixture
     def mocked_urlopen_invalid(self, monkeypatch):
-        """
-        monkeypatch the actual urlopen calls done by the internal plugin
+        """Monkeypatch the actual urlopen calls done by the internal plugin
         function that connects to bpaste service, but return a url in an
-        unexpected format
-        """
+        unexpected format."""
         calls = []
 
         def mocked(url, data):
@@ -128,10 +124,8 @@ class TestPaste:
 
     @pytest.fixture
     def mocked_urlopen(self, monkeypatch):
-        """
-        monkeypatch the actual urlopen calls done by the internal plugin
-        function that connects to bpaste service.
-        """
+        """Monkeypatch the actual urlopen calls done by the internal plugin
+        function that connects to bpaste service."""
         calls = []
 
         def mocked(url, data):
index 2c1a1c021f8b6d9c869373dfc2569f25e149d341..41228d6b0954b67e6917dbd212e75ac1fa84c2e9 100644 (file)
@@ -6,6 +6,8 @@ from textwrap import dedent
 import py
 
 import pytest
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import commonpath
 from _pytest.pathlib import ensure_deletable
 from _pytest.pathlib import fnmatch_ex
 from _pytest.pathlib import get_extended_length_path_str
@@ -18,9 +20,8 @@ from _pytest.pathlib import resolve_package_path
 
 
 class TestFNMatcherPort:
-    """Test that our port of py.common.FNMatcher (fnmatch_ex) produces the same results as the
-    original py.path.local.fnmatch method.
-    """
+    """Test that our port of py.common.FNMatcher (fnmatch_ex) produces the
+    same results as the original py.path.local.fnmatch method."""
 
     @pytest.fixture(params=["pathlib", "py.path"])
     def match(self, request):
@@ -268,19 +269,19 @@ class TestImportPath:
         return fn
 
     def test_importmode_importlib(self, simple_module):
-        """importlib mode does not change sys.path"""
+        """`importlib` mode does not change sys.path."""
         module = import_path(simple_module, mode="importlib")
         assert module.foo(2) == 42  # type: ignore[attr-defined]
         assert simple_module.dirname not in sys.path
 
     def test_importmode_twice_is_different_module(self, simple_module):
-        """importlib mode always returns a new module"""
+        """`importlib` mode always returns a new module."""
         module1 = import_path(simple_module, mode="importlib")
         module2 = import_path(simple_module, mode="importlib")
         assert module1 is not module2
 
     def test_no_meta_path_found(self, simple_module, monkeypatch):
-        """Even without any meta_path should still import module"""
+        """Even without any meta_path should still import module."""
         monkeypatch.setattr(sys, "meta_path", [])
         module = import_path(simple_module, mode="importlib")
         assert module.foo(2) == 42  # type: ignore[attr-defined]
@@ -382,3 +383,21 @@ def test_suppress_error_removing_lock(tmp_path):
     # check now that we can remove the lock file in normal circumstances
     assert ensure_deletable(path, consider_lock_dead_if_created_before=mtime + 30)
     assert not lock.is_file()
+
+
+def test_bestrelpath() -> None:
+    curdir = Path("/foo/bar/baz/path")
+    assert bestrelpath(curdir, curdir) == "."
+    assert bestrelpath(curdir, curdir / "hello" / "world") == "hello" + os.sep + "world"
+    assert bestrelpath(curdir, curdir.parent / "sister") == ".." + os.sep + "sister"
+    assert bestrelpath(curdir, curdir.parent) == ".."
+    assert bestrelpath(curdir, Path("hello")) == "hello"
+
+
+def test_commonpath() -> None:
+    path = Path("/foo/bar/baz/path")
+    subpath = path / "sampledir"
+    assert commonpath(path, subpath) == path
+    assert commonpath(subpath, path) == path
+    assert commonpath(Path(str(path) + "suffix"), path) == path.parent
+    assert commonpath(path, path.parent.parent) == path.parent.parent
index 448900501b14a9c909e0b54fedd7a80d0f8652ff..a083f4b4f374240d3074273b8d9b48f5950c3d1e 100644 (file)
@@ -362,10 +362,10 @@ class TestPytestPluginManagerBootstrapming:
     def test_plugin_prevent_register_stepwise_on_cacheprovider_unregister(
         self, pytestpm
     ):
-        """ From PR #4304 : The only way to unregister a module is documented at
+        """From PR #4304: The only way to unregister a module is documented at
         the end of https://docs.pytest.org/en/stable/plugins.html.
 
-        When unregister cacheprovider, then unregister stepwise too
+        When unregister cacheprovider, then unregister stepwise too.
         """
         pytestpm.register(42, name="cacheprovider")
         pytestpm.register(43, name="stepwise")
index 46f3e1cabfa57e6fd1b925d3e42ab95a1f59213d..46fab0ce89371fb1806e3f743f3f5df0772a59e1 100644 (file)
@@ -23,7 +23,9 @@ def test_make_hook_recorder(testdir) -> None:
     recorder = testdir.make_hook_recorder(item.config.pluginmanager)
     assert not recorder.getfailures()
 
-    pytest.xfail("internal reportrecorder tests need refactoring")
+    # (The silly condition is to fool mypy that the code below this is reachable)
+    if 1 + 1 == 2:
+        pytest.xfail("internal reportrecorder tests need refactoring")
 
     class rep:
         excinfo = None
@@ -166,18 +168,18 @@ def test_xpassed_with_strict_is_considered_a_failure(testdir) -> None:
 def make_holder():
     class apiclass:
         def pytest_xyz(self, arg):
-            "x"
+            """X"""
 
         def pytest_xyz_noarg(self):
-            "x"
+            """X"""
 
     apimod = type(os)("api")
 
     def pytest_xyz(arg):
-        "x"
+        """X"""
 
     def pytest_xyz_noarg():
-        "x"
+        """X"""
 
     apimod.pytest_xyz = pytest_xyz  # type: ignore
     apimod.pytest_xyz_noarg = pytest_xyz_noarg  # type: ignore
index 08ac014a40b7d2ff77cad3910f0fa830f7341fcd..dbe9489620787330410e51b97ea2794024a515ed 100644 (file)
@@ -1,16 +1,20 @@
 import sys
+from typing import Sequence
+from typing import Union
 
 import pytest
 from _pytest._code.code import ExceptionChainRepr
+from _pytest._code.code import ExceptionRepr
+from _pytest.config import Config
 from _pytest.pathlib import Path
+from _pytest.pytester import Testdir
 from _pytest.reports import CollectReport
 from _pytest.reports import TestReport
 
 
 class TestReportSerialization:
-    def test_xdist_longrepr_to_str_issue_241(self, testdir):
-        """
-        Regarding issue pytest-xdist#241
+    def test_xdist_longrepr_to_str_issue_241(self, testdir: Testdir) -> None:
+        """Regarding issue pytest-xdist#241.
 
         This test came originally from test_remote.py in xdist (ca03269).
         """
@@ -32,7 +36,7 @@ class TestReportSerialization:
         assert test_b_call.outcome == "passed"
         assert test_b_call._to_json()["longrepr"] is None
 
-    def test_xdist_report_longrepr_reprcrash_130(self, testdir) -> None:
+    def test_xdist_report_longrepr_reprcrash_130(self, testdir: Testdir) -> None:
         """Regarding issue pytest-xdist#130
 
         This test came originally from test_remote.py in xdist (ca03269).
@@ -47,15 +51,18 @@ class TestReportSerialization:
         assert len(reports) == 3
         rep = reports[1]
         added_section = ("Failure Metadata", "metadata metadata", "*")
+        assert isinstance(rep.longrepr, ExceptionRepr)
         rep.longrepr.sections.append(added_section)
         d = rep._to_json()
         a = TestReport._from_json(d)
-        assert a.longrepr is not None
+        assert isinstance(a.longrepr, ExceptionRepr)
         # Check assembled == rep
         assert a.__dict__.keys() == rep.__dict__.keys()
         for key in rep.__dict__.keys():
             if key != "longrepr":
                 assert getattr(a, key) == getattr(rep, key)
+        assert rep.longrepr.reprcrash is not None
+        assert a.longrepr.reprcrash is not None
         assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno
         assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message
         assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path
@@ -68,7 +75,7 @@ class TestReportSerialization:
         # Missing section attribute PR171
         assert added_section in a.longrepr.sections
 
-    def test_reprentries_serialization_170(self, testdir) -> None:
+    def test_reprentries_serialization_170(self, testdir: Testdir) -> None:
         """Regarding issue pytest-xdist#170
 
         This test came originally from test_remote.py in xdist (ca03269).
@@ -86,25 +93,35 @@ class TestReportSerialization:
         reports = reprec.getreports("pytest_runtest_logreport")
         assert len(reports) == 3
         rep = reports[1]
+        assert isinstance(rep.longrepr, ExceptionRepr)
         d = rep._to_json()
         a = TestReport._from_json(d)
-        assert a.longrepr is not None
+        assert isinstance(a.longrepr, ExceptionRepr)
 
         rep_entries = rep.longrepr.reprtraceback.reprentries
         a_entries = a.longrepr.reprtraceback.reprentries
         for i in range(len(a_entries)):
-            assert isinstance(rep_entries[i], ReprEntry)
-            assert rep_entries[i].lines == a_entries[i].lines
-            assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno
-            assert (
-                rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message
-            )
-            assert rep_entries[i].reprfileloc.path == a_entries[i].reprfileloc.path
-            assert rep_entries[i].reprfuncargs.args == a_entries[i].reprfuncargs.args
-            assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines
-            assert rep_entries[i].style == a_entries[i].style
-
-    def test_reprentries_serialization_196(self, testdir) -> None:
+            rep_entry = rep_entries[i]
+            assert isinstance(rep_entry, ReprEntry)
+            assert rep_entry.reprfileloc is not None
+            assert rep_entry.reprfuncargs is not None
+            assert rep_entry.reprlocals is not None
+
+            a_entry = a_entries[i]
+            assert isinstance(a_entry, ReprEntry)
+            assert a_entry.reprfileloc is not None
+            assert a_entry.reprfuncargs is not None
+            assert a_entry.reprlocals is not None
+
+            assert rep_entry.lines == a_entry.lines
+            assert rep_entry.reprfileloc.lineno == a_entry.reprfileloc.lineno
+            assert rep_entry.reprfileloc.message == a_entry.reprfileloc.message
+            assert rep_entry.reprfileloc.path == a_entry.reprfileloc.path
+            assert rep_entry.reprfuncargs.args == a_entry.reprfuncargs.args
+            assert rep_entry.reprlocals.lines == a_entry.reprlocals.lines
+            assert rep_entry.style == a_entry.style
+
+    def test_reprentries_serialization_196(self, testdir: Testdir) -> None:
         """Regarding issue pytest-xdist#196
 
         This test came originally from test_remote.py in xdist (ca03269).
@@ -122,9 +139,10 @@ class TestReportSerialization:
         reports = reprec.getreports("pytest_runtest_logreport")
         assert len(reports) == 3
         rep = reports[1]
+        assert isinstance(rep.longrepr, ExceptionRepr)
         d = rep._to_json()
         a = TestReport._from_json(d)
-        assert a.longrepr is not None
+        assert isinstance(a.longrepr, ExceptionRepr)
 
         rep_entries = rep.longrepr.reprtraceback.reprentries
         a_entries = a.longrepr.reprtraceback.reprentries
@@ -132,10 +150,8 @@ class TestReportSerialization:
             assert isinstance(rep_entries[i], ReprEntryNative)
             assert rep_entries[i].lines == a_entries[i].lines
 
-    def test_itemreport_outcomes(self, testdir):
-        """
-        This test came originally from test_remote.py in xdist (ca03269).
-        """
+    def test_itemreport_outcomes(self, testdir: Testdir) -> None:
+        # This test came originally from test_remote.py in xdist (ca03269).
         reprec = testdir.inline_runsource(
             """
             import pytest
@@ -160,7 +176,7 @@ class TestReportSerialization:
             assert newrep.failed == rep.failed
             assert newrep.skipped == rep.skipped
             if newrep.skipped and not hasattr(newrep, "wasxfail"):
-                assert newrep.longrepr is not None
+                assert isinstance(newrep.longrepr, tuple)
                 assert len(newrep.longrepr) == 3
             assert newrep.outcome == rep.outcome
             assert newrep.when == rep.when
@@ -168,7 +184,7 @@ class TestReportSerialization:
             if rep.failed:
                 assert newrep.longreprtext == rep.longreprtext
 
-    def test_collectreport_passed(self, testdir):
+    def test_collectreport_passed(self, testdir: Testdir) -> None:
         """This test came originally from test_remote.py in xdist (ca03269)."""
         reprec = testdir.inline_runsource("def test_func(): pass")
         reports = reprec.getreports("pytest_collectreport")
@@ -179,7 +195,7 @@ class TestReportSerialization:
             assert newrep.failed == rep.failed
             assert newrep.skipped == rep.skipped
 
-    def test_collectreport_fail(self, testdir):
+    def test_collectreport_fail(self, testdir: Testdir) -> None:
         """This test came originally from test_remote.py in xdist (ca03269)."""
         reprec = testdir.inline_runsource("qwe abc")
         reports = reprec.getreports("pytest_collectreport")
@@ -193,13 +209,13 @@ class TestReportSerialization:
             if rep.failed:
                 assert newrep.longrepr == str(rep.longrepr)
 
-    def test_extended_report_deserialization(self, testdir):
+    def test_extended_report_deserialization(self, testdir: Testdir) -> None:
         """This test came originally from test_remote.py in xdist (ca03269)."""
         reprec = testdir.inline_runsource("qwe abc")
         reports = reprec.getreports("pytest_collectreport")
         assert reports
         for rep in reports:
-            rep.extra = True
+            rep.extra = True  # type: ignore[attr-defined]
             d = rep._to_json()
             newrep = CollectReport._from_json(d)
             assert newrep.extra
@@ -209,7 +225,7 @@ class TestReportSerialization:
             if rep.failed:
                 assert newrep.longrepr == str(rep.longrepr)
 
-    def test_paths_support(self, testdir):
+    def test_paths_support(self, testdir: Testdir) -> None:
         """Report attributes which are py.path or pathlib objects should become strings."""
         testdir.makepyfile(
             """
@@ -221,13 +237,13 @@ class TestReportSerialization:
         reports = reprec.getreports("pytest_runtest_logreport")
         assert len(reports) == 3
         test_a_call = reports[1]
-        test_a_call.path1 = testdir.tmpdir
-        test_a_call.path2 = Path(testdir.tmpdir)
+        test_a_call.path1 = testdir.tmpdir  # type: ignore[attr-defined]
+        test_a_call.path2 = Path(testdir.tmpdir)  # type: ignore[attr-defined]
         data = test_a_call._to_json()
         assert data["path1"] == str(testdir.tmpdir)
         assert data["path2"] == str(testdir.tmpdir)
 
-    def test_deserialization_failure(self, testdir):
+    def test_deserialization_failure(self, testdir: Testdir) -> None:
         """Check handling of failure during deserialization of report types."""
         testdir.makepyfile(
             """
@@ -250,7 +266,7 @@ class TestReportSerialization:
             TestReport._from_json(data)
 
     @pytest.mark.parametrize("report_class", [TestReport, CollectReport])
-    def test_chained_exceptions(self, testdir, tw_mock, report_class):
+    def test_chained_exceptions(self, testdir: Testdir, tw_mock, report_class) -> None:
         """Check serialization/deserialization of report objects containing chained exceptions (#5786)"""
         testdir.makepyfile(
             """
@@ -270,7 +286,9 @@ class TestReportSerialization:
 
         reprec = testdir.inline_run()
         if report_class is TestReport:
-            reports = reprec.getreports("pytest_runtest_logreport")
+            reports = reprec.getreports(
+                "pytest_runtest_logreport"
+            )  # type: Union[Sequence[TestReport], Sequence[CollectReport]]
             # we have 3 reports: setup/call/teardown
             assert len(reports) == 3
             # get the call report
@@ -282,7 +300,7 @@ class TestReportSerialization:
             assert len(reports) == 2
             report = reports[1]
 
-        def check_longrepr(longrepr):
+        def check_longrepr(longrepr: ExceptionChainRepr) -> None:
             """Check the attributes of the given longrepr object according to the test file.
 
             We can get away with testing both CollectReport and TestReport with this function because
@@ -306,6 +324,7 @@ class TestReportSerialization:
 
         assert report.failed
         assert len(report.sections) == 0
+        assert isinstance(report.longrepr, ExceptionChainRepr)
         report.longrepr.addsection("title", "contents", "=")
         check_longrepr(report.longrepr)
 
@@ -320,7 +339,7 @@ class TestReportSerialization:
         # elsewhere and we do check the contents of the longrepr object after loading it.
         loaded_report.longrepr.toterminal(tw_mock)
 
-    def test_chained_exceptions_no_reprcrash(self, testdir, tw_mock) -> None:
+    def test_chained_exceptions_no_reprcrash(self, testdir: Testdir, tw_mock) -> None:
         """Regression test for tracebacks without a reprcrash (#5971)
 
         This happens notably on exceptions raised by multiprocess.pool: the exception transfer
@@ -371,7 +390,7 @@ class TestReportSerialization:
 
         reports = reprec.getreports("pytest_runtest_logreport")
 
-        def check_longrepr(longrepr) -> None:
+        def check_longrepr(longrepr: object) -> None:
             assert isinstance(longrepr, ExceptionChainRepr)
             assert len(longrepr.chain) == 2
             entry1, entry2 = longrepr.chain
@@ -400,9 +419,12 @@ class TestReportSerialization:
 
         # for same reasons as previous test, ensure we don't blow up here
         assert loaded_report.longrepr is not None
+        assert isinstance(loaded_report.longrepr, ExceptionChainRepr)
         loaded_report.longrepr.toterminal(tw_mock)
 
-    def test_report_prevent_ConftestImportFailure_hiding_exception(self, testdir):
+    def test_report_prevent_ConftestImportFailure_hiding_exception(
+        self, testdir: Testdir
+    ) -> None:
         sub_dir = testdir.tmpdir.join("ns").ensure_dir()
         sub_dir.join("conftest").new(ext=".py").write("import unknown")
 
@@ -414,7 +436,7 @@ class TestReportSerialization:
 class TestHooks:
     """Test that the hooks are working correctly for plugins"""
 
-    def test_test_report(self, testdir, pytestconfig):
+    def test_test_report(self, testdir: Testdir, pytestconfig: Config) -> None:
         testdir.makepyfile(
             """
             def test_a(): assert False
@@ -436,7 +458,7 @@ class TestHooks:
             assert new_rep.when == rep.when
             assert new_rep.outcome == rep.outcome
 
-    def test_collect_report(self, testdir, pytestconfig):
+    def test_collect_report(self, testdir: Testdir, pytestconfig: Config) -> None:
         testdir.makepyfile(
             """
             def test_a(): assert False
@@ -461,7 +483,9 @@ class TestHooks:
     @pytest.mark.parametrize(
         "hook_name", ["pytest_runtest_logreport", "pytest_collectreport"]
     )
-    def test_invalid_report_types(self, testdir, pytestconfig, hook_name):
+    def test_invalid_report_types(
+        self, testdir: Testdir, pytestconfig: Config, hook_name: str
+    ) -> None:
         testdir.makepyfile(
             """
             def test_a(): pass
diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py
deleted file mode 100644 (file)
index 8fc93d2..0000000
+++ /dev/null
@@ -1,252 +0,0 @@
-import os
-from io import StringIO
-
-import _pytest._code
-import pytest
-from _pytest.resultlog import pytest_configure
-from _pytest.resultlog import pytest_unconfigure
-from _pytest.resultlog import ResultLog
-from _pytest.resultlog import resultlog_key
-
-pytestmark = pytest.mark.filterwarnings("ignore:--result-log is deprecated")
-
-
-def test_write_log_entry():
-    reslog = ResultLog(None, None)
-    reslog.logfile = StringIO()
-    reslog.write_log_entry("name", ".", "")
-    entry = reslog.logfile.getvalue()
-    assert entry[-1] == "\n"
-    entry_lines = entry.splitlines()
-    assert len(entry_lines) == 1
-    assert entry_lines[0] == ". name"
-
-    reslog.logfile = StringIO()
-    reslog.write_log_entry("name", "s", "Skipped")
-    entry = reslog.logfile.getvalue()
-    assert entry[-1] == "\n"
-    entry_lines = entry.splitlines()
-    assert len(entry_lines) == 2
-    assert entry_lines[0] == "s name"
-    assert entry_lines[1] == " Skipped"
-
-    reslog.logfile = StringIO()
-    reslog.write_log_entry("name", "s", "Skipped\n")
-    entry = reslog.logfile.getvalue()
-    assert entry[-1] == "\n"
-    entry_lines = entry.splitlines()
-    assert len(entry_lines) == 2
-    assert entry_lines[0] == "s name"
-    assert entry_lines[1] == " Skipped"
-
-    reslog.logfile = StringIO()
-    longrepr = " tb1\n tb 2\nE tb3\nSome Error"
-    reslog.write_log_entry("name", "F", longrepr)
-    entry = reslog.logfile.getvalue()
-    assert entry[-1] == "\n"
-    entry_lines = entry.splitlines()
-    assert len(entry_lines) == 5
-    assert entry_lines[0] == "F name"
-    assert entry_lines[1:] == [" " + line for line in longrepr.splitlines()]
-
-
-class TestWithFunctionIntegration:
-    # XXX (hpk) i think that the resultlog plugin should
-    # provide a Parser object so that one can remain
-    # ignorant regarding formatting details.
-    def getresultlog(self, testdir, arg):
-        resultlog = testdir.tmpdir.join("resultlog")
-        testdir.plugins.append("resultlog")
-        args = ["--resultlog=%s" % resultlog] + [arg]
-        testdir.runpytest(*args)
-        return [x for x in resultlog.readlines(cr=0) if x]
-
-    def test_collection_report(self, testdir):
-        ok = testdir.makepyfile(test_collection_ok="")
-        fail = testdir.makepyfile(test_collection_fail="XXX")
-        lines = self.getresultlog(testdir, ok)
-        assert not lines
-
-        lines = self.getresultlog(testdir, fail)
-        assert lines
-        assert lines[0].startswith("F ")
-        assert lines[0].endswith("test_collection_fail.py"), lines[0]
-        for x in lines[1:]:
-            assert x.startswith(" ")
-        assert "XXX" in "".join(lines[1:])
-
-    def test_log_test_outcomes(self, testdir):
-        mod = testdir.makepyfile(
-            test_mod="""
-            import pytest
-            def test_pass(): pass
-            def test_skip(): pytest.skip("hello")
-            def test_fail(): raise ValueError("FAIL")
-
-            @pytest.mark.xfail
-            def test_xfail(): raise ValueError("XFAIL")
-            @pytest.mark.xfail
-            def test_xpass(): pass
-
-        """
-        )
-        lines = self.getresultlog(testdir, mod)
-        assert len(lines) >= 3
-        assert lines[0].startswith(". ")
-        assert lines[0].endswith("test_pass")
-        assert lines[1].startswith("s "), lines[1]
-        assert lines[1].endswith("test_skip")
-        assert lines[2].find("hello") != -1
-
-        assert lines[3].startswith("F ")
-        assert lines[3].endswith("test_fail")
-        tb = "".join(lines[4:8])
-        assert tb.find('raise ValueError("FAIL")') != -1
-
-        assert lines[8].startswith("x ")
-        tb = "".join(lines[8:14])
-        assert tb.find('raise ValueError("XFAIL")') != -1
-
-        assert lines[14].startswith("X ")
-        assert len(lines) == 15
-
-    @pytest.mark.parametrize("style", ("native", "long", "short"))
-    def test_internal_exception(self, style):
-        # they are produced for example by a teardown failing
-        # at the end of the run or a failing hook invocation
-        try:
-            raise ValueError
-        except ValueError:
-            excinfo = _pytest._code.ExceptionInfo.from_current()
-        reslog = ResultLog(None, StringIO())
-        reslog.pytest_internalerror(excinfo.getrepr(style=style))
-        entry = reslog.logfile.getvalue()
-        entry_lines = entry.splitlines()
-
-        assert entry_lines[0].startswith("! ")
-        if style != "native":
-            assert os.path.basename(__file__)[:-9] in entry_lines[0]  # .pyc/class
-        assert entry_lines[-1][0] == " "
-        assert "ValueError" in entry
-
-
-def test_generic(testdir, LineMatcher):
-    testdir.plugins.append("resultlog")
-    testdir.makepyfile(
-        """
-        import pytest
-        def test_pass():
-            pass
-        def test_fail():
-            assert 0
-        def test_skip():
-            pytest.skip("")
-        @pytest.mark.xfail
-        def test_xfail():
-            assert 0
-        @pytest.mark.xfail(run=False)
-        def test_xfail_norun():
-            assert 0
-    """
-    )
-    testdir.runpytest("--resultlog=result.log")
-    lines = testdir.tmpdir.join("result.log").readlines(cr=0)
-    LineMatcher(lines).fnmatch_lines(
-        [
-            ". *:test_pass",
-            "F *:test_fail",
-            "s *:test_skip",
-            "x *:test_xfail",
-            "x *:test_xfail_norun",
-        ]
-    )
-
-
-def test_makedir_for_resultlog(testdir, LineMatcher):
-    """--resultlog should automatically create directories for the log file"""
-    testdir.plugins.append("resultlog")
-    testdir.makepyfile(
-        """
-        import pytest
-        def test_pass():
-            pass
-    """
-    )
-    testdir.runpytest("--resultlog=path/to/result.log")
-    lines = testdir.tmpdir.join("path/to/result.log").readlines(cr=0)
-    LineMatcher(lines).fnmatch_lines([". *:test_pass"])
-
-
-def test_no_resultlog_on_workers(testdir):
-    config = testdir.parseconfig("-p", "resultlog", "--resultlog=resultlog")
-
-    assert resultlog_key not in config._store
-    pytest_configure(config)
-    assert resultlog_key in config._store
-    pytest_unconfigure(config)
-    assert resultlog_key not in config._store
-
-    config.workerinput = {}
-    pytest_configure(config)
-    assert resultlog_key not in config._store
-    pytest_unconfigure(config)
-    assert resultlog_key not in config._store
-
-
-def test_unknown_teststatus(testdir):
-    """Ensure resultlog correctly handles unknown status from pytest_report_teststatus
-
-    Inspired on pytest-rerunfailures.
-    """
-    testdir.makepyfile(
-        """
-        def test():
-            assert 0
-    """
-    )
-    testdir.makeconftest(
-        """
-        import pytest
-
-        def pytest_report_teststatus(report):
-            if report.outcome == 'rerun':
-                return "rerun", "r", "RERUN"
-
-        @pytest.hookimpl(hookwrapper=True)
-        def pytest_runtest_makereport():
-            res = yield
-            report = res.get_result()
-            if report.when == "call":
-                report.outcome = 'rerun'
-    """
-    )
-    result = testdir.runpytest("--resultlog=result.log")
-    result.stdout.fnmatch_lines(
-        ["test_unknown_teststatus.py r *[[]100%[]]", "* 1 rerun *"]
-    )
-
-    lines = testdir.tmpdir.join("result.log").readlines(cr=0)
-    assert lines[0] == "r test_unknown_teststatus.py::test"
-
-
-def test_failure_issue380(testdir):
-    testdir.makeconftest(
-        """
-        import pytest
-        class MyCollector(pytest.File):
-            def collect(self):
-                raise ValueError()
-            def repr_failure(self, excinfo):
-                return "somestring"
-        def pytest_collect_file(path, parent):
-            return MyCollector(parent=parent, fspath=path)
-    """
-    )
-    testdir.makepyfile(
-        """
-        def test_func():
-            pass
-    """
-    )
-    result = testdir.runpytest("--resultlog=log")
-    assert result.ret == 2
index b207ccc927f33f03c9722c25c514c80344970946..b9d22370a7b82c9cf27fdbbbc39b0541424eac6d 100644 (file)
@@ -310,7 +310,7 @@ class BaseFunctionalTests:
         assert reps[5].failed
 
     def test_exact_teardown_issue1206(self, testdir) -> None:
-        """issue shadowing error with wrong number of arguments on teardown_method."""
+        """Issue shadowing error with wrong number of arguments on teardown_method."""
         rec = testdir.inline_runsource(
             """
             import pytest
@@ -742,7 +742,7 @@ def test_importorskip_dev_module(monkeypatch) -> None:
 
 
 def test_importorskip_module_level(testdir) -> None:
-    """importorskip must be able to skip entire modules when used at module level"""
+    """`importorskip` must be able to skip entire modules when used at module level."""
     testdir.makepyfile(
         """
         import pytest
@@ -757,7 +757,7 @@ def test_importorskip_module_level(testdir) -> None:
 
 
 def test_importorskip_custom_reason(testdir) -> None:
-    """make sure custom reasons are used"""
+    """Make sure custom reasons are used."""
     testdir.makepyfile(
         """
         import pytest
@@ -871,9 +871,8 @@ def test_makereport_getsource_dynamic_code(testdir, monkeypatch) -> None:
 
 
 def test_store_except_info_on_error() -> None:
-    """ Test that upon test failure, the exception info is stored on
-    sys.last_traceback and friends.
-    """
+    """Test that upon test failure, the exception info is stored on
+    sys.last_traceback and friends."""
     # Simulate item that might raise a specific exception, depending on `raise_error` class var
     class ItemMightRaise:
         nodeid = "item_that_raises"
@@ -934,9 +933,7 @@ def test_current_test_env_var(testdir, monkeypatch) -> None:
 
 
 class TestReportContents:
-    """
-    Test user-level API of ``TestReport`` objects.
-    """
+    """Test user-level API of ``TestReport`` objects."""
 
     def getrunner(self):
         return lambda item: runner.runtestprotocol(item, log=False)
index 1b5d973717778020b1a8806e1ae33c44c0a4ffe9..1abb35043b7d60cebd45deaa672280adc7d70bbd 100644 (file)
@@ -1,7 +1,4 @@
-"""
- test correct setup/teardowns at
- module, class, and instance level
-"""
+"""Test correct setup/teardowns at module, class, and instance level."""
 from typing import List
 
 import pytest
@@ -246,7 +243,7 @@ def test_setup_funcarg_setup_when_outer_scope_fails(testdir):
 def test_setup_teardown_function_level_with_optional_argument(
     testdir, monkeypatch, arg: str,
 ) -> None:
-    """parameter to setup/teardown xunit-style functions parameter is now optional (#1728)."""
+    """Parameter to setup/teardown xunit-style functions parameter is now optional (#1728)."""
     import sys
 
     trace_setups_teardowns = []  # type: List[str]
index 221c32a310bf1bcadfaa06624ee3aae616f4467f..a43c850696e28b23287e5549db8752bbb9f84948 100644 (file)
@@ -254,7 +254,7 @@ def test_capturing(testdir):
 
 
 def test_show_fixtures_and_execute_test(testdir):
-    """ Verifies that setups are shown and tests are executed. """
+    """Verify that setups are shown and tests are executed."""
     p = testdir.makepyfile(
         """
         import pytest
index 64b464b32dd0ebae1264519d2f1433d3cf86906f..929e883cce2fa451e2f8eb7b85280ae2c6c2c835 100644 (file)
@@ -1,5 +1,5 @@
 def test_show_fixtures_and_test(testdir, dummy_yaml_custom_test):
-    """ Verifies that fixtures are not executed. """
+    """Verify that fixtures are not executed."""
     testdir.makepyfile(
         """
         import pytest
@@ -20,8 +20,7 @@ def test_show_fixtures_and_test(testdir, dummy_yaml_custom_test):
 
 
 def test_show_multi_test_fixture_setup_and_teardown_correctly_simple(testdir):
-    """
-    Verify that when a fixture lives for longer than a single test, --setup-plan
+    """Verify that when a fixture lives for longer than a single test, --setup-plan
     correctly displays the SETUP/TEARDOWN indicators the right number of times.
 
     As reported in https://github.com/pytest-dev/pytest/issues/2049
@@ -68,9 +67,7 @@ def test_show_multi_test_fixture_setup_and_teardown_correctly_simple(testdir):
 
 
 def test_show_multi_test_fixture_setup_and_teardown_same_as_setup_show(testdir):
-    """
-    Verify that SETUP/TEARDOWN messages match what comes out of --setup-show.
-    """
+    """Verify that SETUP/TEARDOWN messages match what comes out of --setup-show."""
     testdir.makepyfile(
         """
         import pytest
index 92182ff382fa041bc5c49627889b43774e295ef6..b32d2267d2196b1f78a0c67ba32b37c951422756 100644 (file)
@@ -188,9 +188,7 @@ class TestXFail:
         assert callreport.wasxfail == "this is an xfail"
 
     def test_xfail_using_platform(self, testdir):
-        """
-        Verify that platform can be used with xfail statements.
-        """
+        """Verify that platform can be used with xfail statements."""
         item = testdir.getitem(
             """
             import pytest
@@ -476,9 +474,8 @@ class TestXFail:
         result.stdout.fnmatch_lines([matchline])
 
     def test_strict_sanity(self, testdir):
-        """sanity check for xfail(strict=True): a failing test should behave
-        exactly like a normal xfail.
-        """
+        """Sanity check for xfail(strict=True): a failing test should behave
+        exactly like a normal xfail."""
         p = testdir.makepyfile(
             """
             import pytest
@@ -1137,9 +1134,7 @@ def test_xfail_item(testdir):
 
 
 def test_module_level_skip_error(testdir):
-    """
-    Verify that using pytest.skip at module level causes a collection error
-    """
+    """Verify that using pytest.skip at module level causes a collection error."""
     testdir.makepyfile(
         """
         import pytest
@@ -1156,9 +1151,7 @@ def test_module_level_skip_error(testdir):
 
 
 def test_module_level_skip_with_allow_module_level(testdir):
-    """
-    Verify that using pytest.skip(allow_module_level=True) is allowed
-    """
+    """Verify that using pytest.skip(allow_module_level=True) is allowed."""
     testdir.makepyfile(
         """
         import pytest
@@ -1173,9 +1166,7 @@ def test_module_level_skip_with_allow_module_level(testdir):
 
 
 def test_invalid_skip_keyword_parameter(testdir):
-    """
-    Verify that using pytest.skip() with unknown parameter raises an error
-    """
+    """Verify that using pytest.skip() with unknown parameter raises an error."""
     testdir.makepyfile(
         """
         import pytest
index 19aff99545c85c7cc1679dba1bd1160840f64b83..57db1b9a529183f0db7184029e23d28c2cd7086c 100644 (file)
@@ -1,6 +1,4 @@
-"""
-terminal reporting of the full testing process.
-"""
+"""Terminal reporting of the full testing process."""
 import collections
 import os
 import sys
@@ -20,6 +18,7 @@ import pytest
 from _pytest._io.wcwidth import wcswidth
 from _pytest.config import Config
 from _pytest.config import ExitCode
+from _pytest.pathlib import Path
 from _pytest.pytester import Testdir
 from _pytest.reports import BaseReport
 from _pytest.reports import CollectReport
@@ -440,13 +439,13 @@ class TestCollectonly:
         )
 
     def test_collectonly_missing_path(self, testdir):
-        """this checks issue 115,
-            failure in parseargs will cause session
-            not to have the items attribute
-        """
+        """Issue 115: failure in parseargs will cause session not to
+        have the items attribute."""
         result = testdir.runpytest("--collect-only", "uhm_missing_path")
         assert result.ret == 4
-        result.stderr.fnmatch_lines(["*ERROR: file not found*"])
+        result.stderr.fnmatch_lines(
+            ["*ERROR: file or directory not found: uhm_missing_path"]
+        )
 
     def test_collectonly_quiet(self, testdir):
         testdir.makepyfile("def test_foo(): pass")
@@ -531,7 +530,7 @@ class TestFixtureReporting:
         )
 
     def test_setup_teardown_output_and_test_failure(self, testdir):
-        """ Test for issue #442 """
+        """Test for issue #442."""
         testdir.makepyfile(
             """
             def setup_function(function):
@@ -1076,9 +1075,7 @@ def test_color_no(testdir):
 
 @pytest.mark.parametrize("verbose", [True, False])
 def test_color_yes_collection_on_non_atty(testdir, verbose):
-    """skip collect progress report when working on non-terminals.
-    #1397
-    """
+    """#1397: Skip collect progress report when working on non-terminals."""
     testdir.makepyfile(
         """
         import pytest
@@ -1208,9 +1205,8 @@ def test_traceconfig(testdir):
 
 
 class TestGenericReporting:
-    """ this test class can be subclassed with a different option
-        provider to run e.g. distributed tests.
-    """
+    """Test class which can be subclassed with a different option provider to
+    run e.g. distributed tests."""
 
     def test_collect_fail(self, testdir, option):
         testdir.makepyfile("import xyz\n")
@@ -1564,66 +1560,66 @@ def tr() -> TerminalReporter:
         # dict value, not the actual contents, so tuples of anything
         # suffice
         # Important statuses -- the highest priority of these always wins
-        ("red", [("1 failed", {"bold": True, "red": True})], {"failed": (1,)}),
+        ("red", [("1 failed", {"bold": True, "red": True})], {"failed": [1]}),
         (
             "red",
             [
                 ("1 failed", {"bold": True, "red": True}),
                 ("1 passed", {"bold": False, "green": True}),
             ],
-            {"failed": (1,), "passed": (1,)},
+            {"failed": [1], "passed": [1]},
         ),
-        ("red", [("1 error", {"bold": True, "red": True})], {"error": (1,)}),
-        ("red", [("2 errors", {"bold": True, "red": True})], {"error": (1, 2)}),
+        ("red", [("1 error", {"bold": True, "red": True})], {"error": [1]}),
+        ("red", [("2 errors", {"bold": True, "red": True})], {"error": [1, 2]}),
         (
             "red",
             [
                 ("1 passed", {"bold": False, "green": True}),
                 ("1 error", {"bold": True, "red": True}),
             ],
-            {"error": (1,), "passed": (1,)},
+            {"error": [1], "passed": [1]},
         ),
         # (a status that's not known to the code)
-        ("yellow", [("1 weird", {"bold": True, "yellow": True})], {"weird": (1,)}),
+        ("yellow", [("1 weird", {"bold": True, "yellow": True})], {"weird": [1]}),
         (
             "yellow",
             [
                 ("1 passed", {"bold": False, "green": True}),
                 ("1 weird", {"bold": True, "yellow": True}),
             ],
-            {"weird": (1,), "passed": (1,)},
+            {"weird": [1], "passed": [1]},
         ),
-        ("yellow", [("1 warning", {"bold": True, "yellow": True})], {"warnings": (1,)}),
+        ("yellow", [("1 warning", {"bold": True, "yellow": True})], {"warnings": [1]}),
         (
             "yellow",
             [
                 ("1 passed", {"bold": False, "green": True}),
                 ("1 warning", {"bold": True, "yellow": True}),
             ],
-            {"warnings": (1,), "passed": (1,)},
+            {"warnings": [1], "passed": [1]},
         ),
         (
             "green",
             [("5 passed", {"bold": True, "green": True})],
-            {"passed": (1, 2, 3, 4, 5)},
+            {"passed": [1, 2, 3, 4, 5]},
         ),
         # "Boring" statuses.  These have no effect on the color of the summary
         # line.  Thus, if *every* test has a boring status, the summary line stays
         # at its default color, i.e. yellow, to warn the user that the test run
         # produced no useful information
-        ("yellow", [("1 skipped", {"bold": True, "yellow": True})], {"skipped": (1,)}),
+        ("yellow", [("1 skipped", {"bold": True, "yellow": True})], {"skipped": [1]}),
         (
             "green",
             [
                 ("1 passed", {"bold": True, "green": True}),
                 ("1 skipped", {"bold": False, "yellow": True}),
             ],
-            {"skipped": (1,), "passed": (1,)},
+            {"skipped": [1], "passed": [1]},
         ),
         (
             "yellow",
             [("1 deselected", {"bold": True, "yellow": True})],
-            {"deselected": (1,)},
+            {"deselected": [1]},
         ),
         (
             "green",
@@ -1631,34 +1627,34 @@ def tr() -> TerminalReporter:
                 ("1 passed", {"bold": True, "green": True}),
                 ("1 deselected", {"bold": False, "yellow": True}),
             ],
-            {"deselected": (1,), "passed": (1,)},
+            {"deselected": [1], "passed": [1]},
         ),
-        ("yellow", [("1 xfailed", {"bold": True, "yellow": True})], {"xfailed": (1,)}),
+        ("yellow", [("1 xfailed", {"bold": True, "yellow": True})], {"xfailed": [1]}),
         (
             "green",
             [
                 ("1 passed", {"bold": True, "green": True}),
                 ("1 xfailed", {"bold": False, "yellow": True}),
             ],
-            {"xfailed": (1,), "passed": (1,)},
+            {"xfailed": [1], "passed": [1]},
         ),
-        ("yellow", [("1 xpassed", {"bold": True, "yellow": True})], {"xpassed": (1,)}),
+        ("yellow", [("1 xpassed", {"bold": True, "yellow": True})], {"xpassed": [1]}),
         (
             "yellow",
             [
                 ("1 passed", {"bold": False, "green": True}),
                 ("1 xpassed", {"bold": True, "yellow": True}),
             ],
-            {"xpassed": (1,), "passed": (1,)},
+            {"xpassed": [1], "passed": [1]},
         ),
         # Likewise if no tests were found at all
         ("yellow", [("no tests ran", {"yellow": True})], {}),
         # Test the empty-key special case
-        ("yellow", [("no tests ran", {"yellow": True})], {"": (1,)}),
+        ("yellow", [("no tests ran", {"yellow": True})], {"": [1]}),
         (
             "green",
             [("1 passed", {"bold": True, "green": True})],
-            {"": (1,), "passed": (1,)},
+            {"": [1], "passed": [1]},
         ),
         # A couple more complex combinations
         (
@@ -1668,7 +1664,7 @@ def tr() -> TerminalReporter:
                 ("2 passed", {"bold": False, "green": True}),
                 ("3 xfailed", {"bold": False, "yellow": True}),
             ],
-            {"passed": (1, 2), "failed": (1,), "xfailed": (1, 2, 3)},
+            {"passed": [1, 2], "failed": [1], "xfailed": [1, 2, 3]},
         ),
         (
             "green",
@@ -1679,10 +1675,10 @@ def tr() -> TerminalReporter:
                 ("2 xfailed", {"bold": False, "yellow": True}),
             ],
             {
-                "passed": (1,),
-                "skipped": (1, 2),
-                "deselected": (1, 2, 3),
-                "xfailed": (1, 2),
+                "passed": [1],
+                "skipped": [1, 2],
+                "deselected": [1, 2, 3],
+                "xfailed": [1, 2],
             },
         ),
     ],
@@ -1691,7 +1687,7 @@ def test_summary_stats(
     tr: TerminalReporter,
     exp_line: List[Tuple[str, Dict[str, bool]]],
     exp_color: str,
-    stats_arg: Dict[str, List],
+    stats_arg: Dict[str, List[object]],
 ) -> None:
     tr.stats = stats_arg
 
@@ -2090,7 +2086,7 @@ def test_skip_reasons_folding() -> None:
     ev3.longrepr = longrepr
     ev3.skipped = True
 
-    values = _folded_skips(py.path.local(), [ev1, ev2, ev3])
+    values = _folded_skips(Path.cwd(), [ev1, ev2, ev3])
     assert len(values) == 1
     num, fspath, lineno_, reason = values[0]
     assert num == 3
index 26a34c6565dcfb79000818ccab88e0312f9228f3..cc03385f3a99bf153f82298116489101cb6927d0 100644 (file)
@@ -2,12 +2,14 @@ import os
 import stat
 import sys
 from typing import Callable
+from typing import cast
 from typing import List
 
 import attr
 
 import pytest
 from _pytest import pathlib
+from _pytest.config import Config
 from _pytest.pathlib import cleanup_numbered_dir
 from _pytest.pathlib import create_cleanup_lock
 from _pytest.pathlib import make_numbered_dir
@@ -45,7 +47,7 @@ class FakeConfig:
 
 class TestTempdirHandler:
     def test_mktemp(self, tmp_path):
-        config = FakeConfig(tmp_path)
+        config = cast(Config, FakeConfig(tmp_path))
         t = TempdirFactory(TempPathFactory.from_config(config))
         tmp = t.mktemp("world")
         assert tmp.relto(t.getbasetemp()) == "world0"
@@ -58,7 +60,7 @@ class TestTempdirHandler:
     def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch):
         """#4425"""
         monkeypatch.chdir(tmp_path)
-        config = FakeConfig("hello")
+        config = cast(Config, FakeConfig("hello"))
         t = TempPathFactory.from_config(config)
         assert t.getbasetemp().resolve() == (tmp_path / "hello").resolve()
 
index 6ddc6186be4202088e3373f94601dccdc624dccb..c7b6bfcec9296e359db67470f2281b0cbc46b41a 100644 (file)
@@ -1,4 +1,5 @@
 import gc
+import sys
 from typing import List
 
 import pytest
@@ -1196,9 +1197,7 @@ def test_pdb_teardown_called(testdir, monkeypatch) -> None:
 
 @pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"])
 def test_pdb_teardown_skipped(testdir, monkeypatch, mark: str) -> None:
-    """
-    With --pdb, setUp and tearDown should not be called for skipped tests.
-    """
+    """With --pdb, setUp and tearDown should not be called for skipped tests."""
     tracked = []  # type: List[str]
     monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False)
 
@@ -1243,3 +1242,26 @@ def test_asynctest_support(testdir):
     testdir.copy_example("unittest/test_unittest_asynctest.py")
     reprec = testdir.inline_run()
     reprec.assertoutcome(failed=1, passed=2)
+
+
+def test_plain_unittest_does_not_support_async(testdir):
+    """Async functions in plain unittest.TestCase subclasses are not supported without plugins.
+
+    This test exists here to avoid introducing this support by accident, leading users
+    to expect that it works, rather than doing so intentionally as a feature.
+
+    See https://github.com/pytest-dev/pytest-asyncio/issues/180 for more context.
+    """
+    testdir.copy_example("unittest/test_unittest_plain_async.py")
+    result = testdir.runpytest_subprocess()
+    if hasattr(sys, "pypy_version_info"):
+        # in PyPy we can't reliable get the warning about the coroutine not being awaited,
+        # because it depends on the coroutine being garbage collected; given that
+        # we are running in a subprocess, that's difficult to enforce
+        expected_lines = ["*1 passed*"]
+    else:
+        expected_lines = [
+            "*RuntimeWarning: coroutine * was never awaited",
+            "*1 passed*",
+        ]
+    result.stdout.fnmatch_lines(expected_lines)
index c366818021638025f3e0f802039e58808c2a22fc..b7a231094fd956b3fbaa6531be5902402288aa40 100644 (file)
@@ -13,9 +13,7 @@ WARNINGS_SUMMARY_HEADER = "warnings summary"
 
 @pytest.fixture
 def pyfile_with_warnings(testdir: Testdir, request: FixtureRequest) -> str:
-    """
-    Create a test file which calls a function in a module which generates warnings.
-    """
+    """Create a test file which calls a function in a module which generates warnings."""
     testdir.syspathinsert()
     test_name = request.function.__name__
     module_name = test_name.lstrip("test_") + "_module"
@@ -42,9 +40,7 @@ def pyfile_with_warnings(testdir: Testdir, request: FixtureRequest) -> str:
 
 @pytest.mark.filterwarnings("default")
 def test_normal_flow(testdir, pyfile_with_warnings):
-    """
-    Check that the warnings section is displayed.
-    """
+    """Check that the warnings section is displayed."""
     result = testdir.runpytest(pyfile_with_warnings)
     result.stdout.fnmatch_lines(
         [
@@ -180,9 +176,8 @@ def test_works_with_filterwarnings(testdir):
 
 @pytest.mark.parametrize("default_config", ["ini", "cmdline"])
 def test_filterwarnings_mark(testdir, default_config):
-    """
-    Test ``filterwarnings`` mark works and takes precedence over command line and ini options.
-    """
+    """Test ``filterwarnings`` mark works and takes precedence over command
+    line and ini options."""
     if default_config == "ini":
         testdir.makeini(
             """
@@ -245,9 +240,8 @@ def test_filterwarnings_mark_registration(testdir):
 def test_warning_captured_hook(testdir):
     testdir.makeconftest(
         """
-        from _pytest.warnings import _issue_warning_captured
         def pytest_configure(config):
-            _issue_warning_captured(UserWarning("config warning"), config.hook, stacklevel=2)
+            config.issue_config_time_warning(UserWarning("config warning"), stacklevel=2)
     """
     )
     testdir.makepyfile(
@@ -305,9 +299,7 @@ def test_warning_captured_hook(testdir):
 
 @pytest.mark.filterwarnings("always")
 def test_collection_warnings(testdir):
-    """
-    Check that we also capture warnings issued during test collection (#3251).
-    """
+    """Check that we also capture warnings issued during test collection (#3251)."""
     testdir.makepyfile(
         """
         import warnings
@@ -387,7 +379,7 @@ def test_hide_pytest_internal_warnings(testdir, ignore_pytest_warnings):
 
 @pytest.mark.parametrize("ignore_on_cmdline", [True, False])
 def test_option_precedence_cmdline_over_ini(testdir, ignore_on_cmdline):
-    """filters defined in the command-line should take precedence over filters in ini files (#3946)."""
+    """Filters defined in the command-line should take precedence over filters in ini files (#3946)."""
     testdir.makeini(
         """
         [pytest]
@@ -520,6 +512,9 @@ class TestDeprecationWarningsByDefault:
 
 
 @pytest.mark.parametrize("change_default", [None, "ini", "cmdline"])
+@pytest.mark.skip(
+    reason="This test should be enabled again before pytest 7.0 is released"
+)
 def test_deprecation_warning_as_error(testdir, change_default):
     """This ensures that PytestDeprecationWarnings raised by pytest are turned into errors.
 
@@ -720,10 +715,22 @@ class TestStackLevel:
         assert "config{sep}__init__.py".format(sep=os.sep) in file
         assert func == "_preparse"
 
+    @pytest.mark.filterwarnings("default")
+    def test_conftest_warning_captured(self, testdir: Testdir) -> None:
+        """Warnings raised during importing of conftest.py files is captured (#2891)."""
+        testdir.makeconftest(
+            """
+            import warnings
+            warnings.warn(UserWarning("my custom warning"))
+            """
+        )
+        result = testdir.runpytest()
+        result.stdout.fnmatch_lines(
+            ["conftest.py:2", "*UserWarning: my custom warning*"]
+        )
+
     def test_issue4445_import_plugin(self, testdir, capwarn):
-        """#4445: Make sure the warning points to a reasonable location
-        See origin of _issue_warning_captured at: _pytest.config.__init__.py:585
-        """
+        """#4445: Make sure the warning points to a reasonable location"""
         testdir.makepyfile(
             some_plugin="""
             import pytest
@@ -742,32 +749,7 @@ class TestStackLevel:
 
         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"
+        assert func == "_warn_about_skipped_plugins"
 
     def test_issue4445_issue5928_mark_generator(self, testdir):
         """#4445 and #5928: Make sure the warning from an unknown mark points to
diff --git a/tox.ini b/tox.ini
index c8165a3c3bb443cf2252a1d5bce3009b97f1b0e0..b42aecdf85cf28e1efdfa7d6651dc3226c389b28 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -13,6 +13,7 @@ envlist =
     pypy3
     py37-{pexpect,xdist,unittestextras,numpy,pluggymaster}
     doctesting
+    plugins
     py37-freeze
     docs
     docs-checklinks
@@ -114,6 +115,43 @@ commands =
     rm -rf {envdir}/.pytest_cache
     make regen
 
+[testenv:plugins]
+# use latest versions of all plugins, including pre-releases
+pip_pre=true
+# use latest pip and new dependency resolver (#7783)
+download=true
+install_command=python -m pip --use-feature=2020-resolver install {opts} {packages}
+changedir = testing/plugins_integration
+deps =
+    anyio[curio,trio]
+    django
+    pytest-asyncio
+    pytest-bdd
+    pytest-cov
+    pytest-django
+    pytest-flakes
+    pytest-html
+    pytest-mock
+    pytest-sugar
+    pytest-trio
+    pytest-twisted
+    twisted
+    pytest-xvfb
+setenv =
+    PYTHONPATH=.
+commands =
+    pip check
+    pytest bdd_wallet.py
+    pytest --cov=. simple_integration.py
+    pytest --ds=django_settings simple_integration.py
+    pytest --html=simple.html simple_integration.py
+    pytest pytest_anyio_integration.py
+    pytest pytest_asyncio_integration.py
+    pytest pytest_mock_integration.py
+    pytest pytest_trio_integration.py
+    pytest pytest_twisted_integration.py
+    pytest simple_integration.py --force-sugar --flakes
+
 [testenv:py37-freeze]
 changedir = testing/freeze
 deps =
@@ -154,7 +192,17 @@ commands = python scripts/publish-gh-release-notes.py {posargs}
 
 [flake8]
 max-line-length = 120
-extend-ignore = E203
+extend-ignore =
+    ; whitespace before ':'
+    E203
+    ; Missing Docstrings
+    D100,D101,D102,D103,D104,D105,D106,D107
+    ; Whitespace Issues
+    D202,D203,D204,D205,D209,D213
+    ; Quotes Issues
+    D302
+    ; Docstring Content Issues
+    D400,D401,D401,D402,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D415,D416,D417
 
 [isort]
 ; This config mimics what reorder-python-imports does.