Merge branch 'main' of github.com:king-alexander/pytest
|
@ -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
|
||||||
|
|
|
@ -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/)
|
||||||
|
|
|
@ -7,6 +7,10 @@ python:
|
||||||
- method: pip
|
- method: pip
|
||||||
path: .
|
path: .
|
||||||
|
|
||||||
|
build:
|
||||||
|
apt_packages:
|
||||||
|
- inkscape
|
||||||
|
|
||||||
formats:
|
formats:
|
||||||
- epub
|
- epub
|
||||||
- pdf
|
- pdf
|
||||||
|
|
2
AUTHORS
|
@ -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
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
The PDF documentation’s list of plugins doesn’t run off the page anymore.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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).
|
|
@ -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.
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Included the module of the class in the error message about direct
|
||||||
|
node construction (without using ``from_parent``).
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed the URL used by ``--pastebin`` to use `bpa.st <http://bpa.st>`__.
|
|
@ -0,0 +1 @@
|
||||||
|
The end line number and end column offset are now properly set for rewritten assert statements.
|
|
@ -0,0 +1 @@
|
||||||
|
Support for the ``files`` API from ``importlib.resources`` within rewritten files.
|
|
@ -0,0 +1 @@
|
||||||
|
:meth:`pytest.Cache.set` now preserves key order when saving dicts.
|
|
@ -0,0 +1 @@
|
||||||
|
Remove incorrect docs about ``confcutdir`` being a configuration option: it can only be set through the ``--confcutdir`` command-line option.
|
|
@ -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
|
||||||
|
|
|
@ -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>`_)
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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`:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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>`_.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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>`_
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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``
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
|
@ -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__":
|
||||||
|
|
|
@ -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__":
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
|
||||||
if "lineno" in node._attributes:
|
|
||||||
node.lineno = lineno
|
|
||||||
if "col_offset" in node._attributes:
|
|
||||||
node.col_offset = col_offset
|
|
||||||
for child in ast.iter_child_nodes(node):
|
for child in ast.iter_child_nodes(node):
|
||||||
_fix(child, lineno, col_offset)
|
yield from traverse_node(child)
|
||||||
|
|
||||||
_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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
try:
|
||||||
action: str = warnings._getaction(action_) # type: ignore[attr-defined]
|
action: str = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||||
category: Type[Warning] = warnings._getcategory(category_) # 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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()."
|
||||||
|
|
|
@ -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]:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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}>"
|
||||||
|
|
|
@ -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 _imply_path(
|
def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
|
||||||
path: Optional[Path], fspath: Optional[LEGACY_PATH]
|
|
||||||
) -> Tuple[Path, LEGACY_PATH]:
|
|
||||||
if path is not None:
|
|
||||||
if fspath is not None:
|
|
||||||
if Path(fspath) != path:
|
if Path(fspath) != path:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Path({fspath!r}) != {path!r}\n"
|
f"Path({fspath!r}) != {path!r}\n"
|
||||||
"if both path and fspath are given they need to be equal"
|
"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
|
|
||||||
|
|
||||||
|
|
||||||
|
def _imply_path(
|
||||||
|
node_type: Type["Node"],
|
||||||
|
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 fspath is not None:
|
||||||
|
_check_path(path, fspath)
|
||||||
|
return path
|
||||||
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])
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,29 +514,11 @@ 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
|
assert result.ret == 0
|
||||||
s = result.stdout.str()
|
s = result.stdout.str()
|
||||||
assert "MarkGenerator" in s
|
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
|
|
||||||
|
|
||||||
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'"
|
||||||
|
|
|
@ -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"),
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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="""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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__
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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__,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|