Merge branch 'main' of github.com:king-alexander/pytest

This commit is contained in:
Alexander King 2021-10-29 10:33:44 -04:00
commit 1230bce325
100 changed files with 8417 additions and 1339 deletions

View File

@ -31,7 +31,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install packaging requests tabulate[widechars] pip install packaging requests tabulate[widechars] tqdm
- name: Update Plugin List - name: Update Plugin List
run: python scripts/update-plugin-list.py run: python scripts/update-plugin-list.py

View File

@ -21,7 +21,7 @@ repos:
exclude: _pytest/(debugging|hookspec).py exclude: _pytest/(debugging|hookspec).py
language_version: python3 language_version: python3
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 3.9.2 rev: 4.0.1
hooks: hooks:
- id: flake8 - id: flake8
language_version: python3 language_version: python3
@ -39,7 +39,7 @@ repos:
- id: pyupgrade - id: pyupgrade
args: [--py36-plus] args: [--py36-plus]
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.17.0 rev: v1.18.0
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
args: [--max-py-version=3.10] args: [--max-py-version=3.10]
@ -48,7 +48,7 @@ repos:
hooks: hooks:
- id: python-use-type-annotations - id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.910 rev: v0.910-1
hooks: hooks:
- id: mypy - id: mypy
files: ^(src/|testing/) files: ^(src/|testing/)

View File

@ -7,6 +7,10 @@ python:
- method: pip - method: pip
path: . path: .
build:
apt_packages:
- inkscape
formats: formats:
- epub - epub
- pdf - pdf

View File

@ -13,6 +13,7 @@ Ahn Ki-Wook
Akiomi Kamakura Akiomi Kamakura
Alan Velasco Alan Velasco
Alexander Johnson Alexander Johnson
Alexander King
Alexei Kozlenok Alexei Kozlenok
Allan Feldman Allan Feldman
Aly Sivji Aly Sivji
@ -76,6 +77,7 @@ Christopher Gilling
Claire Cecil Claire Cecil
Claudio Madotto Claudio Madotto
CrazyMerlyn CrazyMerlyn
Cristian Vera
Cyrus Maden Cyrus Maden
Damian Skrzypczak Damian Skrzypczak
Daniel Grana Daniel Grana

1
changelog/451.doc.rst Normal file
View File

@ -0,0 +1 @@
The PDF documentations list of plugins doesnt run off the page anymore.

View File

@ -0,0 +1,8 @@
The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
Note: pytest was not able to provide a deprecation period for this change.

View File

@ -0,0 +1,3 @@
``py.path.local`` arguments for hooks have been deprecated. See :ref:`the deprecation note <legacy-path-hooks-deprecated>` for full details.
``py.path.local`` arguments to Node constructors have been deprecated. See :ref:`the deprecation note <node-ctor-fspath-deprecation>` for full details.

View File

@ -8,5 +8,6 @@ Directly constructing the following classes is now deprecated:
- ``_pytest._code.ExceptionInfo`` - ``_pytest._code.ExceptionInfo``
- ``_pytest.config.argparsing.Parser`` - ``_pytest.config.argparsing.Parser``
- ``_pytest.config.argparsing.OptionGroup`` - ``_pytest.config.argparsing.OptionGroup``
- ``_pytest.pytester.HookRecorder``
These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.0.0. These constructors have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.

View File

@ -12,8 +12,12 @@ The newly-exported types are:
- ``pytest.ExceptionInfo`` for the :class:`ExceptionInfo <pytest.ExceptionInfo>` type returned from :func:`pytest.raises` and passed to various hooks. - ``pytest.ExceptionInfo`` for the :class:`ExceptionInfo <pytest.ExceptionInfo>` type returned from :func:`pytest.raises` and passed to various hooks.
- ``pytest.Parser`` for the :class:`Parser <pytest.Parser>` type passed to the :func:`pytest_addoption <pytest.hookspec.pytest_addoption>` hook. - ``pytest.Parser`` for the :class:`Parser <pytest.Parser>` type passed to the :func:`pytest_addoption <pytest.hookspec.pytest_addoption>` hook.
- ``pytest.OptionGroup`` for the :class:`OptionGroup <pytest.OptionGroup>` type returned from the :func:`parser.addgroup <pytest.Parser.getgroup>` method. - ``pytest.OptionGroup`` for the :class:`OptionGroup <pytest.OptionGroup>` type returned from the :func:`parser.addgroup <pytest.Parser.getgroup>` method.
- ``pytest.HookRecorder`` for the :class:`HookRecorder <pytest.HookRecorder>` type returned from :class:`~pytest.Pytester`.
- ``pytest.RecordedHookCall`` for the :class:`RecordedHookCall <pytest.HookRecorder>` type returned from :class:`~pytest.HookRecorder`.
- ``pytest.RunResult`` for the :class:`RunResult <pytest.RunResult>` type returned from :class:`~pytest.Pytester`.
- ``pytest.LineMatcher`` for the :class:`LineMatcher <pytest.RunResult>` type used in :class:`~pytest.RunResult` and others.
Constructing them directly is not supported; they are only meant for use in type annotations. Constructing most of them directly is not supported; they are only meant for use in type annotations.
Doing so will emit a deprecation warning, and may become a hard-error in pytest 8.0. Doing so will emit a deprecation warning, and may become a hard-error in pytest 8.0.
Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy. Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.

View File

@ -0,0 +1,4 @@
Improved error messages when parsing warning filters.
Previously pytest would show an internal traceback, which besides ugly sometimes would hide the cause
of the problem (for example an ``ImportError`` while importing a specific warning type).

View File

@ -1,2 +1,2 @@
The test selection options ``pytest -k`` and ``pytest -m`` now support matching The test selection options ``pytest -k`` and ``pytest -m`` now support matching
names containing forwardslash (``\/``)characters. names containing forward slash (``/``) characters.

View File

@ -0,0 +1,2 @@
Included the module of the class in the error message about direct
node construction (without using ``from_parent``).

View File

@ -0,0 +1,4 @@
Full diffs are now always shown for equality assertions of iterables when
`CI` or ``BUILD_NUMBER`` is found in the environment, even when ``-v`` isn't
used.

View File

@ -0,0 +1,2 @@
:class:`RunResult <_pytest.pytester.RunResult>` method :meth:`assert_outcomes <_pytest.pytester.RunResult.assert_outcomes>` now accepts a
``deselected`` argument to assert the total number of deselected tests.

View File

@ -0,0 +1 @@
Fixed the URL used by ``--pastebin`` to use `bpa.st <http://bpa.st>`__.

View File

@ -0,0 +1 @@
The end line number and end column offset are now properly set for rewritten assert statements.

View File

@ -0,0 +1 @@
Support for the ``files`` API from ``importlib.resources`` within rewritten files.

View File

@ -0,0 +1 @@
:meth:`pytest.Cache.set` now preserves key order when saving dicts.

1
changelog/9210.doc.rst Normal file
View File

@ -0,0 +1 @@
Remove incorrect docs about ``confcutdir`` being a configuration option: it can only be set through the ``--confcutdir`` command-line option.

View File

@ -23,14 +23,13 @@ a full list of details. A few feature highlights:
called if the corresponding setup method succeeded. called if the corresponding setup method succeeded.
- integrate tab-completion on command line options if you - integrate tab-completion on command line options if you
have `argcomplete <https://pypi.org/project/argcomplete/>`_ have :pypi:`argcomplete` configured.
configured.
- allow boolean expression directly with skipif/xfail - allow boolean expression directly with skipif/xfail
if a "reason" is also specified. if a "reason" is also specified.
- a new hook ``pytest_load_initial_conftests`` allows plugins like - a new hook ``pytest_load_initial_conftests`` allows plugins like
`pytest-django <https://pypi.org/project/pytest-django/>`_ to :pypi:`pytest-django` to
influence the environment before conftest files import ``django``. influence the environment before conftest files import ``django``.
- reporting: color the last line red or green depending if - reporting: color the last line red or green depending if

View File

@ -1004,7 +1004,7 @@ Trivial/Internal Changes
- `#7264 <https://github.com/pytest-dev/pytest/issues/7264>`_: The dependency on the ``wcwidth`` package has been removed. - `#7264 <https://github.com/pytest-dev/pytest/issues/7264>`_: The dependency on the ``wcwidth`` package has been removed.
- `#7291 <https://github.com/pytest-dev/pytest/issues/7291>`_: Replaced ``py.iniconfig`` with `iniconfig <https://pypi.org/project/iniconfig/>`__. - `#7291 <https://github.com/pytest-dev/pytest/issues/7291>`_: Replaced ``py.iniconfig`` with :pypi:`iniconfig`.
- `#7295 <https://github.com/pytest-dev/pytest/issues/7295>`_: ``src/_pytest/config/__init__.py`` now uses the ``warnings`` module to report warnings instead of ``sys.stderr.write``. - `#7295 <https://github.com/pytest-dev/pytest/issues/7295>`_: ``src/_pytest/config/__init__.py`` now uses the ``warnings`` module to report warnings instead of ``sys.stderr.write``.
@ -1795,7 +1795,7 @@ Removals
For more information consult :std:doc:`deprecations` in the docs. For more information consult :std:doc:`deprecations` in the docs.
- `#5565 <https://github.com/pytest-dev/pytest/issues/5565>`_: Removed unused support code for `unittest2 <https://pypi.org/project/unittest2/>`__. - `#5565 <https://github.com/pytest-dev/pytest/issues/5565>`_: Removed unused support code for :pypi:`unittest2`.
The ``unittest2`` backport module is no longer The ``unittest2`` backport module is no longer
necessary since Python 3.3+, and the small amount of code in pytest to support it also doesn't seem necessary since Python 3.3+, and the small amount of code in pytest to support it also doesn't seem
@ -2520,7 +2520,7 @@ Trivial/Internal Changes
- `#4942 <https://github.com/pytest-dev/pytest/issues/4942>`_: ``logging.raiseExceptions`` is not set to ``False`` anymore. - `#4942 <https://github.com/pytest-dev/pytest/issues/4942>`_: ``logging.raiseExceptions`` is not set to ``False`` anymore.
- `#5013 <https://github.com/pytest-dev/pytest/issues/5013>`_: pytest now depends on `wcwidth <https://pypi.org/project/wcwidth>`__ to properly track unicode character sizes for more precise terminal output. - `#5013 <https://github.com/pytest-dev/pytest/issues/5013>`_: pytest now depends on :pypi:`wcwidth` to properly track unicode character sizes for more precise terminal output.
- `#5059 <https://github.com/pytest-dev/pytest/issues/5059>`_: pytester's ``Testdir.popen()`` uses ``stdout`` and ``stderr`` via keyword arguments with defaults now (``subprocess.PIPE``). - `#5059 <https://github.com/pytest-dev/pytest/issues/5059>`_: pytester's ``Testdir.popen()`` uses ``stdout`` and ``stderr`` via keyword arguments with defaults now (``subprocess.PIPE``).
@ -2618,9 +2618,7 @@ Features
- `#4855 <https://github.com/pytest-dev/pytest/issues/4855>`_: The ``--pdbcls`` option handles classes via module attributes now (e.g. - `#4855 <https://github.com/pytest-dev/pytest/issues/4855>`_: The ``--pdbcls`` option handles classes via module attributes now (e.g.
``pdb:pdb.Pdb`` with `pdb++`_), and its validation was improved. ``pdb:pdb.Pdb`` with :pypi:`pdbpp`), and its validation was improved.
.. _pdb++: https://pypi.org/project/pdbpp/
- `#4875 <https://github.com/pytest-dev/pytest/issues/4875>`_: The :confval:`testpaths` configuration option is now displayed next - `#4875 <https://github.com/pytest-dev/pytest/issues/4875>`_: The :confval:`testpaths` configuration option is now displayed next
@ -2691,9 +2689,7 @@ Bug Fixes
Previously they were loaded (imported) always, making e.g. the ``capfd`` fixture available. Previously they were loaded (imported) always, making e.g. the ``capfd`` fixture available.
- `#4968 <https://github.com/pytest-dev/pytest/issues/4968>`_: The pdb ``quit`` command is handled properly when used after the ``debug`` command with `pdb++`_. - `#4968 <https://github.com/pytest-dev/pytest/issues/4968>`_: The pdb ``quit`` command is handled properly when used after the ``debug`` command with :pypi:`pdbpp`.
.. _pdb++: https://pypi.org/project/pdbpp/
- `#4975 <https://github.com/pytest-dev/pytest/issues/4975>`_: Fix the interpretation of ``-qq`` option where it was being considered as ``-v`` instead. - `#4975 <https://github.com/pytest-dev/pytest/issues/4975>`_: Fix the interpretation of ``-qq`` option where it was being considered as ``-v`` instead.
@ -3124,7 +3120,7 @@ Features
will not issue the warning. will not issue the warning.
- `#3632 <https://github.com/pytest-dev/pytest/issues/3632>`_: Richer equality comparison introspection on ``AssertionError`` for objects created using `attrs <https://www.attrs.org/en/stable/>`__ or :mod:`dataclasses` (Python 3.7+, `backported to 3.6 <https://pypi.org/project/dataclasses>`__). - `#3632 <https://github.com/pytest-dev/pytest/issues/3632>`_: Richer equality comparison introspection on ``AssertionError`` for objects created using `attrs <https://www.attrs.org/en/stable/>`__ or :mod:`dataclasses` (Python 3.7+, :pypi:`backported to 3.6 <dataclasses>`).
- `#4278 <https://github.com/pytest-dev/pytest/issues/4278>`_: ``CACHEDIR.TAG`` files are now created inside cache directories. - `#4278 <https://github.com/pytest-dev/pytest/issues/4278>`_: ``CACHEDIR.TAG`` files are now created inside cache directories.
@ -4865,8 +4861,7 @@ Features
markers. Also, a ``caplog`` fixture is available that enables users to test markers. Also, a ``caplog`` fixture is available that enables users to test
the captured log during specific tests (similar to ``capsys`` for example). the captured log during specific tests (similar to ``capsys`` for example).
For more information, please see the :doc:`logging docs <how-to/logging>`. This feature was For more information, please see the :doc:`logging docs <how-to/logging>`. This feature was
introduced by merging the popular `pytest-catchlog introduced by merging the popular :pypi:`pytest-catchlog` plugin, thanks to `Thomas Hisch
<https://pypi.org/project/pytest-catchlog/>`_ plugin, thanks to `Thomas Hisch
<https://github.com/thisch>`_. Be advised that during the merging the <https://github.com/thisch>`_. Be advised that during the merging the
backward compatibility interface with the defunct ``pytest-capturelog`` has backward compatibility interface with the defunct ``pytest-capturelog`` has
been dropped. (`#2794 <https://github.com/pytest-dev/pytest/issues/2794>`_) been dropped. (`#2794 <https://github.com/pytest-dev/pytest/issues/2794>`_)
@ -4943,7 +4938,7 @@ Bug Fixes
Trivial/Internal Changes Trivial/Internal Changes
------------------------ ------------------------
- pytest now depends on `attrs <https://pypi.org/project/attrs/>`__ for internal - pytest now depends on :pypi:`attrs` for internal
structures to ease code maintainability. (`#2641 structures to ease code maintainability. (`#2641
<https://github.com/pytest-dev/pytest/issues/2641>`_) <https://github.com/pytest-dev/pytest/issues/2641>`_)

View File

@ -17,6 +17,7 @@
# The short X.Y version. # The short X.Y version.
import ast import ast
import os import os
import shutil
import sys import sys
from typing import List from typing import List
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -38,6 +39,10 @@ autodoc_member_order = "bysource"
autodoc_typehints = "description" autodoc_typehints = "description"
todo_include_todos = 1 todo_include_todos = 1
# Use a different latex engine due to possible Unicode characters in the documentation:
# https://docs.readthedocs.io/en/stable/guides/pdf-non-ascii-languages.html
latex_engine = "xelatex"
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
@ -50,6 +55,7 @@ extensions = [
"pygments_pytest", "pygments_pytest",
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.autosummary", "sphinx.ext.autosummary",
"sphinx.ext.extlinks",
"sphinx.ext.intersphinx", "sphinx.ext.intersphinx",
"sphinx.ext.todo", "sphinx.ext.todo",
"sphinx.ext.viewcode", "sphinx.ext.viewcode",
@ -57,6 +63,13 @@ extensions = [
"sphinxcontrib_trio", "sphinxcontrib_trio",
] ]
# Building PDF docs on readthedocs requires inkscape for svg to pdf
# conversion. The relevant plugin is not useful for normal HTML builds, but
# it still raises warnings and fails CI if inkscape is not available. So
# only use the plugin if inkscape is actually available.
if shutil.which("inkscape"):
extensions.append("sphinxcontrib.inkscapeconverter")
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ["_templates"]
@ -133,6 +146,11 @@ linkcheck_ignore = [
linkcheck_workers = 5 linkcheck_workers = 5
extlinks = {
"pypi": ("https://pypi.org/project/%s/", ""),
}
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------------
sys.path.append(os.path.abspath("_themes")) sys.path.append(os.path.abspath("_themes"))
@ -350,6 +368,14 @@ intersphinx_mapping = {
"pluggy": ("https://pluggy.readthedocs.io/en/stable", None), "pluggy": ("https://pluggy.readthedocs.io/en/stable", None),
"python": ("https://docs.python.org/3", None), "python": ("https://docs.python.org/3", None),
"numpy": ("https://numpy.org/doc/stable", None), "numpy": ("https://numpy.org/doc/stable", None),
"pip": ("https://pip.pypa.io/en/stable", None),
"tox": ("https://tox.wiki/en/stable", None),
"virtualenv": ("https://virtualenv.pypa.io/en/stable", None),
"django": (
"http://docs.djangoproject.com/en/stable",
"http://docs.djangoproject.com/en/stable/_objects",
),
"setuptools": ("https://setuptools.pypa.io/en/stable", None),
} }

View File

@ -18,17 +18,40 @@ Deprecated Features
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`. :class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
.. _node-ctor-fspath-deprecation:
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
is now deprecated.
Plugins which construct nodes should pass the ``path`` argument, of type
:class:`pathlib.Path`, instead of the ``fspath`` argument.
Plugins which implement custom items and collectors are encouraged to replace
``py.path.local`` ``fspath`` parameters with ``pathlib.Path`` parameters, and
drop any other usage of the ``py`` library if possible.
.. _legacy-path-hooks-deprecated:
``py.path.local`` arguments for hooks replaced with ``pathlib.Path`` ``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order to support the transition to :mod:`pathlib`, the following hooks now receive additional arguments: .. deprecated:: 7.0
* :func:`pytest_ignore_collect(fspath: pathlib.Path) <_pytest.hookspec.pytest_ignore_collect>` In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
* :func:`pytest_collect_file(fspath: pathlib.Path) <_pytest.hookspec.pytest_collect_file>`
* :func:`pytest_pycollect_makemodule(fspath: pathlib.Path) <_pytest.hookspec.pytest_pycollect_makemodule>` * :func:`pytest_ignore_collect(fspath: pathlib.Path) <_pytest.hookspec.pytest_ignore_collect>` instead of ``path``
* :func:`pytest_report_header(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_header>` * :func:`pytest_collect_file(fspath: pathlib.Path) <_pytest.hookspec.pytest_collect_file>` instead of ``path``
* :func:`pytest_report_collectionfinish(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_collectionfinish>` * :func:`pytest_pycollect_makemodule(fspath: pathlib.Path) <_pytest.hookspec.pytest_pycollect_makemodule>` instead of ``path``
* :func:`pytest_report_header(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_header>` instead of ``startdir``
* :func:`pytest_report_collectionfinish(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_collectionfinish>` instead of ``startdir``
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments. The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
@ -59,7 +82,7 @@ Implement the :func:`pytest_load_initial_conftests <_pytest.hookspec.pytest_load
Diamond inheritance between :class:`pytest.File` and :class:`pytest.Item` Diamond inheritance between :class:`pytest.File` and :class:`pytest.Item`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.3 .. deprecated:: 7.0
Inheriting from both Item and file at once has never been supported officially, Inheriting from both Item and file at once has never been supported officially,
however some plugins providing linting/code analysis have been using this as a hack. however some plugins providing linting/code analysis have been using this as a hack.
@ -86,7 +109,7 @@ scheduled for removal in pytest 7 (deprecated since pytest 2.4.0):
Raising ``unittest.SkipTest`` during collection Raising ``unittest.SkipTest`` during collection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.3 .. deprecated:: 7.0
Raising :class:`unittest.SkipTest` to skip collection of tests during the Raising :class:`unittest.SkipTest` to skip collection of tests during the
pytest collection phase is deprecated. Use :func:`pytest.skip` instead. pytest collection phase is deprecated. Use :func:`pytest.skip` instead.

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="572" height="542"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="572" height="542">
<style> <style>
text { text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
@ -37,7 +37,7 @@
<path d="M 26,271 A 260 260 0 0 1 546 271" id="testp"/> <path d="M 26,271 A 260 260 0 0 1 546 271" id="testp"/>
</defs> </defs>
<text class="package"> <text class="package">
<textPath href="#testp" startOffset="50%">tests</textPath> <textPath xlink:href="#testp" startOffset="50%">tests</textPath>
</text> </text>
<!-- subpackage --> <!-- subpackage -->
@ -47,7 +47,7 @@
<path d="M 56,271 A 130 130 0 0 1 316 271" id="subpackage"/> <path d="M 56,271 A 130 130 0 0 1 316 271" id="subpackage"/>
</defs> </defs>
<text class="package"> <text class="package">
<textPath href="#subpackage" startOffset="50%">subpackage</textPath> <textPath xlink:href="#subpackage" startOffset="50%">subpackage</textPath>
</text> </text>
<!-- test_subpackage.py --> <!-- test_subpackage.py -->
@ -57,7 +57,7 @@
<path d="M 106,311 A 80 80 0 0 1 266 311" id="testSubpackage"/> <path d="M 106,311 A 80 80 0 0 1 266 311" id="testSubpackage"/>
</defs> </defs>
<text class="module"> <text class="module">
<textPath href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath> <textPath xlink:href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath>
</text> </text>
<!-- innermost --> <!-- innermost -->
<line x1="186" x2="186" y1="271" y2="351"/> <line x1="186" x2="186" y1="271" y2="351"/>
@ -102,7 +102,7 @@
<path d="M 366,271 A 75 75 0 0 1 516 271" id="testTop"/> <path d="M 366,271 A 75 75 0 0 1 516 271" id="testTop"/>
</defs> </defs>
<text class="module"> <text class="module">
<textPath href="#testTop" startOffset="50%">test_top.py</textPath> <textPath xlink:href="#testTop" startOffset="50%">test_top.py</textPath>
</text> </text>
<!-- innermost --> <!-- innermost -->
<line x1="441" x2="441" y1="306" y2="236"/> <line x1="441" x2="441" y1="306" y2="236"/>

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="587" height="382"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="587" height="382">
<style> <style>
text { text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
@ -38,7 +38,7 @@
<path d="M 411,86 A 75 75 0 0 1 561 86" id="pluginA"/> <path d="M 411,86 A 75 75 0 0 1 561 86" id="pluginA"/>
</defs> </defs>
<text class="plugin"> <text class="plugin">
<textPath href="#pluginA" startOffset="50%">plugin_a</textPath> <textPath xlink:href="#pluginA" startOffset="50%">plugin_a</textPath>
</text> </text>
<!-- scope order number --> <!-- scope order number -->
<mask id="pluginAOrderMask"> <mask id="pluginAOrderMask">
@ -55,7 +55,7 @@
<path d="M 411,296 A 75 75 0 0 1 561 296" id="pluginB"/> <path d="M 411,296 A 75 75 0 0 1 561 296" id="pluginB"/>
</defs> </defs>
<text class="plugin"> <text class="plugin">
<textPath href="#pluginB" startOffset="50%">plugin_b</textPath> <textPath xlink:href="#pluginB" startOffset="50%">plugin_b</textPath>
</text> </text>
<!-- scope order number --> <!-- scope order number -->
<mask id="pluginBOrderMask"> <mask id="pluginBOrderMask">
@ -72,7 +72,7 @@
<path d="M 11,191 A 180 180 0 0 1 371 191" id="testp"/> <path d="M 11,191 A 180 180 0 0 1 371 191" id="testp"/>
</defs> </defs>
<text class="package"> <text class="package">
<textPath href="#testp" startOffset="50%">tests</textPath> <textPath xlink:href="#testp" startOffset="50%">tests</textPath>
</text> </text>
<!-- scope order number --> <!-- scope order number -->
<mask id="mainOrderMask"> <mask id="mainOrderMask">
@ -89,7 +89,7 @@
<path d="M 61,231 A 130 130 0 0 1 321 231" id="subpackage"/> <path d="M 61,231 A 130 130 0 0 1 321 231" id="subpackage"/>
</defs> </defs>
<text class="package"> <text class="package">
<textPath href="#subpackage" startOffset="50%">subpackage</textPath> <textPath xlink:href="#subpackage" startOffset="50%">subpackage</textPath>
</text> </text>
<!-- scope order number --> <!-- scope order number -->
<mask id="subpackageOrderMask"> <mask id="subpackageOrderMask">
@ -106,7 +106,7 @@
<path d="M 111,271 A 80 80 0 0 1 271 271" id="testSubpackage"/> <path d="M 111,271 A 80 80 0 0 1 271 271" id="testSubpackage"/>
</defs> </defs>
<text class="module"> <text class="module">
<textPath href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath> <textPath xlink:href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath>
</text> </text>
<!-- scope order number --> <!-- scope order number -->
<mask id="testSubpackageOrderMask"> <mask id="testSubpackageOrderMask">

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="862" height="402"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="862" height="402">
<style> <style>
text { text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
@ -48,7 +48,7 @@
<path d="M31,201 A 190 190 0 0 1 411 201" id="testClassWith"/> <path d="M31,201 A 190 190 0 0 1 411 201" id="testClassWith"/>
</defs> </defs>
<text class="class"> <text class="class">
<textPath href="#testClassWith" startOffset="50%">TestWithC1Request</textPath> <textPath xlink:href="#testClassWith" startOffset="50%">TestWithC1Request</textPath>
</text> </text>
<!-- TestWithoutC1Request --> <!-- TestWithoutC1Request -->
@ -67,7 +67,7 @@
<path d="M451,201 A 190 190 0 0 1 831 201" id="testClassWithout"/> <path d="M451,201 A 190 190 0 0 1 831 201" id="testClassWithout"/>
</defs> </defs>
<text class="class"> <text class="class">
<textPath href="#testClassWithout" startOffset="50%">TestWithoutC1Request</textPath> <textPath xlink:href="#testClassWithout" startOffset="50%">TestWithoutC1Request</textPath>
</text> </text>
<rect class="autouse" width="862" height="40" x="1" y="181" /> <rect class="autouse" width="862" height="40" x="1" y="181" />

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="862" height="502"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="862" height="502">
<style> <style>
text { text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
@ -39,7 +39,7 @@
<path d="M11,251 A 240 240 0 0 1 491 251" id="testClassWith"/> <path d="M11,251 A 240 240 0 0 1 491 251" id="testClassWith"/>
</defs> </defs>
<text class="class"> <text class="class">
<textPath href="#testClassWith" startOffset="50%">TestWithAutouse</textPath> <textPath xlink:href="#testClassWith" startOffset="50%">TestWithAutouse</textPath>
</text> </text>
<mask id="autouseScope"> <mask id="autouseScope">
<circle fill="white" r="249" cx="251" cy="251" /> <circle fill="white" r="249" cx="251" cy="251" />
@ -79,7 +79,7 @@
<path d="M 531,251 A 160 160 0 0 1 851 251" id="testClassWithout"/> <path d="M 531,251 A 160 160 0 0 1 851 251" id="testClassWithout"/>
</defs> </defs>
<text class="class"> <text class="class">
<textPath href="#testClassWithout" startOffset="50%">TestWithoutAutouse</textPath> <textPath xlink:href="#testClassWithout" startOffset="50%">TestWithoutAutouse</textPath>
</text> </text>
<!-- TestWithoutAutouse.test_req --> <!-- TestWithoutAutouse.test_req -->

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="262" height="537"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="262" height="537">
<style> <style>
text { text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
@ -50,6 +50,6 @@
<path d="M131,526 A 120 120 0 0 1 136 286" id="testClass"/> <path d="M131,526 A 120 120 0 0 1 136 286" id="testClass"/>
</defs> </defs>
<text class="class"> <text class="class">
<textPath href="#testClass" startOffset="50%">TestClass</textPath> <textPath xlink:href="#testClass" startOffset="50%">TestClass</textPath>
</text> </text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="562" height="532"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="562" height="532">
<style> <style>
text { text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
@ -41,7 +41,7 @@
<path d="M 26,266 A 255 255 0 0 1 536 266" id="testModule"/> <path d="M 26,266 A 255 255 0 0 1 536 266" id="testModule"/>
</defs> </defs>
<text class="module"> <text class="module">
<textPath href="#testModule" startOffset="50%">test_fixtures_request_different_scope.py</textPath> <textPath xlink:href="#testModule" startOffset="50%">test_fixtures_request_different_scope.py</textPath>
</text> </text>
<!-- TestOne --> <!-- TestOne -->
@ -61,7 +61,7 @@
<path d="M 51,266 A 90 90 0 0 1 231 266" id="testOne"/> <path d="M 51,266 A 90 90 0 0 1 231 266" id="testOne"/>
</defs> </defs>
<text class="class"> <text class="class">
<textPath href="#testOne" startOffset="50%">TestOne</textPath> <textPath xlink:href="#testOne" startOffset="50%">TestOne</textPath>
</text> </text>
<!-- scope order number --> <!-- scope order number -->
<mask id="testOneOrderMask"> <mask id="testOneOrderMask">
@ -95,7 +95,7 @@
<path d="M 331,266 A 90 90 0 0 1 511 266" id="testTwo"/> <path d="M 331,266 A 90 90 0 0 1 511 266" id="testTwo"/>
</defs> </defs>
<text class="class"> <text class="class">
<textPath href="#testTwo" startOffset="50%">TestTwo</textPath> <textPath xlink:href="#testTwo" startOffset="50%">TestTwo</textPath>
</text> </text>
<!-- scope order number --> <!-- scope order number -->
<mask id="testTwoOrderMask"> <mask id="testTwoOrderMask">

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -10,7 +10,6 @@ A basic example for specifying tests in Yaml files
-------------------------------------------------------------- --------------------------------------------------------------
.. _`pytest-yamlwsgi`: http://bitbucket.org/aafshar/pytest-yamlwsgi/src/tip/pytest_yamlwsgi.py .. _`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 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: 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:
@ -22,7 +21,7 @@ You can create a simple example file:
.. include:: nonpython/test_simple.yaml .. include:: nonpython/test_simple.yaml
:literal: :literal:
and if you installed `PyYAML`_ or a compatible YAML-parser you can and if you installed :pypi:`PyYAML` or a compatible YAML-parser you can
now execute the test specification: now execute the test specification:
.. code-block:: pytest .. code-block:: pytest

View File

@ -40,7 +40,7 @@ class YamlItem(pytest.Item):
) )
def reportinfo(self): def reportinfo(self):
return self.fspath, 0, f"usecase: {self.name}" return self.path, 0, f"usecase: {self.name}"
class YamlException(Exception): class YamlException(Exception):

View File

@ -183,9 +183,7 @@ together with the actual data, instead of listing them separately.
A quick port of "testscenarios" A quick port of "testscenarios"
------------------------------------ ------------------------------------
.. _`test scenarios`: https://pypi.org/project/testscenarios/ Here is a quick port to run tests configured with :pypi:`testscenarios`,
Here is a quick port to run tests configured with `test scenarios`_,
an add-on from Robert Collins for the standard unittest framework. We an add-on from Robert Collins for the standard unittest framework. We
only have to work a bit to construct the correct arguments for pytest's only have to work a bit to construct the correct arguments for pytest's
:py:func:`Metafunc.parametrize`: :py:func:`Metafunc.parametrize`:

View File

@ -223,7 +223,7 @@ the command line arguments before they get processed:
num = max(multiprocessing.cpu_count() / 2, 1) num = max(multiprocessing.cpu_count() / 2, 1)
args[:] = ["-n", str(num)] + args args[:] = ["-n", str(num)] + args
If you have the `xdist plugin <https://pypi.org/project/pytest-xdist/>`_ installed If you have the :pypi:`xdist plugin <pytest-xdist>` installed
you will now always perform test runs using a number you will now always perform test runs using a number
of subprocesses close to your CPU. Running in an empty of subprocesses close to your CPU. Running in an empty
directory with the above conftest.py: directory with the above conftest.py:
@ -1014,8 +1014,7 @@ which test got stuck, for example if pytest was run in quiet mode (``-q``) or yo
output. This is particularly a problem if the problem happens only sporadically, the famous "flaky" kind of tests. output. This is particularly a problem if the problem happens only sporadically, the famous "flaky" kind of tests.
``pytest`` sets the :envvar:`PYTEST_CURRENT_TEST` environment variable when running tests, which can be inspected ``pytest`` sets the :envvar:`PYTEST_CURRENT_TEST` environment variable when running tests, which can be inspected
by process monitoring utilities or libraries like `psutil <https://pypi.org/project/psutil/>`_ to discover which by process monitoring utilities or libraries like :pypi:`psutil` to discover which test got stuck if necessary:
test got stuck if necessary:
.. code-block:: python .. code-block:: python

View File

@ -154,8 +154,7 @@ This makes use of the automatic caching mechanisms of pytest.
Another good approach is by adding the data files in the ``tests`` folder. Another good approach is by adding the data files in the ``tests`` folder.
There are also community plugins available to help to manage this aspect of There are also community plugins available to help to manage this aspect of
testing, e.g. `pytest-datadir <https://pypi.org/project/pytest-datadir/>`__ testing, e.g. :pypi:`pytest-datadir` and :pypi:`pytest-datafiles`.
and `pytest-datafiles <https://pypi.org/project/pytest-datafiles/>`__.
.. _fixtures-signal-cleanup: .. _fixtures-signal-cleanup:

View File

@ -8,27 +8,47 @@ Install package with pip
------------------------------------------------- -------------------------------------------------
For development, we recommend you use :mod:`venv` for virtual environments and For development, we recommend you use :mod:`venv` for virtual environments and
pip_ for installing your application and any dependencies, :doc:`pip:index` for installing your application and any dependencies,
as well as the ``pytest`` package itself. as well as the ``pytest`` package itself.
This ensures your code and dependencies are isolated from your system Python installation. This ensures your code and dependencies are isolated from your system Python installation.
Next, place a ``setup.py`` file in the root of your package with the following minimum content: Next, place a ``pyproject.toml`` file in the root of your package:
.. code-block:: python .. code-block:: toml
from setuptools import setup, find_packages [build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
setup(name="PACKAGENAME", packages=find_packages()) and a ``setup.cfg`` file containing your package's metadata with the following minimum content:
Where ``PACKAGENAME`` is the name of your package. You can then install your package in "editable" mode by running from the same directory: .. code-block:: ini
[metadata]
name = PACKAGENAME
[options]
packages = find:
where ``PACKAGENAME`` is the name of your package.
.. note::
If your pip version is older than ``21.3``, you'll also need a ``setup.py`` file:
.. code-block:: python
from setuptools import setup
setup()
You can then install your package in "editable" mode by running from the same directory:
.. code-block:: bash .. code-block:: bash
pip install -e . pip install -e .
which lets you change your source code (both tests and application) and rerun tests at will. which lets you change your source code (both tests and application) and rerun tests at will.
This is similar to running ``python setup.py develop`` or ``conda develop`` in that it installs
your package using a symlink to your development code.
.. _`test discovery`: .. _`test discovery`:
.. _`Python test discovery`: .. _`Python test discovery`:
@ -68,7 +88,8 @@ to keep tests separate from actual application code (often a good idea):
.. code-block:: text .. code-block:: text
setup.py pyproject.toml
setup.cfg
mypkg/ mypkg/
__init__.py __init__.py
app.py app.py
@ -82,7 +103,7 @@ This has the following benefits:
* Your tests can run against an installed version after executing ``pip install .``. * Your tests can run against an installed version after executing ``pip install .``.
* Your tests can run against the local copy with an editable install after executing ``pip install --editable .``. * Your tests can run against the local copy with an editable install after executing ``pip install --editable .``.
* If you don't have a ``setup.py`` file and are relying on the fact that Python by default puts the current * If you don't use an editable install and are relying on the fact that Python by default puts the current
directory in ``sys.path`` to import your package, you can execute ``python -m pytest`` to execute the tests against the directory in ``sys.path`` to import your package, you can execute ``python -m pytest`` to execute the tests against the
local copy directly, without using ``pip``. local copy directly, without using ``pip``.
@ -103,7 +124,8 @@ If you need to have test modules with the same name, you might add ``__init__.py
.. code-block:: text .. code-block:: text
setup.py pyproject.toml
setup.cfg
mypkg/ mypkg/
... ...
tests/ tests/
@ -130,7 +152,8 @@ sub-directory of your root:
.. code-block:: text .. code-block:: text
setup.py pyproject.toml
setup.cfg
src/ src/
mypkg/ mypkg/
__init__.py __init__.py
@ -167,7 +190,8 @@ want to distribute them along with your application:
.. code-block:: text .. code-block:: text
setup.py pyproject.toml
setup.cfg
mypkg/ mypkg/
__init__.py __init__.py
app.py app.py
@ -191,11 +215,11 @@ Note that this layout also works in conjunction with the ``src`` layout mentione
.. note:: .. note::
You can use Python3 namespace packages (PEP420) for your application You can use namespace packages (PEP420) for your application
but pytest will still perform `test package name`_ discovery based on the but pytest will still perform `test package name`_ discovery based on the
presence of ``__init__.py`` files. If you use one of the presence of ``__init__.py`` files. If you use one of the
two recommended file system layouts above but leave away the ``__init__.py`` two recommended file system layouts above but leave away the ``__init__.py``
files from your directories it should just work on Python3.3 and above. From files from your directories, it should just work. From
"inlined tests", however, you will need to use absolute imports for "inlined tests", however, you will need to use absolute imports for
getting at your application code. getting at your application code.
@ -230,21 +254,35 @@ Note that this layout also works in conjunction with the ``src`` layout mentione
much less surprising. much less surprising.
.. _`virtualenv`: https://pypi.org/project/virtualenv/
.. _`buildout`: http://www.buildout.org/en/latest/ .. _`buildout`: http://www.buildout.org/en/latest/
.. _pip: https://pypi.org/project/pip/
.. _`use tox`: .. _`use tox`:
tox tox
------ ---
Once you are done with your work and want to make sure that your actual Once you are done with your work and want to make sure that your actual
package passes all tests you may want to look into `tox <https://tox.readthedocs.io/>`_, the package passes all tests you may want to look into :doc:`tox <tox:index>`, the
virtualenv test automation tool and its `pytest support virtualenv test automation tool and its :doc:`pytest support <tox:example/pytest>`.
<https://tox.readthedocs.io/en/latest/example/pytest.html>`_.
tox helps you to setup virtualenv environments with pre-defined tox helps you to setup virtualenv environments with pre-defined
dependencies and then executing a pre-configured test command with dependencies and then executing a pre-configured test command with
options. It will run tests against the installed package and not options. It will run tests against the installed package and not
against your source code checkout, helping to detect packaging against your source code checkout, helping to detect packaging
glitches. glitches.
Do not run via setuptools
-------------------------
Integration with setuptools is **not recommended**,
i.e. you should not be using ``python setup.py test`` or ``pytest-runner``,
and may stop working in the future.
This is deprecated since it depends on deprecated features of setuptools
and relies on features that break security mechanisms in pip.
For example 'setup_requires' and 'tests_require' bypass ``pip --require-hashes``.
For more information and migration instructions,
see the `pytest-runner notice <https://github.com/pytest-dev/pytest-runner#deprecation-notice>`_.
See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>`_.
setuptools intends to
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_.

View File

@ -4,8 +4,8 @@ History
pytest has a long and interesting history. The `first commit pytest has a long and interesting history. The `first commit
<https://github.com/pytest-dev/pytest/commit/5992a8ef21424d7571305a8d7e2a3431ee7e1e23>`__ <https://github.com/pytest-dev/pytest/commit/5992a8ef21424d7571305a8d7e2a3431ee7e1e23>`__
in this repository is from January 2007, and even that commit alone already in this repository is from January 2007, and even that commit alone already
tells a lot: The repository originally was from the `py tells a lot: The repository originally was from the :pypi:`py`
<https://pypi.org/project/py/>`__ library (later split off to pytest), and it library (later split off to pytest), and it
originally was a SVN revision, migrated to Mercurial, and finally migrated to originally was a SVN revision, migrated to Mercurial, and finally migrated to
git. git.
@ -99,9 +99,8 @@ project:
- It seemed to get rather quiet for a while, and little seemed to happen - It seemed to get rather quiet for a while, and little seemed to happen
between October 2004 (removing ``py`` from PyPy) and January between October 2004 (removing ``py`` from PyPy) and January
2007 (first commit in the now-pytest repository). However, there were 2007 (first commit in the now-pytest repository). However, there were
various discussions about features/ideas on the mailinglist, and `a various discussions about features/ideas on the mailinglist, and
couple of :pypi:`a couple of releases <py/0.8.0-alpha2/#history>` every
releases <https://pypi.org/project/py/0.8.0-alpha2/#history>`__ every
couple of months: couple of months:
- March 2006: py 0.8.0-alpha2 - March 2006: py 0.8.0-alpha2

View File

@ -268,7 +268,7 @@ argument ``match`` to assert that the exception matches a text or regex::
... warnings.warn("this is not here", UserWarning) ... warnings.warn("this is not here", UserWarning)
Traceback (most recent call last): Traceback (most recent call last):
... ...
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
You can also call :func:`pytest.warns` on a function or code string: You can also call :func:`pytest.warns` on a function or code string:

View File

@ -248,8 +248,8 @@ through ``add_color_level()``. Example:
Release notes Release notes
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
This feature was introduced as a drop-in replacement for the `pytest-catchlog This feature was introduced as a drop-in replacement for the
<https://pypi.org/project/pytest-catchlog/>`_ plugin and they conflict :pypi:`pytest-catchlog` plugin and they conflict
with each other. The backward compatibility API with ``pytest-capturelog`` with each other. The backward compatibility API with ``pytest-capturelog``
has been dropped when this feature was introduced, so if for that reason you has been dropped when this feature was introduced, so if for that reason you
still need ``pytest-catchlog`` you can disable the internal feature by still need ``pytest-catchlog`` you can disable the internal feature by

View File

@ -20,40 +20,38 @@ there is no need to activate it.
Here is a little annotated list for some popular plugins: Here is a little annotated list for some popular plugins:
.. _`django`: https://www.djangoproject.com/ * :pypi:`pytest-django`: write tests
for :std:doc:`django <django:index>` apps, using pytest integration.
* `pytest-django <https://pypi.org/project/pytest-django/>`_: write tests * :pypi:`pytest-twisted`: write tests
for `django`_ apps, using pytest integration.
* `pytest-twisted <https://pypi.org/project/pytest-twisted/>`_: write tests
for `twisted <https://twistedmatrix.com/>`_ apps, starting a reactor and for `twisted <https://twistedmatrix.com/>`_ apps, starting a reactor and
processing deferreds from test functions. processing deferreds from test functions.
* `pytest-cov <https://pypi.org/project/pytest-cov/>`__: * :pypi:`pytest-cov`:
coverage reporting, compatible with distributed testing coverage reporting, compatible with distributed testing
* `pytest-xdist <https://pypi.org/project/pytest-xdist/>`_: * :pypi:`pytest-xdist`:
to distribute tests to CPUs and remote hosts, to run in boxed to distribute tests to CPUs and remote hosts, to run in boxed
mode which allows to survive segmentation faults, to run in mode which allows to survive segmentation faults, to run in
looponfailing mode, automatically re-running failing tests looponfailing mode, automatically re-running failing tests
on file changes. on file changes.
* `pytest-instafail <https://pypi.org/project/pytest-instafail/>`_: * :pypi:`pytest-instafail`:
to report failures while the test run is happening. to report failures while the test run is happening.
* `pytest-bdd <https://pypi.org/project/pytest-bdd/>`_: * :pypi:`pytest-bdd`:
to write tests using behaviour-driven testing. to write tests using behaviour-driven testing.
* `pytest-timeout <https://pypi.org/project/pytest-timeout/>`_: * :pypi:`pytest-timeout`:
to timeout tests based on function marks or global definitions. to timeout tests based on function marks or global definitions.
* `pytest-pep8 <https://pypi.org/project/pytest-pep8/>`_: * :pypi:`pytest-pep8`:
a ``--pep8`` option to enable PEP8 compliance checking. a ``--pep8`` option to enable PEP8 compliance checking.
* `pytest-flakes <https://pypi.org/project/pytest-flakes/>`_: * :pypi:`pytest-flakes`:
check source code with pyflakes. check source code with pyflakes.
* `oejskit <https://pypi.org/project/oejskit/>`_: * :pypi:`oejskit`:
a plugin to run javascript unittests in live browsers. a plugin to run javascript unittests in live browsers.
To see a complete list of all plugins with their latest testing To see a complete list of all plugins with their latest testing

View File

@ -46,9 +46,9 @@ in most cases without having to modify existing code:
* :ref:`maxfail`; * :ref:`maxfail`;
* :ref:`--pdb <pdb-option>` command-line option for debugging on test failures * :ref:`--pdb <pdb-option>` command-line option for debugging on test failures
(see :ref:`note <pdb-unittest-note>` below); (see :ref:`note <pdb-unittest-note>` below);
* Distribute tests to multiple CPUs using the `pytest-xdist <https://pypi.org/project/pytest-xdist/>`_ plugin; * Distribute tests to multiple CPUs using the :pypi:`pytest-xdist` plugin;
* Use :ref:`plain assert-statements <assert>` instead of ``self.assert*`` functions (`unittest2pytest * Use :ref:`plain assert-statements <assert>` instead of ``self.assert*`` functions
<https://pypi.org/project/unittest2pytest/>`__ is immensely helpful in this); (:pypi:`unittest2pytest` is immensely helpful in this);
pytest features in ``unittest.TestCase`` subclasses pytest features in ``unittest.TestCase`` subclasses

View File

@ -120,7 +120,7 @@ The option receives a ``name`` parameter, which can be:
* A full module dotted name, for example ``myproject.plugins``. This dotted name must be importable. * A full module dotted name, for example ``myproject.plugins``. This dotted name must be importable.
* The entry-point name of a plugin. This is the name passed to ``setuptools`` when the plugin is * The entry-point name of a plugin. This is the name passed to ``setuptools`` when the plugin is
registered. For example to early-load the `pytest-cov <https://pypi.org/project/pytest-cov/>`__ plugin you can use:: registered. For example to early-load the :pypi:`pytest-cov` plugin you can use::
pytest -p pytest_cov pytest -p pytest_cov

View File

@ -115,8 +115,6 @@ Here is how you might run it::
Writing your own plugin Writing your own plugin
----------------------- -----------------------
.. _`setuptools`: https://pypi.org/project/setuptools/
If you want to write a plugin, there are many real-life examples If you want to write a plugin, there are many real-life examples
you can copy from: you can copy from:
@ -150,7 +148,7 @@ Making your plugin installable by others
If you want to make your plugin externally available, you If you want to make your plugin externally available, you
may define a so-called entry point for your distribution so may define a so-called entry point for your distribution so
that ``pytest`` finds your plugin module. Entry points are that ``pytest`` finds your plugin module. Entry points are
a feature that is provided by `setuptools`_. pytest looks up a feature that is provided by :std:doc:`setuptools:index`. pytest looks up
the ``pytest11`` entrypoint to discover its the ``pytest11`` entrypoint to discover its
plugins and you can thus make your plugin available by defining plugins and you can thus make your plugin available by defining
it in your setuptools-invocation: it in your setuptools-invocation:

View File

@ -19,7 +19,7 @@ scale to support complex functional testing for applications and libraries.
**Pythons**: ``pytest`` requires: Python 3.6, 3.7, 3.8, 3.9, or PyPy3. **Pythons**: ``pytest`` requires: Python 3.6, 3.7, 3.8, 3.9, or PyPy3.
**PyPI package name**: `pytest <https://pypi.org/project/pytest/>`_ **PyPI package name**: :pypi:`pytest`
**Documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_ **Documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_

View File

@ -120,7 +120,7 @@ all parameters marked as a fixture.
.. note:: .. note::
The `pytest-lazy-fixture <https://pypi.org/project/pytest-lazy-fixture/>`_ plugin implements a very The :pypi:`pytest-lazy-fixture` plugin implements a very
similar solution to the proposal below, make sure to check it out. similar solution to the proposal below, make sure to check it out.
.. code-block:: python .. code-block:: python

View File

@ -116,7 +116,7 @@ fixture (``inner``) from a scope it wasn't defined in:
From the tests' perspectives, they have no problem seeing each of the fixtures From the tests' perspectives, they have no problem seeing each of the fixtures
they're dependent on: they're dependent on:
.. image:: /example/fixtures/test_fixtures_request_different_scope.svg .. image:: /example/fixtures/test_fixtures_request_different_scope.*
:align: center :align: center
So when they run, ``outer`` will have no problem finding ``inner``, because So when they run, ``outer`` will have no problem finding ``inner``, because
@ -193,7 +193,7 @@ For example, given a test file structure like this:
The boundaries of the scopes can be visualized like this: The boundaries of the scopes can be visualized like this:
.. image:: /example/fixtures/fixture_availability.svg .. image:: /example/fixtures/fixture_availability.*
:align: center :align: center
The directories become their own sort of scope where fixtures that are defined The directories become their own sort of scope where fixtures that are defined
@ -319,7 +319,7 @@ The test will pass because the larger scoped fixtures are executing first.
The order breaks down to this: The order breaks down to this:
.. image:: /example/fixtures/test_fixtures_order_scope.svg .. image:: /example/fixtures/test_fixtures_order_scope.*
:align: center :align: center
Fixtures of the same order execute based on dependencies Fixtures of the same order execute based on dependencies
@ -337,13 +337,13 @@ For example:
If we map out what depends on what, we get something that look like this: If we map out what depends on what, we get something that look like this:
.. image:: /example/fixtures/test_fixtures_order_dependencies.svg .. image:: /example/fixtures/test_fixtures_order_dependencies.*
:align: center :align: center
The rules provided by each fixture (as to what fixture(s) each one has to come The rules provided by each fixture (as to what fixture(s) each one has to come
after) are comprehensive enough that it can be flattened to this: after) are comprehensive enough that it can be flattened to this:
.. image:: /example/fixtures/test_fixtures_order_dependencies_flat.svg .. image:: /example/fixtures/test_fixtures_order_dependencies_flat.*
:align: center :align: center
Enough information has to be provided through these requests in order for pytest Enough information has to be provided through these requests in order for pytest
@ -354,7 +354,7 @@ could go with any one of those interpretations at any point.
For example, if ``d`` didn't request ``c``, i.e.the graph would look like this: For example, if ``d`` didn't request ``c``, i.e.the graph would look like this:
.. image:: /example/fixtures/test_fixtures_order_dependencies_unclear.svg .. image:: /example/fixtures/test_fixtures_order_dependencies_unclear.*
:align: center :align: center
Because nothing requested ``c`` other than ``g``, and ``g`` also requests ``f``, Because nothing requested ``c`` other than ``g``, and ``g`` also requests ``f``,
@ -395,7 +395,7 @@ So if the test file looked like this:
the graph would look like this: the graph would look like this:
.. image:: /example/fixtures/test_fixtures_order_autouse.svg .. image:: /example/fixtures/test_fixtures_order_autouse.*
:align: center :align: center
Because ``c`` can now be put above ``d`` in the graph, pytest can once again Because ``c`` can now be put above ``d`` in the graph, pytest can once again
@ -413,7 +413,7 @@ example, consider this file:
Even though nothing in ``TestClassWithoutC1Request`` is requesting ``c1``, it still Even though nothing in ``TestClassWithoutC1Request`` is requesting ``c1``, it still
is executed for the tests inside it anyway: is executed for the tests inside it anyway:
.. image:: /example/fixtures/test_fixtures_order_autouse_multiple_scopes.svg .. image:: /example/fixtures/test_fixtures_order_autouse_multiple_scopes.*
:align: center :align: center
But just because one autouse fixture requested a non-autouse fixture, that But just because one autouse fixture requested a non-autouse fixture, that
@ -428,7 +428,7 @@ For example, take a look at this test file:
It would break down to something like this: It would break down to something like this:
.. image:: /example/fixtures/test_fixtures_order_autouse_temp_effects.svg .. image:: /example/fixtures/test_fixtures_order_autouse_temp_effects.*
:align: center :align: center
For ``test_req`` and ``test_no_req`` inside ``TestClassWithAutouse``, ``c3`` For ``test_req`` and ``test_no_req`` inside ``TestClassWithAutouse``, ``c3``

File diff suppressed because it is too large Load Diff

View File

@ -558,14 +558,17 @@ To use it, include in your topmost ``conftest.py`` file:
.. autoclass:: pytest.Pytester() .. autoclass:: pytest.Pytester()
:members: :members:
.. autoclass:: _pytest.pytester.RunResult() .. autoclass:: pytest.RunResult()
:members: :members:
.. autoclass:: _pytest.pytester.LineMatcher() .. autoclass:: pytest.LineMatcher()
:members: :members:
:special-members: __str__ :special-members: __str__
.. autoclass:: _pytest.pytester.HookRecorder() .. autoclass:: pytest.HookRecorder()
:members:
.. autoclass:: pytest.RecordedHookCall()
:members: :members:
.. fixture:: testdir .. fixture:: testdir
@ -1196,19 +1199,8 @@ passed multiple times. The expected format is ``name=value``. For example::
variables, that will be expanded. For more information about cache plugin variables, that will be expanded. For more information about cache plugin
please refer to :ref:`cache_provider`. please refer to :ref:`cache_provider`.
.. confval:: confcutdir
Sets a directory where search upwards for ``conftest.py`` files stops.
By default, pytest will stop searching for ``conftest.py`` files upwards
from ``pytest.ini``/``tox.ini``/``setup.cfg`` of the project if any,
or up to the file-system root.
.. confval:: console_output_style .. confval:: console_output_style
Sets the console output style while running tests: Sets the console output style while running tests:
* ``classic``: classic pytest output. * ``classic``: classic pytest output.

View File

@ -4,3 +4,4 @@ pygments-pytest>=2.2.0
sphinx-removed-in>=0.2.0 sphinx-removed-in>=0.2.0
sphinx>=3.1,<4 sphinx>=3.1,<4
sphinxcontrib-trio sphinxcontrib-trio
sphinxcontrib-svg2pdfconverter

View File

@ -54,8 +54,16 @@ def prepare_release_pr(
check_call(["git", "checkout", f"origin/{base_branch}"]) check_call(["git", "checkout", f"origin/{base_branch}"])
changelog = Path("changelog")
features = list(changelog.glob("*.feature.rst"))
breaking = list(changelog.glob("*.breaking.rst"))
is_feature_release = bool(features or breaking)
try: try:
version = find_next_version(base_branch, is_major, prerelease) version = find_next_version(
base_branch, is_major, is_feature_release, prerelease
)
except InvalidFeatureRelease as e: except InvalidFeatureRelease as e:
print(f"{Fore.RED}{e}") print(f"{Fore.RED}{e}")
raise SystemExit(1) raise SystemExit(1)
@ -80,9 +88,24 @@ def prepare_release_pr(
print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.") print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.")
if prerelease:
template_name = "release.pre.rst"
elif is_feature_release:
template_name = "release.minor.rst"
else:
template_name = "release.patch.rst"
# important to use tox here because we have changed branches, so dependencies # important to use tox here because we have changed branches, so dependencies
# might have changed as well # might have changed as well
cmdline = ["tox", "-e", "release", "--", version, "--skip-check-links"] cmdline = [
"tox",
"-e",
"release",
"--",
version,
template_name,
"--skip-check-links",
]
print("Running", " ".join(cmdline)) print("Running", " ".join(cmdline))
run( run(
cmdline, cmdline,
@ -107,7 +130,9 @@ def prepare_release_pr(
print(f"Pull request {Fore.CYAN}{pr.url}{Fore.RESET} created.") print(f"Pull request {Fore.CYAN}{pr.url}{Fore.RESET} created.")
def find_next_version(base_branch: str, is_major: bool, prerelease: str) -> str: def find_next_version(
base_branch: str, is_major: bool, is_feature_release: bool, prerelease: str
) -> str:
output = check_output(["git", "tag"], encoding="UTF-8") output = check_output(["git", "tag"], encoding="UTF-8")
valid_versions = [] valid_versions = []
for v in output.splitlines(): for v in output.splitlines():
@ -118,12 +143,6 @@ def find_next_version(base_branch: str, is_major: bool, prerelease: str) -> str:
valid_versions.sort() valid_versions.sort()
last_version = valid_versions[-1] last_version = valid_versions[-1]
changelog = Path("changelog")
features = list(changelog.glob("*.feature.rst"))
breaking = list(changelog.glob("*.breaking.rst"))
is_feature_release = features or breaking
if is_major: if is_major:
return f"{last_version[0]+1}.0.0{prerelease}" return f"{last_version[0]+1}.0.0{prerelease}"
elif is_feature_release: elif is_feature_release:

29
scripts/release.pre.rst Normal file
View File

@ -0,0 +1,29 @@
pytest-{version}
=======================================
The pytest team is proud to announce the {version} prerelease!
This is a prerelease, not intended for production use, but to test the upcoming features and improvements
in order to catch any major problems before the final version is released to the major public.
We appreciate your help testing this out before the final release, making sure to report any
regressions to our issue tracker:
https://github.com/pytest-dev/pytest/issues
When doing so, please include the string ``[prerelease]`` in the title.
You can upgrade from PyPI via:
pip install pytest=={version}
Users are encouraged to take a look at the CHANGELOG carefully:
https://docs.pytest.org/en/stable/changelog.html
Thanks to all the contributors to this release:
{contributors}
Happy testing,
The pytest Development Team

View File

@ -10,7 +10,7 @@ from colorama import Fore
from colorama import init from colorama import init
def announce(version): def announce(version, template_name):
"""Generates a new release announcement entry in the docs.""" """Generates a new release announcement entry in the docs."""
# Get our list of authors # Get our list of authors
stdout = check_output(["git", "describe", "--abbrev=0", "--tags"]) stdout = check_output(["git", "describe", "--abbrev=0", "--tags"])
@ -22,9 +22,6 @@ def announce(version):
contributors = {name for name in stdout.splitlines() if not name.endswith("[bot]")} contributors = {name for name in stdout.splitlines() if not name.endswith("[bot]")}
template_name = (
"release.minor.rst" if version.endswith(".0") else "release.patch.rst"
)
template_text = ( template_text = (
Path(__file__).parent.joinpath(template_name).read_text(encoding="UTF-8") Path(__file__).parent.joinpath(template_name).read_text(encoding="UTF-8")
) )
@ -81,9 +78,9 @@ def check_links():
check_call(["tox", "-e", "docs-checklinks"]) check_call(["tox", "-e", "docs-checklinks"])
def pre_release(version, *, skip_check_links): def pre_release(version, template_name, *, skip_check_links):
"""Generates new docs, release announcements and creates a local tag.""" """Generates new docs, release announcements and creates a local tag."""
announce(version) announce(version, template_name)
regen(version) regen(version)
changelog(version, write_out=True) changelog(version, write_out=True)
fix_formatting() fix_formatting()
@ -108,9 +105,16 @@ def main():
init(autoreset=True) init(autoreset=True)
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("version", help="Release version") parser.add_argument("version", help="Release version")
parser.add_argument(
"template_name", help="Name of template file to use for release announcement"
)
parser.add_argument("--skip-check-links", action="store_true", default=False) parser.add_argument("--skip-check-links", action="store_true", default=False)
options = parser.parse_args() options = parser.parse_args()
pre_release(options.version, skip_check_links=options.skip_check_links) pre_release(
options.version,
options.template_name,
skip_check_links=options.skip_check_links,
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,10 +1,14 @@
import datetime import datetime
import pathlib import pathlib
import re import re
from textwrap import dedent
from textwrap import indent
import packaging.version import packaging.version
import requests import requests
import tabulate import tabulate
import wcwidth
from tqdm import tqdm
FILE_HEAD = r""" FILE_HEAD = r"""
.. _plugin-list: .. _plugin-list:
@ -14,6 +18,11 @@ Plugin List
PyPI projects that match "pytest-\*" are considered plugins and are listed PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically. Packages classified as inactive are excluded. automatically. Packages classified as inactive are excluded.
.. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the
page.
""" """
DEVELOPMENT_STATUS_CLASSIFIERS = ( DEVELOPMENT_STATUS_CLASSIFIERS = (
"Development Status :: 1 - Planning", "Development Status :: 1 - Planning",
@ -42,10 +51,15 @@ def escape_rst(text: str) -> str:
def iter_plugins(): def iter_plugins():
regex = r">([\d\w-]*)</a>" regex = r">([\d\w-]*)</a>"
response = requests.get("https://pypi.org/simple") response = requests.get("https://pypi.org/simple")
for match in re.finditer(regex, response.text):
matches = list(
match
for match in re.finditer(regex, response.text)
if match.groups()[0].startswith("pytest-")
)
for match in tqdm(matches, smoothing=0):
name = match.groups()[0] name = match.groups()[0]
if not name.startswith("pytest-"):
continue
response = requests.get(f"https://pypi.org/pypi/{name}/json") response = requests.get(f"https://pypi.org/pypi/{name}/json")
if response.status_code == 404: if response.status_code == 404:
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but # Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but
@ -75,26 +89,51 @@ def iter_plugins():
) )
last_release = release_date.strftime("%b %d, %Y") last_release = release_date.strftime("%b %d, %Y")
break break
name = f'`{info["name"]} <{info["project_url"]}>`_' name = f':pypi:`{info["name"]}`'
summary = escape_rst(info["summary"].replace("\n", "")) summary = escape_rst(info["summary"].replace("\n", ""))
yield { yield {
"name": name, "name": name,
"summary": summary, "summary": summary.strip(),
"last release": last_release, "last release": last_release,
"status": status, "status": status,
"requires": requires, "requires": requires,
} }
def plugin_definitions(plugins):
"""Return RST for the plugin list that fits better on a vertical page."""
for plugin in plugins:
yield dedent(
f"""
{plugin['name']}
*last release*: {plugin["last release"]},
*status*: {plugin["status"]},
*requires*: {plugin["requires"]}
{plugin["summary"]}
"""
)
def main(): def main():
plugins = list(iter_plugins()) plugins = list(iter_plugins())
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
plugin_list = pathlib.Path("doc", "en", "reference", "plugin_list.rst") reference_dir = pathlib.Path("doc", "en", "reference")
plugin_list = reference_dir / "plugin_list.rst"
with plugin_list.open("w") as f: with plugin_list.open("w") as f:
f.write(FILE_HEAD) f.write(FILE_HEAD)
f.write(f"This list contains {len(plugins)} plugins.\n\n") f.write(f"This list contains {len(plugins)} plugins.\n\n")
f.write(plugin_table) f.write(".. only:: not latex\n\n")
f.write("\n")
wcwidth # reference library that must exist for tabulate to work
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
f.write(indent(plugin_table, " "))
f.write("\n\n")
f.write(".. only:: latex\n\n")
f.write(indent("".join(plugin_definitions(plugins)), " "))
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1240,7 +1240,6 @@ _PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
if _PLUGGY_DIR.name == "__init__.py": if _PLUGGY_DIR.name == "__init__.py":
_PLUGGY_DIR = _PLUGGY_DIR.parent _PLUGGY_DIR = _PLUGGY_DIR.parent
_PYTEST_DIR = Path(_pytest.__file__).parent _PYTEST_DIR = Path(_pytest.__file__).parent
_PY_DIR = Path(__import__("py").__file__).parent
def filter_traceback(entry: TracebackEntry) -> bool: def filter_traceback(entry: TracebackEntry) -> bool:
@ -1268,7 +1267,5 @@ def filter_traceback(entry: TracebackEntry) -> bool:
return False return False
if _PYTEST_DIR in parents: if _PYTEST_DIR in parents:
return False return False
if _PY_DIR in parents:
return False
return True return True

View File

@ -19,6 +19,7 @@ from typing import Callable
from typing import Dict from typing import Dict
from typing import IO from typing import IO
from typing import Iterable from typing import Iterable
from typing import Iterator
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
@ -63,7 +64,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
except ValueError: except ValueError:
self.fnpats = ["test_*.py", "*_test.py"] self.fnpats = ["test_*.py", "*_test.py"]
self.session: Optional[Session] = None self.session: Optional[Session] = None
self._rewritten_names: Set[str] = set() self._rewritten_names: Dict[str, Path] = {}
self._must_rewrite: Set[str] = set() self._must_rewrite: Set[str] = set()
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
# which might result in infinite recursion (#3506) # which might result in infinite recursion (#3506)
@ -133,7 +134,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
fn = Path(module.__spec__.origin) fn = Path(module.__spec__.origin)
state = self.config.stash[assertstate_key] state = self.config.stash[assertstate_key]
self._rewritten_names.add(module.__name__) self._rewritten_names[module.__name__] = fn
# The requested module looks like a test file, so rewrite it. This is # The requested module looks like a test file, so rewrite it. This is
# the most magical part of the process: load the source, rewrite the # the most magical part of the process: load the source, rewrite the
@ -275,6 +276,14 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
with open(pathname, "rb") as f: with open(pathname, "rb") as f:
return f.read() return f.read()
if sys.version_info >= (3, 9):
def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore
from types import SimpleNamespace
from importlib.readers import FileReader
return FileReader(SimpleNamespace(path=self._rewritten_names[name]))
def _write_pyc_fp( def _write_pyc_fp(
fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType
@ -333,7 +342,7 @@ else:
try: try:
_write_pyc_fp(fp, source_stat, co) _write_pyc_fp(fp, source_stat, co)
os.rename(proc_pyc, os.fspath(pyc)) os.rename(proc_pyc, pyc)
except OSError as e: except OSError as e:
state.trace(f"error writing pyc file at {pyc}: {e}") state.trace(f"error writing pyc file at {pyc}: {e}")
# we ignore any failure to write the cache file # we ignore any failure to write the cache file
@ -347,13 +356,12 @@ else:
def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]: 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_ = os.fspath(fn) stat = os.stat(fn)
stat = os.stat(fn_) source = fn.read_bytes()
with open(fn_, "rb") as f: strfn = str(fn)
source = f.read() tree = ast.parse(source, filename=strfn)
tree = ast.parse(source, filename=fn_) rewrite_asserts(tree, source, strfn, config)
rewrite_asserts(tree, source, fn_, config) co = compile(tree, strfn, "exec", dont_inherit=True)
co = compile(tree, fn_, "exec", dont_inherit=True)
return stat, co return stat, co
@ -365,14 +373,14 @@ def _read_pyc(
Return rewritten code if successful or None if not. Return rewritten code if successful or None if not.
""" """
try: try:
fp = open(os.fspath(pyc), "rb") fp = open(pyc, "rb")
except OSError: except OSError:
return None return None
with fp: with fp:
# https://www.python.org/dev/peps/pep-0552/ # https://www.python.org/dev/peps/pep-0552/
has_flags = sys.version_info >= (3, 7) has_flags = sys.version_info >= (3, 7)
try: try:
stat_result = os.stat(os.fspath(source)) stat_result = os.stat(source)
mtime = int(stat_result.st_mtime) mtime = int(stat_result.st_mtime)
size = stat_result.st_size size = stat_result.st_size
data = fp.read(16 if has_flags else 12) data = fp.read(16 if has_flags else 12)
@ -539,19 +547,11 @@ BINOP_MAP = {
} }
def set_location(node, lineno, col_offset): def traverse_node(node: ast.AST) -> Iterator[ast.AST]:
"""Set node location information recursively.""" """Recursively yield node and all its children in depth-first order."""
yield node
def _fix(node, lineno, col_offset): for child in ast.iter_child_nodes(node):
if "lineno" in node._attributes: yield from traverse_node(child)
node.lineno = lineno
if "col_offset" in node._attributes:
node.col_offset = col_offset
for child in ast.iter_child_nodes(node):
_fix(child, lineno, col_offset)
_fix(node, lineno, col_offset)
return node
@functools.lru_cache(maxsize=1) @functools.lru_cache(maxsize=1)
@ -862,7 +862,7 @@ class AssertionRewriter(ast.NodeVisitor):
"assertion is always true, perhaps remove parentheses?" "assertion is always true, perhaps remove parentheses?"
), ),
category=None, category=None,
filename=os.fspath(self.module_path), filename=self.module_path,
lineno=assert_.lineno, lineno=assert_.lineno,
) )
@ -954,9 +954,10 @@ class AssertionRewriter(ast.NodeVisitor):
variables = [ast.Name(name, ast.Store()) for name in self.variables] variables = [ast.Name(name, ast.Store()) for name in self.variables]
clear = ast.Assign(variables, ast.NameConstant(None)) clear = ast.Assign(variables, ast.NameConstant(None))
self.statements.append(clear) self.statements.append(clear)
# Fix line numbers. # Fix locations (line numbers/column offsets).
for stmt in self.statements: for stmt in self.statements:
set_location(stmt, assert_.lineno, assert_.col_offset) for node in traverse_node(stmt):
ast.copy_location(node, assert_)
return self.statements return self.statements
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]: def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
@ -1103,7 +1104,7 @@ def try_makedirs(cache_dir: Path) -> bool:
Returns True if successful or if it already exists. Returns True if successful or if it already exists.
""" """
try: try:
os.makedirs(os.fspath(cache_dir), exist_ok=True) os.makedirs(cache_dir, exist_ok=True)
except (FileNotFoundError, NotADirectoryError, FileExistsError): except (FileNotFoundError, NotADirectoryError, FileExistsError):
# One of the path components was not a directory: # One of the path components was not a directory:
# - we're in a zip file # - we're in a zip file

View File

@ -3,10 +3,10 @@
Current default behaviour is to truncate assertion explanations at Current default behaviour is to truncate assertion explanations at
~8 terminal lines, unless running in "-vv" mode or running on CI. ~8 terminal lines, unless running in "-vv" mode or running on CI.
""" """
import os
from typing import List from typing import List
from typing import Optional from typing import Optional
from _pytest.assertion import util
from _pytest.nodes import Item from _pytest.nodes import Item
@ -27,13 +27,7 @@ def truncate_if_required(
def _should_truncate_item(item: Item) -> bool: 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 verbose = item.config.option.verbose
return verbose < 2 and not _running_on_ci() return verbose < 2 and not util.running_on_ci()
def _running_on_ci() -> bool:
"""Check if we're currently running on a CI system."""
env_vars = ["CI", "BUILD_NUMBER"]
return any(var in os.environ for var in env_vars)
def _truncate_explanation( def _truncate_explanation(

View File

@ -1,5 +1,6 @@
"""Utilities for assertion debugging.""" """Utilities for assertion debugging."""
import collections.abc import collections.abc
import os
import pprint import pprint
from typing import AbstractSet from typing import AbstractSet
from typing import Any from typing import Any
@ -17,7 +18,6 @@ from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.config import Config from _pytest.config import Config
# The _reprcompare attribute on the util module is used by the new assertion # The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was # interpretation code and assertion rewriter to detect this plugin was
# loaded and in turn call the hooks defined here as part of the # loaded and in turn call the hooks defined here as part of the
@ -287,7 +287,7 @@ def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
def _compare_eq_iterable( def _compare_eq_iterable(
left: Iterable[Any], right: Iterable[Any], verbose: int = 0 left: Iterable[Any], right: Iterable[Any], verbose: int = 0
) -> List[str]: ) -> List[str]:
if not verbose: if not verbose and not running_on_ci():
return ["Use -v to get the full diff"] return ["Use -v to get the full diff"]
# dynamic import to speedup pytest # dynamic import to speedup pytest
import difflib import difflib
@ -490,3 +490,9 @@ def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
else: else:
newdiff.append(line) newdiff.append(line)
return newdiff return newdiff
def running_on_ci() -> bool:
"""Check if we're currently running on a CI system."""
env_vars = ["CI", "BUILD_NUMBER"]
return any(var in os.environ for var in env_vars)

View File

@ -128,7 +128,7 @@ class Cache:
it to manage files to e.g. store/retrieve database dumps across test it to manage files to e.g. store/retrieve database dumps across test
sessions. sessions.
.. versionadded:: 6.3 .. versionadded:: 7.0
:param name: :param name:
Must be a string not containing a ``/`` separator. Must be a string not containing a ``/`` separator.
@ -193,7 +193,7 @@ class Cache:
return return
if not cache_dir_exists_already: if not cache_dir_exists_already:
self._ensure_supporting_files() self._ensure_supporting_files()
data = json.dumps(value, indent=2, sort_keys=True) data = json.dumps(value, indent=2)
try: try:
f = path.open("w") f = path.open("w")
except OSError: except OSError:

View File

@ -13,9 +13,11 @@ import types
import warnings import warnings
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path
from textwrap import dedent
from types import TracebackType from types import TracebackType
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast
from typing import Dict from typing import Dict
from typing import Generator from typing import Generator
from typing import IO from typing import IO
@ -1612,17 +1614,54 @@ def parse_warning_filter(
) -> Tuple[str, str, Type[Warning], str, int]: ) -> Tuple[str, str, Type[Warning], str, int]:
"""Parse a warnings filter string. """Parse a warnings filter string.
This is copied from warnings._setoption, but does not apply the filter, This is copied from warnings._setoption with the following changes:
only parses it, and makes the escaping optional.
* Does not apply the filter.
* Escaping is optional.
* Raises UsageError so we get nice error messages on failure.
""" """
__tracebackhide__ = True
error_template = dedent(
f"""\
while parsing the following warning configuration:
{arg}
This error occurred:
{{error}}
"""
)
parts = arg.split(":") parts = arg.split(":")
if len(parts) > 5: if len(parts) > 5:
raise warnings._OptionError(f"too many fields (max 5): {arg!r}") doc_url = (
"https://docs.python.org/3/library/warnings.html#describing-warning-filters"
)
error = dedent(
f"""\
Too many fields ({len(parts)}), expected at most 5 separated by colons:
action:message:category:module:line
For more information please consult: {doc_url}
"""
)
raise UsageError(error_template.format(error=error))
while len(parts) < 5: while len(parts) < 5:
parts.append("") parts.append("")
action_, message, category_, module, lineno_ = (s.strip() for s in parts) action_, message, category_, module, lineno_ = (s.strip() for s in parts)
action: str = warnings._getaction(action_) # type: ignore[attr-defined] try:
category: Type[Warning] = warnings._getcategory(category_) # type: ignore[attr-defined] action: str = warnings._getaction(action_) # type: ignore[attr-defined]
except warnings._OptionError as e:
raise UsageError(error_template.format(error=str(e)))
try:
category: Type[Warning] = _resolve_warning_category(category_)
except Exception:
exc_info = ExceptionInfo.from_current()
exception_text = exc_info.getrepr(style="native")
raise UsageError(error_template.format(error=exception_text))
if message and escape: if message and escape:
message = re.escape(message) message = re.escape(message)
if module and escape: if module and escape:
@ -1631,14 +1670,38 @@ def parse_warning_filter(
try: try:
lineno = int(lineno_) lineno = int(lineno_)
if lineno < 0: if lineno < 0:
raise ValueError raise ValueError("number is negative")
except (ValueError, OverflowError) as e: except ValueError as e:
raise warnings._OptionError(f"invalid lineno {lineno_!r}") from e raise UsageError(
error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
)
else: else:
lineno = 0 lineno = 0
return action, message, category, module, lineno return action, message, category, module, lineno
def _resolve_warning_category(category: str) -> Type[Warning]:
"""
Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors)
propagate so we can get access to their tracebacks (#9218).
"""
__tracebackhide__ = True
if not category:
return Warning
if "." not in category:
import builtins as m
klass = category
else:
module, _, klass = category.rpartition(".")
m = __import__(module, None, None, [klass])
cat = getattr(m, klass)
if not issubclass(cat, Warning):
raise UsageError(f"{cat} is not a Warning subclass")
return cast(Type[Warning], cat)
def apply_warning_filters( def apply_warning_filters(
config_filters: Iterable[str], cmdline_filters: Iterable[str] config_filters: Iterable[str], cmdline_filters: Iterable[str]
) -> None: ) -> None:

View File

@ -185,7 +185,7 @@ class Parser:
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
* ``pathlist``: a list of ``py.path``, separated as in a shell * ``pathlist``: a list of ``py.path``, separated as in a shell
.. versionadded:: 6.3 .. versionadded:: 7.0
The ``paths`` variable type. The ``paths`` variable type.
Defaults to ``string`` if ``None`` or not passed. Defaults to ``string`` if ``None`` or not passed.

View File

@ -4,8 +4,9 @@ from pathlib import Path
from typing import Optional from typing import Optional
from ..compat import LEGACY_PATH from ..compat import LEGACY_PATH
from ..compat import legacy_path
from ..deprecated import HOOK_LEGACY_PATH_ARG from ..deprecated import HOOK_LEGACY_PATH_ARG
from _pytest.nodes import _imply_path from _pytest.nodes import _check_path
# hookname: (Path, LEGACY_PATH) # hookname: (Path, LEGACY_PATH)
imply_paths_hooks = { imply_paths_hooks = {
@ -52,7 +53,15 @@ class PathAwareHookProxy:
), ),
stacklevel=2, stacklevel=2,
) )
path_value, fspath_value = _imply_path(path_value, fspath_value) if path_value is not None:
if fspath_value is not None:
_check_path(path_value, fspath_value)
else:
fspath_value = legacy_path(path_value)
else:
assert fspath_value is not None
path_value = Path(fspath_value)
kw[path_var] = path_value kw[path_var] = path_value
kw[fspath_var] = fspath_value kw[fspath_var] = fspath_value
return hook(**kw) return hook(**kw)

View File

@ -88,7 +88,7 @@ def pytest_configure(config: Config) -> None:
pytestPDB._config, pytestPDB._config,
) = pytestPDB._saved.pop() ) = pytestPDB._saved.pop()
config._cleanup.append(fin) config.add_cleanup(fin)
class pytestPDB: class pytestPDB:

View File

@ -101,6 +101,14 @@ HOOK_LEGACY_PATH_ARG = UnformattedWarning(
"#py-path-local-arguments-for-hooks-replaced-with-pathlib-path", "#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
) )
NODE_CTOR_FSPATH_ARG = UnformattedWarning(
PytestDeprecationWarning,
"The (fspath: py.path.local) argument to {node_type_name} is deprecated. "
"Please use the (path: pathlib.Path) argument instead.\n"
"See https://docs.pytest.org/en/latest/deprecations.html"
"#fspath-argument-for-node-constructors-replaced-with-pathlib-path",
)
WARNS_NONE_ARG = PytestDeprecationWarning( WARNS_NONE_ARG = PytestDeprecationWarning(
"Passing None to catch any warning has been deprecated, pass no arguments instead:\n" "Passing None to catch any warning has been deprecated, pass no arguments instead:\n"
" Replace pytest.warns(None) by simply pytest.warns()." " Replace pytest.warns(None) by simply pytest.warns()."

View File

@ -1,6 +1,7 @@
"""Discover and run doctests in modules and test files.""" """Discover and run doctests in modules and test files."""
import bdb import bdb
import inspect import inspect
import os
import platform import platform
import sys import sys
import traceback import traceback
@ -28,7 +29,6 @@ from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprFileLocation from _pytest._code.code import ReprFileLocation
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import legacy_path
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.config import Config from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
@ -371,9 +371,9 @@ class DoctestItem(pytest.Item):
reprlocation_lines.append((reprlocation, lines)) reprlocation_lines.append((reprlocation, lines))
return ReprFailDoctest(reprlocation_lines) return ReprFailDoctest(reprlocation_lines)
def reportinfo(self): def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
assert self.dtest is not None assert self.dtest is not None
return legacy_path(self.path), self.dtest.lineno, "[doctest] %s" % self.name return self.path, self.dtest.lineno, "[doctest] %s" % self.name
def _get_flag_lookup() -> Dict[str, int]: def _get_flag_lookup() -> Dict[str, int]:

View File

@ -9,11 +9,9 @@ from typing import Union
def freeze_includes() -> List[str]: def freeze_includes() -> List[str]:
"""Return a list of module names used by pytest that should be """Return a list of module names used by pytest that should be
included by cx_freeze.""" included by cx_freeze."""
import py
import _pytest import _pytest
result = list(_iter_all_modules(py)) result = list(_iter_all_modules(_pytest))
result += list(_iter_all_modules(_pytest))
return result return result

View File

@ -6,8 +6,6 @@ from typing import List
from typing import Optional from typing import Optional
from typing import Union from typing import Union
import py
import pytest import pytest
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
@ -108,11 +106,10 @@ def pytest_cmdline_parse():
path = config.option.debug path = config.option.debug
debugfile = open(path, "w") debugfile = open(path, "w")
debugfile.write( debugfile.write(
"versions pytest-%s, py-%s, " "versions pytest-%s, "
"python-%s\ncwd=%s\nargs=%s\n\n" "python-%s\ncwd=%s\nargs=%s\n\n"
% ( % (
pytest.__version__, pytest.__version__,
py.__version__,
".".join(map(str, sys.version_info)), ".".join(map(str, sys.version_info)),
os.getcwd(), os.getcwd(),
config.invocation_params.args, config.invocation_params.args,
@ -249,7 +246,7 @@ def getpluginversioninfo(config: Config) -> List[str]:
def pytest_report_header(config: Config) -> List[str]: def pytest_report_header(config: Config) -> List[str]:
lines = [] lines = []
if config.option.debug or config.option.traceconfig: if config.option.debug or config.option.traceconfig:
lines.append(f"using: pytest-{pytest.__version__} pylib-{py.__version__}") lines.append(f"using: pytest-{pytest.__version__}")
verinfo = getpluginversioninfo(config) verinfo = getpluginversioninfo(config)
if verinfo: if verinfo:

View File

@ -272,12 +272,13 @@ def pytest_ignore_collect(
Stops at first non-None result, see :ref:`firstresult`. Stops at first non-None result, see :ref:`firstresult`.
:param pathlib.Path fspath: The path to analyze. :param pathlib.Path fspath: The path to analyze.
:param LEGACY_PATH path: The path to analyze. :param LEGACY_PATH path: The path to analyze (deprecated).
:param pytest.Config config: The pytest config object. :param pytest.Config config: The pytest config object.
.. versionchanged:: 6.3.0 .. versionchanged:: 7.0.0
The ``fspath`` parameter was added as a :class:`pathlib.Path` The ``fspath`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
""" """
@ -289,11 +290,12 @@ def pytest_collect_file(
The new node needs to have the specified ``parent`` as a parent. The new node needs to have the specified ``parent`` as a parent.
:param pathlib.Path fspath: The path to analyze. :param pathlib.Path fspath: The path to analyze.
:param LEGACY_PATH path: The path to collect. :param LEGACY_PATH path: The path to collect (deprecated).
.. versionchanged:: 6.3.0 .. versionchanged:: 7.0.0
The ``fspath`` parameter was added as a :class:`pathlib.Path` The ``fspath`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
""" """
@ -345,11 +347,13 @@ def pytest_pycollect_makemodule(
Stops at first non-None result, see :ref:`firstresult`. Stops at first non-None result, see :ref:`firstresult`.
:param pathlib.Path fspath: The path of the module to collect. :param pathlib.Path fspath: The path of the module to collect.
:param legacy_path path: The path of the module to collect. :param LEGACY_PATH path: The path of the module to collect (deprecated).
.. versionchanged:: 6.3.0 .. versionchanged:: 7.0.0
The ``fspath`` parameter was added as a :class:`pathlib.Path` The ``fspath`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. equivalent of the ``path`` parameter.
The ``path`` parameter has been deprecated in favor of ``fspath``.
""" """
@ -674,7 +678,7 @@ def pytest_report_header(
:param pytest.Config config: The pytest config object. :param pytest.Config config: The pytest config object.
:param Path startpath: The starting dir. :param Path startpath: The starting dir.
:param LEGACY_PATH startdir: The starting dir. :param LEGACY_PATH startdir: The starting dir (deprecated).
.. note:: .. note::
@ -689,9 +693,10 @@ def pytest_report_header(
files situated at the tests root directory due to how pytest files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`. :ref:`discovers plugins during startup <pluginorder>`.
.. versionchanged:: 6.3.0 .. versionchanged:: 7.0.0
The ``startpath`` parameter was added as a :class:`pathlib.Path` The ``startpath`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter. equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
""" """
@ -709,8 +714,8 @@ def pytest_report_collectionfinish(
.. versionadded:: 3.2 .. versionadded:: 3.2
:param pytest.Config config: The pytest config object. :param pytest.Config config: The pytest config object.
:param Path startpath: The starting path. :param Path startpath: The starting dir.
:param LEGACY_PATH startdir: The starting dir. :param LEGACY_PATH startdir: The starting dir (deprecated).
:param items: List of pytest items that are going to be executed; this list should not be modified. :param items: List of pytest items that are going to be executed; this list should not be modified.
.. note:: .. note::
@ -720,9 +725,10 @@ def pytest_report_collectionfinish(
If you want to have your line(s) displayed first, use If you want to have your line(s) displayed first, use
:ref:`trylast=True <plugin-hookorder>`. :ref:`trylast=True <plugin-hookorder>`.
.. versionchanged:: 6.3.0 .. versionchanged:: 7.0.0
The ``startpath`` parameter was added as a :class:`pathlib.Path` The ``startpath`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter. equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
""" """

View File

@ -500,7 +500,7 @@ class Session(nodes.FSCollector):
def startpath(self) -> Path: def startpath(self) -> Path:
"""The path from which pytest was invoked. """The path from which pytest was invoked.
.. versionadded:: 6.3.0 .. versionadded:: 7.0.0
""" """
return self.config.invocation_params.dir return self.config.invocation_params.dir

View File

@ -6,7 +6,7 @@ expression: expr? EOF
expr: and_expr ('or' and_expr)* expr: and_expr ('or' and_expr)*
and_expr: not_expr ('and' not_expr)* and_expr: not_expr ('and' not_expr)*
not_expr: 'not' not_expr | '(' expr ')' | ident not_expr: 'not' not_expr | '(' expr ')' | ident
ident: (\w|:|\+|-|\.|\[|\]|\\|\/)+ ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
The semantics are: The semantics are:
@ -88,7 +88,7 @@ class Scanner:
yield Token(TokenType.RPAREN, ")", pos) yield Token(TokenType.RPAREN, ")", pos)
pos += 1 pos += 1
else: else:
match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|\/)+", input[pos:]) match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
if match: if match:
value = match.group(0) value = match.group(0)
if value == "or": if value == "or":

View File

@ -539,6 +539,8 @@ MARK_GEN = MarkGenerator(_ispytest=True)
@final @final
class NodeKeywords(MutableMapping[str, Any]): class NodeKeywords(MutableMapping[str, Any]):
__slots__ = ("node", "parent", "_markers")
def __init__(self, node: "Node") -> None: def __init__(self, node: "Node") -> None:
self.node = node self.node = node
self.parent = node.parent self.parent = node.parent
@ -555,21 +557,39 @@ class NodeKeywords(MutableMapping[str, Any]):
def __setitem__(self, key: str, value: Any) -> None: def __setitem__(self, key: str, value: Any) -> None:
self._markers[key] = value self._markers[key] = value
# Note: we could've avoided explicitly implementing some of the methods
# below and use the collections.abc fallback, but that would be slow.
def __contains__(self, key: object) -> bool:
return (
key in self._markers
or self.parent is not None
and key in self.parent.keywords
)
def update( # type: ignore[override]
self,
other: Union[Mapping[str, Any], Iterable[Tuple[str, Any]]] = (),
**kwds: Any,
) -> None:
self._markers.update(other)
self._markers.update(kwds)
def __delitem__(self, key: str) -> None: def __delitem__(self, key: str) -> None:
raise ValueError("cannot delete key in keywords dict") raise ValueError("cannot delete key in keywords dict")
def __iter__(self) -> Iterator[str]: def __iter__(self) -> Iterator[str]:
seen = self._seen() # Doesn't need to be fast.
return iter(seen) yield from self._markers
def _seen(self) -> Set[str]:
seen = set(self._markers)
if self.parent is not None: if self.parent is not None:
seen.update(self.parent.keywords) for keyword in self.parent.keywords:
return seen # self._marks and self.parent.keywords can have duplicates.
if keyword not in self._markers:
yield keyword
def __len__(self) -> int: def __len__(self) -> int:
return len(self._seen()) # Doesn't need to be fast.
return sum(1 for keyword in self)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<NodeKeywords for node {self.node}>" return f"<NodeKeywords for node {self.node}>"

View File

@ -28,6 +28,7 @@ from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
from _pytest.mark.structures import Mark from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords from _pytest.mark.structures import NodeKeywords
@ -93,24 +94,33 @@ def iterparentnodeids(nodeid: str) -> Iterator[str]:
yield nodeid yield nodeid
def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
if Path(fspath) != path:
raise ValueError(
f"Path({fspath!r}) != {path!r}\n"
"if both path and fspath are given they need to be equal"
)
def _imply_path( def _imply_path(
path: Optional[Path], fspath: Optional[LEGACY_PATH] node_type: Type["Node"],
) -> Tuple[Path, LEGACY_PATH]: path: Optional[Path],
fspath: Optional[LEGACY_PATH],
) -> Path:
if fspath is not None:
warnings.warn(
NODE_CTOR_FSPATH_ARG.format(
node_type_name=node_type.__name__,
),
stacklevel=3,
)
if path is not None: if path is not None:
if fspath is not None: if fspath is not None:
if Path(fspath) != path: _check_path(path, fspath)
raise ValueError( return path
f"Path({fspath!r}) != {path!r}\n"
"if both path and fspath are given they need to be equal"
)
assert Path(fspath) == path, f"{fspath} != {path}"
else:
fspath = legacy_path(path)
return path, fspath
else: else:
assert fspath is not None assert fspath is not None
return Path(fspath), fspath return Path(fspath)
_NodeType = TypeVar("_NodeType", bound="Node") _NodeType = TypeVar("_NodeType", bound="Node")
@ -123,7 +133,7 @@ class NodeMeta(type):
"See " "See "
"https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent" "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
" for more details." " for more details."
).format(name=self.__name__) ).format(name=f"{self.__module__}.{self.__name__}")
fail(msg, pytrace=False) fail(msg, pytrace=False)
def _create(self, *k, **kw): def _create(self, *k, **kw):
@ -196,7 +206,9 @@ class Node(metaclass=NodeMeta):
self.session = parent.session 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.path = _imply_path(path or getattr(parent, "path", None), fspath=fspath)[0] if path is None and fspath is None:
path = getattr(parent, "path", None)
self.path = _imply_path(type(self), path, fspath=fspath)
# The explicit annotation is to avoid publicly exposing NodeKeywords. # The explicit annotation is to avoid publicly exposing NodeKeywords.
#: Keywords/markers collected from all scopes. #: Keywords/markers collected from all scopes.
@ -573,7 +585,7 @@ class FSCollector(Collector):
assert path is None assert path is None
path = path_or_parent path = path_or_parent
path, fspath = _imply_path(path, fspath=fspath) path = _imply_path(type(self), path, fspath=fspath)
if name is None: if name is None:
name = path.name name = path.name
if parent is not None and parent.path != path: if parent is not None and parent.path != path:
@ -618,7 +630,6 @@ class FSCollector(Collector):
**kw, **kw,
): ):
"""The public constructor.""" """The public constructor."""
path, fspath = _imply_path(path, fspath=fspath)
return super().from_parent(parent=parent, fspath=fspath, path=path, **kw) return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
def gethookproxy(self, fspath: "os.PathLike[str]"): def gethookproxy(self, fspath: "os.PathLike[str]"):
@ -702,15 +713,13 @@ class Item(Node):
if content: if content:
self._report_sections.append((when, key, content)) self._report_sections.append((when, key, content))
def reportinfo(self) -> Tuple[Union[LEGACY_PATH, str], Optional[int], str]: def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
return self.path, None, ""
# TODO: enable Path objects in reportinfo
return legacy_path(self.path), None, ""
@cached_property @cached_property
def location(self) -> Tuple[str, Optional[int], str]: def location(self) -> Tuple[str, Optional[int], str]:
location = self.reportinfo() location = self.reportinfo()
fspath = absolutepath(str(location[0])) path = absolutepath(os.fspath(location[0]))
relfspath = self.session._node_location_to_relpath(fspath) relfspath = self.session._node_location_to_relpath(path)
assert type(location[2]) is str assert type(location[2]) is str
return (relfspath, location[1], location[2]) return (relfspath, location[1], location[2])

View File

@ -77,7 +77,7 @@ def create_new_paste(contents: Union[str, bytes]) -> str:
from urllib.parse import urlencode from urllib.parse import urlencode
params = {"code": contents, "lexer": "text", "expiry": "1week"} params = {"code": contents, "lexer": "text", "expiry": "1week"}
url = "https://bpaste.net" url = "https://bpa.st"
try: try:
response: str = ( response: str = (
urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8") urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")

View File

@ -214,7 +214,20 @@ def get_public_names(values: Iterable[str]) -> List[str]:
return [x for x in values if x[0] != "_"] return [x for x in values if x[0] != "_"]
class ParsedCall: @final
class RecordedHookCall:
"""A recorded call to a hook.
The arguments to the hook call are set as attributes.
For example:
.. code-block:: python
calls = hook_recorder.getcalls("pytest_runtest_setup")
# Suppose pytest_runtest_setup was called once with `item=an_item`.
assert calls[0].item is an_item
"""
def __init__(self, name: str, kwargs) -> None: def __init__(self, name: str, kwargs) -> None:
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
self._name = name self._name = name
@ -222,7 +235,7 @@ class ParsedCall:
def __repr__(self) -> str: def __repr__(self) -> str:
d = self.__dict__.copy() d = self.__dict__.copy()
del d["_name"] del d["_name"]
return f"<ParsedCall {self._name!r}(**{d!r})>" return f"<RecordedHookCall {self._name!r}(**{d!r})>"
if TYPE_CHECKING: if TYPE_CHECKING:
# The class has undetermined attributes, this tells mypy about it. # The class has undetermined attributes, this tells mypy about it.
@ -230,20 +243,27 @@ class ParsedCall:
... ...
@final
class HookRecorder: class HookRecorder:
"""Record all hooks called in a plugin manager. """Record all hooks called in a plugin manager.
Hook recorders are created by :class:`Pytester`.
This wraps all the hook calls in the plugin manager, recording each call This wraps all the hook calls in the plugin manager, recording each call
before propagating the normal calls. before propagating the normal calls.
""" """
def __init__(self, pluginmanager: PytestPluginManager) -> None: def __init__(
self, pluginmanager: PytestPluginManager, *, _ispytest: bool = False
) -> None:
check_ispytest(_ispytest)
self._pluginmanager = pluginmanager self._pluginmanager = pluginmanager
self.calls: List[ParsedCall] = [] self.calls: List[RecordedHookCall] = []
self.ret: Optional[Union[int, ExitCode]] = None self.ret: Optional[Union[int, ExitCode]] = None
def before(hook_name: str, hook_impls, kwargs) -> None: def before(hook_name: str, hook_impls, kwargs) -> None:
self.calls.append(ParsedCall(hook_name, kwargs)) self.calls.append(RecordedHookCall(hook_name, kwargs))
def after(outcome, hook_name: str, hook_impls, kwargs) -> None: def after(outcome, hook_name: str, hook_impls, kwargs) -> None:
pass pass
@ -253,7 +273,8 @@ class HookRecorder:
def finish_recording(self) -> None: def finish_recording(self) -> None:
self._undo_wrapping() self._undo_wrapping()
def getcalls(self, names: Union[str, Iterable[str]]) -> List[ParsedCall]: def getcalls(self, names: Union[str, Iterable[str]]) -> List[RecordedHookCall]:
"""Get all recorded calls to hooks with the given names (or name)."""
if isinstance(names, str): if isinstance(names, str):
names = names.split() names = names.split()
return [call for call in self.calls if call._name in names] return [call for call in self.calls if call._name in names]
@ -279,7 +300,7 @@ class HookRecorder:
else: else:
fail(f"could not find {name!r} check {check!r}") fail(f"could not find {name!r} check {check!r}")
def popcall(self, name: str) -> ParsedCall: def popcall(self, name: str) -> RecordedHookCall:
__tracebackhide__ = True __tracebackhide__ = True
for i, call in enumerate(self.calls): for i, call in enumerate(self.calls):
if call._name == name: if call._name == name:
@ -289,7 +310,7 @@ class HookRecorder:
lines.extend([" %s" % x for x in self.calls]) lines.extend([" %s" % x for x in self.calls])
fail("\n".join(lines)) fail("\n".join(lines))
def getcall(self, name: str) -> ParsedCall: def getcall(self, name: str) -> RecordedHookCall:
values = self.getcalls(name) values = self.getcalls(name)
assert len(values) == 1, (name, values) assert len(values) == 1, (name, values)
return values[0] return values[0]
@ -507,8 +528,9 @@ rex_session_duration = re.compile(r"\d+\.\d\ds")
rex_outcome = re.compile(r"(\d+) (\w+)") rex_outcome = re.compile(r"(\d+) (\w+)")
@final
class RunResult: class RunResult:
"""The result of running a command.""" """The result of running a command from :class:`~pytest.Pytester`."""
def __init__( def __init__(
self, self,
@ -527,13 +549,13 @@ class RunResult:
self.errlines = errlines self.errlines = errlines
"""List of lines captured from stderr.""" """List of lines captured from stderr."""
self.stdout = LineMatcher(outlines) self.stdout = LineMatcher(outlines)
""":class:`LineMatcher` of stdout. """:class:`~pytest.LineMatcher` of stdout.
Use e.g. :func:`str(stdout) <LineMatcher.__str__()>` to reconstruct stdout, or the commonly used Use e.g. :func:`str(stdout) <pytest.LineMatcher.__str__()>` to reconstruct stdout, or the commonly used
:func:`stdout.fnmatch_lines() <LineMatcher.fnmatch_lines()>` method. :func:`stdout.fnmatch_lines() <pytest.LineMatcher.fnmatch_lines()>` method.
""" """
self.stderr = LineMatcher(errlines) self.stderr = LineMatcher(errlines)
""":class:`LineMatcher` of stderr.""" """:class:`~pytest.LineMatcher` of stderr."""
self.duration = duration self.duration = duration
"""Duration in seconds.""" """Duration in seconds."""
@ -588,6 +610,7 @@ class RunResult:
xpassed: int = 0, xpassed: int = 0,
xfailed: int = 0, xfailed: int = 0,
warnings: int = 0, warnings: int = 0,
deselected: int = 0,
) -> None: ) -> None:
"""Assert that the specified outcomes appear with the respective """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."""
@ -604,6 +627,7 @@ class RunResult:
xpassed=xpassed, xpassed=xpassed,
xfailed=xfailed, xfailed=xfailed,
warnings=warnings, warnings=warnings,
deselected=deselected,
) )
@ -739,7 +763,7 @@ class Pytester:
def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder: def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
"""Create a new :py:class:`HookRecorder` for a PluginManager.""" """Create a new :py:class:`HookRecorder` for a PluginManager."""
pluginmanager.reprec = reprec = HookRecorder(pluginmanager) pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True)
self._request.addfinalizer(reprec.finish_recording) self._request.addfinalizer(reprec.finish_recording)
return reprec return reprec
@ -948,8 +972,6 @@ class Pytester:
f'example "{example_path}" is not found as a file or directory' f'example "{example_path}" is not found as a file or directory'
) )
Session = Session
def getnode( def getnode(
self, config: Config, arg: Union[str, "os.PathLike[str]"] self, config: Config, arg: Union[str, "os.PathLike[str]"]
) -> Optional[Union[Collector, Item]]: ) -> Optional[Union[Collector, Item]]:
@ -1021,10 +1043,7 @@ class Pytester:
for the result. 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. :param cmdlineargs: Any extra command line arguments to use.
:returns: :py:class:`HookRecorder` instance of the result.
""" """
p = self.makepyfile(source) p = self.makepyfile(source)
values = list(cmdlineargs) + [p] values = list(cmdlineargs) + [p]
@ -1062,8 +1081,6 @@ class Pytester:
:param no_reraise_ctrlc: :param no_reraise_ctrlc:
Typically we reraise keyboard interrupts from the child run. If Typically we reraise keyboard interrupts from the child run. If
True, the KeyboardInterrupt exception is captured. True, the KeyboardInterrupt exception is captured.
:returns: A :py:class:`HookRecorder` instance.
""" """
# (maybe a cpython bug?) the importlib cache sometimes isn't updated # (maybe a cpython bug?) the importlib cache sometimes isn't updated
# properly between file creation and inline_run (especially if imports # properly between file creation and inline_run (especially if imports
@ -1162,7 +1179,7 @@ class Pytester:
self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any
) -> RunResult: ) -> RunResult:
"""Run pytest inline or in a subprocess, depending on the command line """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:`~pytest.RunResult`."""
new_args = self._ensure_basetemp(args) new_args = self._ensure_basetemp(args)
if self._method == "inprocess": if self._method == "inprocess":
return self.runpytest_inprocess(*new_args, **kwargs) return self.runpytest_inprocess(*new_args, **kwargs)
@ -1370,9 +1387,7 @@ class Pytester:
""" """
__tracebackhide__ = True __tracebackhide__ = True
cmdargs = tuple( cmdargs = tuple(os.fspath(arg) for arg in cmdargs)
os.fspath(arg) if isinstance(arg, os.PathLike) else arg for arg in cmdargs
)
p1 = self.path.joinpath("stdout") p1 = self.path.joinpath("stdout")
p2 = self.path.joinpath("stderr") p2 = self.path.joinpath("stderr")
print("running:", *cmdargs) print("running:", *cmdargs)
@ -1506,7 +1521,7 @@ class LineComp:
def assert_contains_lines(self, lines2: Sequence[str]) -> None: def assert_contains_lines(self, lines2: Sequence[str]) -> None:
"""Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value. """Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value.
Lines are matched using :func:`LineMatcher.fnmatch_lines`. Lines are matched using :func:`LineMatcher.fnmatch_lines <pytest.LineMatcher.fnmatch_lines>`.
""" """
__tracebackhide__ = True __tracebackhide__ = True
val = self.stringio.getvalue() val = self.stringio.getvalue()
@ -1529,7 +1544,6 @@ class Testdir:
CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN
TimeoutExpired: "Final" = Pytester.TimeoutExpired TimeoutExpired: "Final" = Pytester.TimeoutExpired
Session: "Final" = Pytester.Session
def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None: def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest) check_ispytest(_ispytest)
@ -1734,6 +1748,7 @@ class Testdir:
return str(self.tmpdir) return str(self.tmpdir)
@final
class LineMatcher: class LineMatcher:
"""Flexible matching of text. """Flexible matching of text.

View File

@ -43,6 +43,7 @@ def assert_outcomes(
xpassed: int = 0, xpassed: int = 0,
xfailed: int = 0, xfailed: int = 0,
warnings: int = 0, warnings: int = 0,
deselected: int = 0,
) -> None: ) -> None:
"""Assert that the specified outcomes appear with the respective """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."""
@ -56,6 +57,7 @@ def assert_outcomes(
"xpassed": outcomes.get("xpassed", 0), "xpassed": outcomes.get("xpassed", 0),
"xfailed": outcomes.get("xfailed", 0), "xfailed": outcomes.get("xfailed", 0),
"warnings": outcomes.get("warnings", 0), "warnings": outcomes.get("warnings", 0),
"deselected": outcomes.get("deselected", 0),
} }
expected = { expected = {
"passed": passed, "passed": passed,
@ -65,5 +67,6 @@ def assert_outcomes(
"xpassed": xpassed, "xpassed": xpassed,
"xfailed": xfailed, "xfailed": xfailed,
"warnings": warnings, "warnings": warnings,
"deselected": deselected,
} }
assert obtained == expected assert obtained == expected

View File

@ -48,7 +48,6 @@ from _pytest.compat import getlocation
from _pytest.compat import is_async_function from _pytest.compat import is_async_function
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import LEGACY_PATH from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.compat import safe_isclass from _pytest.compat import safe_isclass
@ -321,7 +320,7 @@ class PyobjMixin(nodes.Node):
parts.reverse() parts.reverse()
return ".".join(parts) return ".".join(parts)
def reportinfo(self) -> Tuple[Union[LEGACY_PATH, str], int, str]: def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
# XXX caching? # XXX caching?
obj = self.obj obj = self.obj
compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None)
@ -330,17 +329,13 @@ class PyobjMixin(nodes.Node):
file_path = sys.modules[obj.__module__].__file__ file_path = sys.modules[obj.__module__].__file__
if file_path.endswith(".pyc"): if file_path.endswith(".pyc"):
file_path = file_path[:-1] file_path = file_path[:-1]
fspath: Union[LEGACY_PATH, str] = file_path path: Union["os.PathLike[str]", str] = file_path
lineno = compat_co_firstlineno lineno = compat_co_firstlineno
else: else:
path, lineno = getfslineno(obj) path, lineno = getfslineno(obj)
if isinstance(path, Path):
fspath = legacy_path(path)
else:
fspath = path
modpath = self.getmodpath() modpath = self.getmodpath()
assert isinstance(lineno, int) assert isinstance(lineno, int)
return fspath, lineno, modpath return path, lineno, modpath
# As an optimization, these builtin attribute names are pre-ignored when # As an optimization, these builtin attribute names are pre-ignored when
@ -639,7 +634,6 @@ class Package(Module):
) -> 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) # nodes.FSCollector.__init__(self, fspath, parent=parent)
path, fspath = nodes._imply_path(path, fspath=fspath)
session = parent.session session = parent.session
nodes.FSCollector.__init__( nodes.FSCollector.__init__(
self, self,
@ -650,7 +644,7 @@ class Package(Module):
session=session, session=session,
nodeid=nodeid, nodeid=nodeid,
) )
self.name = path.parent.name self.name = self.path.parent.name
def setup(self) -> None: def setup(self) -> None:
# Not using fixtures to call setup_module here because autouse fixtures # Not using fixtures to call setup_module here because autouse fixtures

View File

@ -136,7 +136,7 @@ def warns(
... warnings.warn("this is not here", UserWarning) ... warnings.warn("this is not here", UserWarning)
Traceback (most recent call last): Traceback (most recent call last):
... ...
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
""" """
__tracebackhide__ = True __tracebackhide__ = True
@ -274,7 +274,7 @@ class WarningsChecker(WarningsRecorder):
if not any(issubclass(r.category, self.expected_warning) for r in self): if not any(issubclass(r.category, self.expected_warning) for r in self):
__tracebackhide__ = True __tracebackhide__ = True
fail( fail(
"DID NOT WARN. No warnings of type {} was emitted. " "DID NOT WARN. No warnings of type {} were emitted. "
"The list of emitted warnings is: {}.".format( "The list of emitted warnings is: {}.".format(
self.expected_warning, [each.message for each in self] self.expected_warning, [each.message for each in self]
) )
@ -287,7 +287,7 @@ class WarningsChecker(WarningsRecorder):
else: else:
fail( fail(
"DID NOT WARN. No warnings of type {} matching" "DID NOT WARN. No warnings of type {} matching"
" ('{}') was emitted. The list of emitted warnings" " ('{}') were emitted. The list of emitted warnings"
" is: {}.".format( " is: {}.".format(
self.expected_warning, self.expected_warning,
self.match_expr, self.match_expr,

View File

@ -324,9 +324,9 @@ class TestReport(BaseReport):
outcome = "skipped" outcome = "skipped"
r = excinfo._getreprcrash() r = excinfo._getreprcrash()
if excinfo.value._use_item_location: if excinfo.value._use_item_location:
filename, line = item.reportinfo()[:2] path, line = item.reportinfo()[:2]
assert line is not None assert line is not None
longrepr = str(filename), line + 1, r.message longrepr = os.fspath(path), line + 1, r.message
else: else:
longrepr = (str(r.path), r.lineno, r.message) longrepr = (str(r.path), r.lineno, r.message)
else: else:

View File

@ -49,7 +49,7 @@ def pytest_configure(config: Config) -> None:
import pytest import pytest
old = pytest.xfail old = pytest.xfail
config._cleanup.append(lambda: setattr(pytest, "xfail", old)) config.add_cleanup(lambda: setattr(pytest, "xfail", old))
def nop(*args, **kwargs): def nop(*args, **kwargs):
pass pass

View File

@ -29,7 +29,6 @@ from typing import Union
import attr import attr
import pluggy import pluggy
import py
import _pytest._version import _pytest._version
from _pytest import nodes from _pytest import nodes
@ -704,8 +703,8 @@ class TerminalReporter:
if pypy_version_info: if pypy_version_info:
verinfo = ".".join(map(str, pypy_version_info[:3])) verinfo = ".".join(map(str, pypy_version_info[:3]))
msg += f"[pypy-{verinfo}-{pypy_version_info[3]}]" msg += f"[pypy-{verinfo}-{pypy_version_info[3]}]"
msg += ", pytest-{}, py-{}, pluggy-{}".format( msg += ", pytest-{}, pluggy-{}".format(
_pytest._version.version, py.__version__, pluggy.__version__ _pytest._version.version, pluggy.__version__
) )
if ( if (
self.verbosity > 0 self.verbosity > 0

View File

@ -199,11 +199,11 @@ def pytest_configure(config: Config) -> None:
to the tmp_path_factory session fixture. to the tmp_path_factory session fixture.
""" """
mp = MonkeyPatch() mp = MonkeyPatch()
tmppath_handler = TempPathFactory.from_config(config, _ispytest=True) config.add_cleanup(mp.undo)
t = TempdirFactory(tmppath_handler, _ispytest=True) _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True)
config._cleanup.append(mp.undo) _tmpdirhandler = TempdirFactory(_tmp_path_factory, _ispytest=True)
mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False) mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)
mp.setattr(config, "_tmpdirhandler", t, raising=False) mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False)
@fixture(scope="session") @fixture(scope="session")

View File

@ -41,7 +41,11 @@ from _pytest.outcomes import fail
from _pytest.outcomes import importorskip from _pytest.outcomes import importorskip
from _pytest.outcomes import skip from _pytest.outcomes import skip
from _pytest.outcomes import xfail from _pytest.outcomes import xfail
from _pytest.pytester import HookRecorder
from _pytest.pytester import LineMatcher
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.pytester import RecordedHookCall
from _pytest.pytester import RunResult
from _pytest.pytester import Testdir from _pytest.pytester import Testdir
from _pytest.python import Class from _pytest.python import Class
from _pytest.python import Function from _pytest.python import Function
@ -98,10 +102,12 @@ __all__ = [
"freeze_includes", "freeze_includes",
"Function", "Function",
"hookimpl", "hookimpl",
"HookRecorder",
"hookspec", "hookspec",
"importorskip", "importorskip",
"Instance", "Instance",
"Item", "Item",
"LineMatcher",
"LogCaptureFixture", "LogCaptureFixture",
"main", "main",
"mark", "mark",
@ -129,7 +135,9 @@ __all__ = [
"PytestUnraisableExceptionWarning", "PytestUnraisableExceptionWarning",
"PytestWarning", "PytestWarning",
"raises", "raises",
"RecordedHookCall",
"register_assert_rewrite", "register_assert_rewrite",
"RunResult",
"Session", "Session",
"set_trace", "set_trace",
"skip", "skip",

View File

@ -3,7 +3,6 @@ import sys
import types import types
import attr import attr
import py
import pytest import pytest
from _pytest.compat import importlib_metadata from _pytest.compat import importlib_metadata
@ -515,28 +514,10 @@ class TestInvocationVariants:
assert result.ret == 0 assert result.ret == 0
def test_pydoc(self, pytester: Pytester) -> None: def test_pydoc(self, pytester: Pytester) -> None:
for name in ("py.test", "pytest"): result = pytester.runpython_c("import pytest;help(pytest)")
result = pytester.runpython_c(f"import {name};help({name})")
assert result.ret == 0
s = result.stdout.str()
assert "MarkGenerator" in s
def test_import_star_py_dot_test(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
from py.test import *
#collect
#cmdline
#Item
# assert collect.Item is Item
# assert collect.Collector is Collector
main
skip
xfail
"""
)
result = pytester.runpython(p)
assert result.ret == 0 assert result.ret == 0
s = result.stdout.str()
assert "MarkGenerator" in s
def test_import_star_pytest(self, pytester: Pytester) -> None: def test_import_star_pytest(self, pytester: Pytester) -> None:
p = pytester.makepyfile( p = pytester.makepyfile(
@ -585,10 +566,6 @@ class TestInvocationVariants:
assert res.ret == 0 assert res.ret == 0
res.stdout.fnmatch_lines(["*1 passed*"]) res.stdout.fnmatch_lines(["*1 passed*"])
def test_equivalence_pytest_pydottest(self) -> None:
# Type ignored because `py.test` is not and will not be typed.
assert pytest.main == py.test.cmdline.main # type: ignore[attr-defined]
def test_invoke_with_invalid_type(self) -> None: def test_invoke_with_invalid_type(self) -> None:
with pytest.raises( with pytest.raises(
TypeError, match="expected to be a list of strings, got: '-h'" TypeError, match="expected to be a list of strings, got: '-h'"

View File

@ -1,6 +1,7 @@
import re import re
import sys import sys
import warnings import warnings
from pathlib import Path
from unittest import mock from unittest import mock
import pytest import pytest
@ -179,6 +180,13 @@ def test_hookproxy_warnings_for_fspath(tmp_path, hooktype, request):
hooks.pytest_ignore_collect(config=request.config, fspath=tmp_path) hooks.pytest_ignore_collect(config=request.config, fspath=tmp_path)
# Passing entirely *different* paths is an outright error.
with pytest.raises(ValueError, match=r"path.*fspath.*need to be equal"):
with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
hooks.pytest_ignore_collect(
config=request.config, path=path, fspath=Path("/bla/bla")
)
def test_warns_none_is_deprecated(): def test_warns_none_is_deprecated():
with pytest.warns( with pytest.warns(
@ -207,3 +215,16 @@ def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None:
"*Please use pytest_load_initial_conftests hook instead.*", "*Please use pytest_load_initial_conftests hook instead.*",
] ]
) )
def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
mod = pytester.getmodulecol("")
with pytest.warns(
pytest.PytestDeprecationWarning,
match=re.escape("The (fspath: py.path.local) argument to File is deprecated."),
):
pytest.File.from_parent(
parent=mod.parent,
fspath=legacy_path("bla"),
)

View File

@ -1,10 +1,10 @@
anyio[curio,trio]==3.3.2 anyio[curio,trio]==3.3.4
django==3.2.7 django==3.2.8
pytest-asyncio==0.15.1 pytest-asyncio==0.16.0
pytest-bdd==4.1.0 pytest-bdd==4.1.0
pytest-cov==3.0.0 pytest-cov==3.0.0
pytest-django==4.4.0 pytest-django==4.4.0
pytest-flakes==4.0.3 pytest-flakes==4.0.4
pytest-html==3.1.1 pytest-html==3.1.1
pytest-mock==3.6.1 pytest-mock==3.6.1
pytest-rerunfailures==10.2 pytest-rerunfailures==10.2

View File

@ -7,6 +7,7 @@ from typing import Dict
import _pytest._code import _pytest._code
import pytest import pytest
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.main import Session
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
@ -294,7 +295,7 @@ class TestFunction:
from _pytest.fixtures import FixtureManager from _pytest.fixtures import FixtureManager
config = pytester.parseconfigure() config = pytester.parseconfigure()
session = pytester.Session.from_config(config) session = Session.from_config(config)
session._fixturemanager = FixtureManager(session) session._fixturemanager = FixtureManager(session)
return pytest.Function.from_parent(parent=session, **kwargs) return pytest.Function.from_parent(parent=session, **kwargs)
@ -1154,8 +1155,8 @@ class TestReportInfo:
def test_func_reportinfo(self, pytester: Pytester) -> None: def test_func_reportinfo(self, pytester: Pytester) -> None:
item = pytester.getitem("def test_func(): pass") item = pytester.getitem("def test_func(): pass")
fspath, lineno, modpath = item.reportinfo() path, lineno, modpath = item.reportinfo()
assert str(fspath) == str(item.path) assert os.fspath(path) == str(item.path)
assert lineno == 0 assert lineno == 0
assert modpath == "test_func" assert modpath == "test_func"
@ -1169,8 +1170,8 @@ class TestReportInfo:
) )
classcol = pytester.collect_by_name(modcol, "TestClass") classcol = pytester.collect_by_name(modcol, "TestClass")
assert isinstance(classcol, Class) assert isinstance(classcol, Class)
fspath, lineno, msg = classcol.reportinfo() path, lineno, msg = classcol.reportinfo()
assert str(fspath) == str(modcol.path) assert os.fspath(path) == str(modcol.path)
assert lineno == 1 assert lineno == 1
assert msg == "TestClass" assert msg == "TestClass"
@ -1194,7 +1195,7 @@ class TestReportInfo:
assert isinstance(classcol, Class) assert isinstance(classcol, Class)
instance = list(classcol.collect())[0] instance = list(classcol.collect())[0]
assert isinstance(instance, Instance) assert isinstance(instance, Instance)
fspath, lineno, msg = instance.reportinfo() path, lineno, msg = instance.reportinfo()
def test_customized_python_discovery(pytester: Pytester) -> None: def test_customized_python_discovery(pytester: Pytester) -> None:

View File

@ -19,7 +19,7 @@ class TestOEJSKITSpecials:
return MyCollector.from_parent(collector, name=name) return MyCollector.from_parent(collector, name=name)
class MyCollector(pytest.Collector): class MyCollector(pytest.Collector):
def reportinfo(self): def reportinfo(self):
return self.fspath, 3, "xyz" return self.path, 3, "xyz"
""" """
) )
modcol = pytester.getmodulecol( modcol = pytester.getmodulecol(
@ -52,7 +52,7 @@ class TestOEJSKITSpecials:
return MyCollector.from_parent(collector, name=name) return MyCollector.from_parent(collector, name=name)
class MyCollector(pytest.Collector): class MyCollector(pytest.Collector):
def reportinfo(self): def reportinfo(self):
return self.fspath, 3, "xyz" return self.path, 3, "xyz"
""" """
) )
modcol = pytester.getmodulecol( modcol = pytester.getmodulecol(

View File

@ -13,6 +13,7 @@ import pytest
from _pytest import outcomes from _pytest import outcomes
from _pytest.assertion import truncate from _pytest.assertion import truncate
from _pytest.assertion import util from _pytest.assertion import util
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
@ -448,6 +449,25 @@ class TestAssert_reprcompare:
assert verbose_expl is not None assert verbose_expl is not None
assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip()) assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip())
def test_iterable_full_diff_ci(
self, monkeypatch: MonkeyPatch, pytester: Pytester
) -> None:
pytester.makepyfile(
r"""
def test_full_diff():
left = [0, 1]
right = [0, 2]
assert left == right
"""
)
monkeypatch.setenv("CI", "true")
result = pytester.runpytest()
result.stdout.fnmatch_lines(["E Full diff:"])
monkeypatch.delenv("CI", raising=False)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["E Use -v to get the full diff"])
def test_list_different_lengths(self) -> None: def test_list_different_lengths(self) -> None:
expl = callequal([0, 1], [0, 1, 2]) expl = callequal([0, 1], [0, 1, 2])
assert expl is not None assert expl is not None

View File

@ -111,6 +111,28 @@ class TestAssertionRewrite:
assert imp.col_offset == 0 assert imp.col_offset == 0
assert isinstance(m.body[3], ast.Expr) assert isinstance(m.body[3], ast.Expr)
def test_location_is_set(self) -> None:
s = textwrap.dedent(
"""
assert False, (
"Ouch"
)
"""
)
m = rewrite(s)
for node in m.body:
if isinstance(node, ast.Import):
continue
for n in [node, *ast.iter_child_nodes(node)]:
assert n.lineno == 3
assert n.col_offset == 0
if sys.version_info >= (3, 8):
assert n.end_lineno == 6
assert n.end_col_offset == 3
def test_dont_rewrite(self) -> None: def test_dont_rewrite(self) -> None:
s = """'PYTEST_DONT_REWRITE'\nassert 14""" s = """'PYTEST_DONT_REWRITE'\nassert 14"""
m = rewrite(s) m = rewrite(s)
@ -773,6 +795,35 @@ class TestRewriteOnImport:
) )
assert pytester.runpytest().ret == ExitCode.NO_TESTS_COLLECTED assert pytester.runpytest().ret == ExitCode.NO_TESTS_COLLECTED
@pytest.mark.skipif(
sys.version_info < (3, 9),
reason="importlib.resources.files was introduced in 3.9",
)
def test_load_resource_via_files_with_rewrite(self, pytester: Pytester) -> None:
example = pytester.path.joinpath("demo") / "example"
init = pytester.path.joinpath("demo") / "__init__.py"
pytester.makepyfile(
**{
"demo/__init__.py": """
from importlib.resources import files
def load():
return files(__name__)
""",
"test_load": f"""
pytest_plugins = ["demo"]
def test_load():
from demo import load
found = {{str(i) for i in load().iterdir() if i.name != "__pycache__"}}
assert found == {{{str(example)!r}, {str(init)!r}}}
""",
}
)
example.mkdir()
assert pytester.runpytest("-vv").ret == ExitCode.OK
def test_readonly(self, pytester: Pytester) -> None: def test_readonly(self, pytester: Pytester) -> None:
sub = pytester.mkdir("testing") sub = pytester.mkdir("testing")
sub.joinpath("test_readonly.py").write_bytes( sub.joinpath("test_readonly.py").write_bytes(
@ -1630,7 +1681,7 @@ def test_try_makedirs(monkeypatch, tmp_path: Path) -> None:
# monkeypatch to simulate all error situations # monkeypatch to simulate all error situations
def fake_mkdir(p, exist_ok=False, *, exc): def fake_mkdir(p, exist_ok=False, *, exc):
assert isinstance(p, str) assert isinstance(p, Path)
raise exc raise exc
monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=FileNotFoundError())) monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=FileNotFoundError()))

View File

@ -1210,6 +1210,17 @@ def test_gitignore(pytester: Pytester) -> None:
assert gitignore_path.read_text(encoding="UTF-8") == "custom" assert gitignore_path.read_text(encoding="UTF-8") == "custom"
def test_preserve_keys_order(pytester: Pytester) -> None:
"""Ensure keys order is preserved when saving dicts (#9205)."""
from _pytest.cacheprovider import Cache
config = pytester.parseconfig()
cache = Cache.for_config(config, _ispytest=True)
cache.set("foo", {"z": 1, "b": 2, "a": 3, "d": 10})
read_back = cache.get("foo", None)
assert list(read_back.items()) == [("z", 1), ("b", 2), ("a", 3), ("d", 10)]
def test_does_not_create_boilerplate_in_existing_dirs(pytester: Pytester) -> None: def test_does_not_create_boilerplate_in_existing_dirs(pytester: Pytester) -> None:
from _pytest.cacheprovider import Cache from _pytest.cacheprovider import Cache

View File

@ -1379,8 +1379,7 @@ def test_capturing_and_logging_fundamentals(pytester: Pytester, method: str) ->
# here we check a fundamental feature # here we check a fundamental feature
p = pytester.makepyfile( p = pytester.makepyfile(
""" """
import sys, os import sys, os, logging
import py, logging
from _pytest import capture from _pytest import capture
cap = capture.MultiCapture( cap = capture.MultiCapture(
in_=None, in_=None,

View File

@ -793,7 +793,7 @@ def test_matchnodes_two_collections_same_file(pytester: Pytester) -> None:
res.stdout.fnmatch_lines(["*1 passed*"]) res.stdout.fnmatch_lines(["*1 passed*"])
class TestNodekeywords: class TestNodeKeywords:
def test_no_under(self, pytester: Pytester) -> None: def test_no_under(self, pytester: Pytester) -> None:
modcol = pytester.getmodulecol( modcol = pytester.getmodulecol(
""" """
@ -859,6 +859,24 @@ class TestNodekeywords:
reprec = pytester.inline_run("-k " + expression) reprec = pytester.inline_run("-k " + expression)
reprec.assertoutcome(passed=num_matching_tests, failed=0) reprec.assertoutcome(passed=num_matching_tests, failed=0)
def test_duplicates_handled_correctly(self, pytester: Pytester) -> None:
item = pytester.getitem(
"""
import pytest
pytestmark = pytest.mark.kw
class TestClass:
pytestmark = pytest.mark.kw
def test_method(self): pass
test_method.kw = 'method'
""",
"test_method",
)
assert item.parent is not None and item.parent.parent is not None
item.parent.parent.keywords["kw"] = "class"
assert item.keywords["kw"] == "method"
assert len(item.keywords) == len(set(item.keywords))
COLLECTION_ERROR_PY_FILES = dict( COLLECTION_ERROR_PY_FILES = dict(
test_01_failure=""" test_01_failure="""

View File

@ -595,7 +595,7 @@ class TestConfigAPI:
def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None: def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None:
somepath = tmp_path.joinpath("x", "y", "z") somepath = tmp_path.joinpath("x", "y", "z")
p = tmp_path.joinpath("conftest.py") p = tmp_path.joinpath("conftest.py")
p.write_text(f"mylist = {['.', os.fspath(somepath)]}") p.write_text(f"mylist = {['.', str(somepath)]}")
config = pytester.parseconfigure(p) config = pytester.parseconfigure(p)
assert ( assert (
config._getconftest_pathlist("notexist", path=tmp_path, rootpath=tmp_path) config._getconftest_pathlist("notexist", path=tmp_path, rootpath=tmp_path)
@ -2042,11 +2042,25 @@ def test_parse_warning_filter(
assert parse_warning_filter(arg, escape=escape) == expected assert parse_warning_filter(arg, escape=escape) == expected
@pytest.mark.parametrize("arg", [":" * 5, "::::-1", "::::not-a-number"]) @pytest.mark.parametrize(
"arg",
[
# Too much parts.
":" * 5,
# Invalid action.
"FOO::",
# ImportError when importing the warning class.
"::test_parse_warning_filter_failure.NonExistentClass::",
# Class is not a Warning subclass.
"::list::",
# Negative line number.
"::::-1",
# Not a line number.
"::::not-a-number",
],
)
def test_parse_warning_filter_failure(arg: str) -> None: def test_parse_warning_filter_failure(arg: str) -> None:
import warnings with pytest.raises(pytest.UsageError):
with pytest.raises(warnings._OptionError):
parse_warning_filter(arg, escape=True) parse_warning_filter(arg, escape=True)

View File

@ -934,7 +934,7 @@ class TestDebuggingBreakpoints:
from _pytest.debugging import pytestPDB from _pytest.debugging import pytestPDB
def pytest_configure(config): def pytest_configure(config):
config._cleanup.append(check_restored) config.add_cleanup(check_restored)
def check_restored(): def check_restored():
assert sys.breakpointhook == sys.__breakpointhook__ assert sys.breakpointhook == sys.__breakpointhook__
@ -983,7 +983,7 @@ class TestDebuggingBreakpoints:
os.environ['PYTHONBREAKPOINT'] = '_pytest._CustomDebugger.set_trace' os.environ['PYTHONBREAKPOINT'] = '_pytest._CustomDebugger.set_trace'
def pytest_configure(config): def pytest_configure(config):
config._cleanup.append(check_restored) config.add_cleanup(check_restored)
def check_restored(): def check_restored():
assert sys.breakpointhook == sys.__breakpointhook__ assert sys.breakpointhook == sys.__breakpointhook__

View File

@ -105,7 +105,7 @@ def test_hookvalidation_optional(pytester: Pytester) -> None:
def test_traceconfig(pytester: Pytester) -> None: def test_traceconfig(pytester: Pytester) -> None:
result = pytester.runpytest("--traceconfig") result = pytester.runpytest("--traceconfig")
result.stdout.fnmatch_lines(["*using*pytest*py*", "*active plugins*"]) result.stdout.fnmatch_lines(["*using*pytest*", "*active plugins*"])
def test_debug(pytester: Pytester) -> None: def test_debug(pytester: Pytester) -> None:

View File

@ -15,9 +15,8 @@ from _pytest.pytester import Pytester
class TestMark: class TestMark:
@pytest.mark.parametrize("attr", ["mark", "param"]) @pytest.mark.parametrize("attr", ["mark", "param"])
@pytest.mark.parametrize("modulename", ["py.test", "pytest"]) def test_pytest_exists_in_namespace_all(self, attr: str) -> None:
def test_pytest_exists_in_namespace_all(self, attr: str, modulename: str) -> None: module = sys.modules["pytest"]
module = sys.modules[modulename]
assert attr in module.__all__ # type: ignore assert attr in module.__all__ # type: ignore
def test_pytest_mark_notcallable(self) -> None: def test_pytest_mark_notcallable(self) -> None:
@ -1112,7 +1111,7 @@ def test_pytest_param_id_allows_none_or_string(s) -> None:
assert pytest.param(id=s) assert pytest.param(id=s)
@pytest.mark.parametrize("expr", ("NOT internal_err", "NOT (internal_err)")) @pytest.mark.parametrize("expr", ("NOT internal_err", "NOT (internal_err)", "bogus="))
def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None: def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None:
foo = pytester.makepyfile( foo = pytester.makepyfile(
""" """

View File

@ -6,6 +6,7 @@ from typing import Type
import pytest import pytest
from _pytest import nodes from _pytest import nodes
from _pytest.compat import legacy_path from _pytest.compat import legacy_path
from _pytest.outcomes import OutcomeException
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
@ -40,6 +41,19 @@ def test_node_from_parent_disallowed_arguments() -> None:
nodes.Node.from_parent(None, config=None) # type: ignore[arg-type] nodes.Node.from_parent(None, config=None) # type: ignore[arg-type]
def test_node_direct_construction_deprecated() -> None:
with pytest.raises(
OutcomeException,
match=(
"Direct construction of _pytest.nodes.Node has been deprecated, please "
"use _pytest.nodes.Node.from_parent.\nSee "
"https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
" for more details."
),
):
nodes.Node(None, session=None) # type: ignore[arg-type]
def test_subclassing_both_item_and_collector_deprecated( def test_subclassing_both_item_and_collector_deprecated(
request, tmp_path: Path request, tmp_path: Path
) -> None: ) -> None:

View File

@ -161,12 +161,12 @@ class TestPaste:
def test_create_new_paste(self, pastebin, mocked_urlopen) -> None: def test_create_new_paste(self, pastebin, mocked_urlopen) -> None:
result = pastebin.create_new_paste(b"full-paste-contents") result = pastebin.create_new_paste(b"full-paste-contents")
assert result == "https://bpaste.net/show/3c0c6750bd" assert result == "https://bpa.st/show/3c0c6750bd"
assert len(mocked_urlopen) == 1 assert len(mocked_urlopen) == 1
url, data = mocked_urlopen[0] url, data = mocked_urlopen[0]
assert type(data) is bytes assert type(data) is bytes
lexer = "text" lexer = "text"
assert url == "https://bpaste.net" assert url == "https://bpa.st"
assert "lexer=%s" % lexer in data.decode() assert "lexer=%s" % lexer in data.decode()
assert "code=full-paste-contents" in data.decode() assert "code=full-paste-contents" in data.decode()
assert "expiry=1week" in data.decode() assert "expiry=1week" in data.decode()

View File

@ -121,7 +121,7 @@ class TestImportPath:
module_c.write_text( module_c.write_text(
dedent( dedent(
""" """
import py; import pluggy;
import otherdir.a import otherdir.a
value = otherdir.a.result value = otherdir.a.result
""" """
@ -131,7 +131,7 @@ class TestImportPath:
module_d.write_text( module_d.write_text(
dedent( dedent(
""" """
import py; import pluggy;
from otherdir import a from otherdir import a
value2 = a.result value2 = a.result
""" """

View File

@ -192,7 +192,7 @@ def make_holder():
def test_hookrecorder_basic(holder) -> None: def test_hookrecorder_basic(holder) -> None:
pm = PytestPluginManager() pm = PytestPluginManager()
pm.add_hookspecs(holder) pm.add_hookspecs(holder)
rec = HookRecorder(pm) rec = HookRecorder(pm, _ispytest=True)
pm.hook.pytest_xyz(arg=123) pm.hook.pytest_xyz(arg=123)
call = rec.popcall("pytest_xyz") call = rec.popcall("pytest_xyz")
assert call.arg == 123 assert call.arg == 123
@ -861,3 +861,17 @@ def test_pytester_assert_outcomes_warnings(pytester: Pytester) -> None:
) )
result = pytester.runpytest() result = pytester.runpytest()
result.assert_outcomes(passed=1, warnings=1) result.assert_outcomes(passed=1, warnings=1)
def test_pytester_outcomes_deselected(pytester: Pytester) -> None:
pytester.makepyfile(
"""
def test_one():
pass
def test_two():
pass
"""
)
result = pytester.runpytest("-k", "test_one")
result.assert_outcomes(passed=1, deselected=1)

View File

@ -263,7 +263,7 @@ class TestWarns:
with pytest.warns(RuntimeWarning): with pytest.warns(RuntimeWarning):
warnings.warn("user", UserWarning) warnings.warn("user", UserWarning)
excinfo.match( excinfo.match(
r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) was emitted. " r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) were emitted. "
r"The list of emitted warnings is: \[UserWarning\('user',?\)\]." r"The list of emitted warnings is: \[UserWarning\('user',?\)\]."
) )
@ -271,7 +271,7 @@ class TestWarns:
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
warnings.warn("runtime", RuntimeWarning) warnings.warn("runtime", RuntimeWarning)
excinfo.match( excinfo.match(
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) was emitted. " r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted. "
r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)\]." r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)\]."
) )
@ -279,7 +279,7 @@ class TestWarns:
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
pass pass
excinfo.match( excinfo.match(
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) was emitted. " r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted. "
r"The list of emitted warnings is: \[\]." r"The list of emitted warnings is: \[\]."
) )
@ -290,7 +290,7 @@ class TestWarns:
warnings.warn("import", ImportWarning) warnings.warn("import", ImportWarning)
message_template = ( message_template = (
"DID NOT WARN. No warnings of type {0} was emitted. " "DID NOT WARN. No warnings of type {0} were emitted. "
"The list of emitted warnings is: {1}." "The list of emitted warnings is: {1}."
) )
excinfo.match( excinfo.match(

View File

@ -12,7 +12,6 @@ from typing import List
from typing import Tuple from typing import Tuple
import pluggy import pluggy
import py
import _pytest.config import _pytest.config
import _pytest.terminal import _pytest.terminal
@ -800,12 +799,11 @@ class TestTerminalFunctional:
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*===== test session starts ====*", "*===== test session starts ====*",
"platform %s -- Python %s*pytest-%s*py-%s*pluggy-%s" "platform %s -- Python %s*pytest-%s**pluggy-%s"
% ( % (
sys.platform, sys.platform,
verinfo, verinfo,
pytest.__version__, pytest.__version__,
py.__version__,
pluggy.__version__, pluggy.__version__,
), ),
"*test_header_trailer_info.py .*", "*test_header_trailer_info.py .*",
@ -828,12 +826,11 @@ class TestTerminalFunctional:
result = pytester.runpytest("--no-header") result = pytester.runpytest("--no-header")
verinfo = ".".join(map(str, sys.version_info[:3])) verinfo = ".".join(map(str, sys.version_info[:3]))
result.stdout.no_fnmatch_line( result.stdout.no_fnmatch_line(
"platform %s -- Python %s*pytest-%s*py-%s*pluggy-%s" "platform %s -- Python %s*pytest-%s**pluggy-%s"
% ( % (
sys.platform, sys.platform,
verinfo, verinfo,
pytest.__version__, pytest.__version__,
py.__version__,
pluggy.__version__, pluggy.__version__,
) )
) )