Merge branch 'pytest-dev:main' into main

This commit is contained in:
Alexander King 2021-10-29 09:51:05 -04:00 committed by GitHub
commit ee61e4887f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 8413 additions and 1335 deletions

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -12,8 +12,12 @@ The newly-exported types are:
- ``pytest.ExceptionInfo`` for the :class:`ExceptionInfo <pytest.ExceptionInfo>` type returned from :func:`pytest.raises` and passed to various hooks.
- ``pytest.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.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.
Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -23,14 +23,13 @@ a full list of details. A few feature highlights:
called if the corresponding setup method succeeded.
- integrate tab-completion on command line options if you
have `argcomplete <https://pypi.org/project/argcomplete/>`_
configured.
have :pypi:`argcomplete` configured.
- allow boolean expression directly with skipif/xfail
if a "reason" is also specified.
- 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``.
- reporting: color the last line red or green depending if

View File

@ -1004,7 +1004,7 @@ Trivial/Internal Changes
- `#7264 <https://github.com/pytest-dev/pytest/issues/7264>`_: The dependency on the ``wcwidth`` package has been removed.
- `#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``.
@ -1795,7 +1795,7 @@ Removals
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
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.
- `#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``).
@ -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.
``pdb:pdb.Pdb`` with `pdb++`_), and its validation was improved.
.. _pdb++: https://pypi.org/project/pdbpp/
``pdb:pdb.Pdb`` with :pypi:`pdbpp`), and its validation was improved.
- `#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.
- `#4968 <https://github.com/pytest-dev/pytest/issues/4968>`_: The pdb ``quit`` command is handled properly when used after the ``debug`` command with `pdb++`_.
.. _pdb++: https://pypi.org/project/pdbpp/
- `#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`.
- `#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.
- `#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.
@ -4865,8 +4861,7 @@ Features
markers. Also, a ``caplog`` fixture is available that enables users to test
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
introduced by merging the popular `pytest-catchlog
<https://pypi.org/project/pytest-catchlog/>`_ plugin, thanks to `Thomas Hisch
introduced by merging the popular :pypi:`pytest-catchlog` plugin, thanks to `Thomas Hisch
<https://github.com/thisch>`_. Be advised that during the merging the
backward compatibility interface with the defunct ``pytest-capturelog`` has
been dropped. (`#2794 <https://github.com/pytest-dev/pytest/issues/2794>`_)
@ -4943,7 +4938,7 @@ Bug Fixes
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
<https://github.com/pytest-dev/pytest/issues/2641>`_)

View File

@ -17,6 +17,7 @@
# The short X.Y version.
import ast
import os
import shutil
import sys
from typing import List
from typing import TYPE_CHECKING
@ -38,6 +39,10 @@ autodoc_member_order = "bysource"
autodoc_typehints = "description"
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 -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@ -50,6 +55,7 @@ extensions = [
"pygments_pytest",
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.extlinks",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.viewcode",
@ -57,6 +63,13 @@ extensions = [
"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.
templates_path = ["_templates"]
@ -133,6 +146,11 @@ linkcheck_ignore = [
linkcheck_workers = 5
extlinks = {
"pypi": ("https://pypi.org/project/%s/", ""),
}
# -- Options for HTML output ---------------------------------------------------
sys.path.append(os.path.abspath("_themes"))
@ -350,6 +368,14 @@ intersphinx_mapping = {
"pluggy": ("https://pluggy.readthedocs.io/en/stable", None),
"python": ("https://docs.python.org/3", None),
"numpy": ("https://numpy.org/doc/stable", None),
"pip": ("https://pip.pypa.io/en/stable", None),
"tox": ("https://tox.wiki/en/stable", None),
"virtualenv": ("https://virtualenv.pypa.io/en/stable", None),
"django": (
"http://docs.djangoproject.com/en/stable",
"http://docs.djangoproject.com/en/stable/_objects",
),
"setuptools": ("https://setuptools.pypa.io/en/stable", None),
}

View File

@ -18,17 +18,40 @@ Deprecated Features
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
: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``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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>`
* :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_report_header(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_header>`
* :func:`pytest_report_collectionfinish(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_collectionfinish>`
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
* :func:`pytest_ignore_collect(fspath: pathlib.Path) <_pytest.hookspec.pytest_ignore_collect>` instead of ``path``
* :func:`pytest_collect_file(fspath: pathlib.Path) <_pytest.hookspec.pytest_collect_file>` instead of ``path``
* :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.
@ -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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.3
.. deprecated:: 7.0
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.
@ -86,7 +109,7 @@ scheduled for removal in pytest 7 (deprecated since pytest 2.4.0):
Raising ``unittest.SkipTest`` during collection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.3
.. deprecated:: 7.0
Raising :class:`unittest.SkipTest` to skip collection of tests during the
pytest collection phase is deprecated. Use :func:`pytest.skip` instead.

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="572" height="542">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="572" height="542">
<style>
text {
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"/>
</defs>
<text class="package">
<textPath href="#testp" startOffset="50%">tests</textPath>
<textPath xlink:href="#testp" startOffset="50%">tests</textPath>
</text>
<!-- subpackage -->
@ -47,7 +47,7 @@
<path d="M 56,271 A 130 130 0 0 1 316 271" id="subpackage"/>
</defs>
<text class="package">
<textPath href="#subpackage" startOffset="50%">subpackage</textPath>
<textPath xlink:href="#subpackage" startOffset="50%">subpackage</textPath>
</text>
<!-- test_subpackage.py -->
@ -57,7 +57,7 @@
<path d="M 106,311 A 80 80 0 0 1 266 311" id="testSubpackage"/>
</defs>
<text class="module">
<textPath href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath>
<textPath xlink:href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath>
</text>
<!-- innermost -->
<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"/>
</defs>
<text class="module">
<textPath href="#testTop" startOffset="50%">test_top.py</textPath>
<textPath xlink:href="#testTop" startOffset="50%">test_top.py</textPath>
</text>
<!-- innermost -->
<line x1="441" x2="441" y1="306" y2="236"/>

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

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

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="862" height="402">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="862" height="402">
<style>
text {
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"/>
</defs>
<text class="class">
<textPath href="#testClassWith" startOffset="50%">TestWithC1Request</textPath>
<textPath xlink:href="#testClassWith" startOffset="50%">TestWithC1Request</textPath>
</text>
<!-- TestWithoutC1Request -->
@ -67,7 +67,7 @@
<path d="M451,201 A 190 190 0 0 1 831 201" id="testClassWithout"/>
</defs>
<text class="class">
<textPath href="#testClassWithout" startOffset="50%">TestWithoutC1Request</textPath>
<textPath xlink:href="#testClassWithout" startOffset="50%">TestWithoutC1Request</textPath>
</text>
<rect class="autouse" width="862" height="40" x="1" y="181" />

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="862" height="502">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="862" height="502">
<style>
text {
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"/>
</defs>
<text class="class">
<textPath href="#testClassWith" startOffset="50%">TestWithAutouse</textPath>
<textPath xlink:href="#testClassWith" startOffset="50%">TestWithAutouse</textPath>
</text>
<mask id="autouseScope">
<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"/>
</defs>
<text class="class">
<textPath href="#testClassWithout" startOffset="50%">TestWithoutAutouse</textPath>
<textPath xlink:href="#testClassWithout" startOffset="50%">TestWithoutAutouse</textPath>
</text>
<!-- TestWithoutAutouse.test_req -->

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="262" height="537">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="262" height="537">
<style>
text {
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"/>
</defs>
<text class="class">
<textPath href="#testClass" startOffset="50%">TestClass</textPath>
<textPath xlink:href="#testClass" startOffset="50%">TestClass</textPath>
</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="562" height="532">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="562" height="532">
<style>
text {
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"/>
</defs>
<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>
<!-- TestOne -->
@ -61,7 +61,7 @@
<path d="M 51,266 A 90 90 0 0 1 231 266" id="testOne"/>
</defs>
<text class="class">
<textPath href="#testOne" startOffset="50%">TestOne</textPath>
<textPath xlink:href="#testOne" startOffset="50%">TestOne</textPath>
</text>
<!-- scope order number -->
<mask id="testOneOrderMask">
@ -95,7 +95,7 @@
<path d="M 331,266 A 90 90 0 0 1 511 266" id="testTwo"/>
</defs>
<text class="class">
<textPath href="#testTwo" startOffset="50%">TestTwo</textPath>
<textPath xlink:href="#testTwo" startOffset="50%">TestTwo</textPath>
</text>
<!-- scope order number -->
<mask id="testTwoOrderMask">

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -10,7 +10,6 @@ A basic example for specifying tests in Yaml files
--------------------------------------------------------------
.. _`pytest-yamlwsgi`: http://bitbucket.org/aafshar/pytest-yamlwsgi/src/tip/pytest_yamlwsgi.py
.. _`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:
@ -22,7 +21,7 @@ You can create a simple example file:
.. include:: nonpython/test_simple.yaml
: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:
.. code-block:: pytest

View File

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

View File

@ -183,9 +183,7 @@ together with the actual data, instead of listing them separately.
A quick port of "testscenarios"
------------------------------------
.. _`test scenarios`: https://pypi.org/project/testscenarios/
Here is a quick port to run tests configured with `test scenarios`_,
Here is a quick port to run tests configured with :pypi:`testscenarios`,
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
:py:func:`Metafunc.parametrize`:

View File

@ -223,7 +223,7 @@ the command line arguments before they get processed:
num = max(multiprocessing.cpu_count() / 2, 1)
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
of subprocesses close to your CPU. Running in an empty
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.
``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
test got stuck if necessary:
by process monitoring utilities or libraries like :pypi:`psutil` to discover which test got stuck if necessary:
.. code-block:: python

View File

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

View File

@ -8,27 +8,47 @@ Install package with pip
-------------------------------------------------
For development, we recommend you use :mod:`venv` for virtual environments and
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.
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
pip install -e .
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`:
.. _`Python test discovery`:
@ -68,7 +88,8 @@ to keep tests separate from actual application code (often a good idea):
.. code-block:: text
setup.py
pyproject.toml
setup.cfg
mypkg/
__init__.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 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
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
setup.py
pyproject.toml
setup.cfg
mypkg/
...
tests/
@ -130,7 +152,8 @@ sub-directory of your root:
.. code-block:: text
setup.py
pyproject.toml
setup.cfg
src/
mypkg/
__init__.py
@ -167,7 +190,8 @@ want to distribute them along with your application:
.. code-block:: text
setup.py
pyproject.toml
setup.cfg
mypkg/
__init__.py
app.py
@ -191,11 +215,11 @@ Note that this layout also works in conjunction with the ``src`` layout mentione
.. 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
presence of ``__init__.py`` files. If you use one of the
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
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.
.. _`virtualenv`: https://pypi.org/project/virtualenv/
.. _`buildout`: http://www.buildout.org/en/latest/
.. _pip: https://pypi.org/project/pip/
.. _`use tox`:
tox
------
---
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
virtualenv test automation tool and its `pytest support
<https://tox.readthedocs.io/en/latest/example/pytest.html>`_.
package passes all tests you may want to look into :doc:`tox <tox:index>`, the
virtualenv test automation tool and its :doc:`pytest support <tox:example/pytest>`.
tox helps you to setup virtualenv environments with pre-defined
dependencies and then executing a pre-configured test command with
options. It will run tests against the installed package and not
against your source code checkout, helping to detect packaging
glitches.
Do not run via setuptools
-------------------------
Integration with setuptools is **not recommended**,
i.e. you should not be using ``python setup.py test`` or ``pytest-runner``,
and may stop working in the future.
This is deprecated since it depends on deprecated features of setuptools
and relies on features that break security mechanisms in pip.
For example 'setup_requires' and 'tests_require' bypass ``pip --require-hashes``.
For more information and migration instructions,
see the `pytest-runner notice <https://github.com/pytest-dev/pytest-runner#deprecation-notice>`_.
See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>`_.
setuptools intends to
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_.

View File

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

View File

@ -268,7 +268,7 @@ argument ``match`` to assert that the exception matches a text or regex::
... warnings.warn("this is not here", UserWarning)
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:

View File

@ -248,8 +248,8 @@ through ``add_color_level()``. Example:
Release notes
^^^^^^^^^^^^^
This feature was introduced as a drop-in replacement for the `pytest-catchlog
<https://pypi.org/project/pytest-catchlog/>`_ plugin and they conflict
This feature was introduced as a drop-in replacement for the
:pypi:`pytest-catchlog` plugin and they conflict
with each other. The backward compatibility API with ``pytest-capturelog``
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

View File

@ -20,40 +20,38 @@ there is no need to activate it.
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
for `django`_ apps, using pytest integration.
* `pytest-twisted <https://pypi.org/project/pytest-twisted/>`_: write tests
* :pypi:`pytest-twisted`: write tests
for `twisted <https://twistedmatrix.com/>`_ apps, starting a reactor and
processing deferreds from test functions.
* `pytest-cov <https://pypi.org/project/pytest-cov/>`__:
* :pypi:`pytest-cov`:
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
mode which allows to survive segmentation faults, to run in
looponfailing mode, automatically re-running failing tests
on file changes.
* `pytest-instafail <https://pypi.org/project/pytest-instafail/>`_:
* :pypi:`pytest-instafail`:
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.
* `pytest-timeout <https://pypi.org/project/pytest-timeout/>`_:
* :pypi:`pytest-timeout`:
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.
* `pytest-flakes <https://pypi.org/project/pytest-flakes/>`_:
* :pypi:`pytest-flakes`:
check source code with pyflakes.
* `oejskit <https://pypi.org/project/oejskit/>`_:
* :pypi:`oejskit`:
a plugin to run javascript unittests in live browsers.
To see a complete list of all plugins with their latest testing

View File

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

View File

@ -120,7 +120,7 @@ The option receives a ``name`` parameter, which can be:
* A full module dotted name, for example ``myproject.plugins``. This dotted name must be importable.
* 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

View File

@ -115,8 +115,6 @@ Here is how you might run it::
Writing your own plugin
-----------------------
.. _`setuptools`: https://pypi.org/project/setuptools/
If you want to write a plugin, there are many real-life examples
you can copy from:
@ -150,7 +148,7 @@ Making your plugin installable by others
If you want to make your plugin externally available, you
may define a so-called entry point for your distribution so
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
plugins and you can thus make your plugin available by defining
it in your setuptools-invocation:

View File

@ -19,7 +19,7 @@ scale to support complex functional testing for applications and libraries.
**Pythons**: ``pytest`` requires: Python 3.6, 3.7, 3.8, 3.9, or PyPy3.
**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>`_

View File

@ -120,7 +120,7 @@ all parameters marked as a fixture.
.. 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.
.. code-block:: python

View File

@ -116,7 +116,7 @@ fixture (``inner``) from a scope it wasn't defined in:
From the tests' perspectives, they have no problem seeing each of the fixtures
they're dependent on:
.. image:: /example/fixtures/test_fixtures_request_different_scope.svg
.. image:: /example/fixtures/test_fixtures_request_different_scope.*
:align: center
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:
.. image:: /example/fixtures/fixture_availability.svg
.. image:: /example/fixtures/fixture_availability.*
:align: center
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:
.. image:: /example/fixtures/test_fixtures_order_scope.svg
.. image:: /example/fixtures/test_fixtures_order_scope.*
:align: center
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:
.. image:: /example/fixtures/test_fixtures_order_dependencies.svg
.. image:: /example/fixtures/test_fixtures_order_dependencies.*
:align: center
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:
.. image:: /example/fixtures/test_fixtures_order_dependencies_flat.svg
.. image:: /example/fixtures/test_fixtures_order_dependencies_flat.*
:align: center
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:
.. image:: /example/fixtures/test_fixtures_order_dependencies_unclear.svg
.. image:: /example/fixtures/test_fixtures_order_dependencies_unclear.*
:align: center
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:
.. image:: /example/fixtures/test_fixtures_order_autouse.svg
.. image:: /example/fixtures/test_fixtures_order_autouse.*
:align: center
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
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
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:
.. image:: /example/fixtures/test_fixtures_order_autouse_temp_effects.svg
.. image:: /example/fixtures/test_fixtures_order_autouse_temp_effects.*
:align: center
For ``test_req`` and ``test_no_req`` inside ``TestClassWithAutouse``, ``c3``

File diff suppressed because it is too large Load Diff

View File

@ -558,14 +558,17 @@ To use it, include in your topmost ``conftest.py`` file:
.. autoclass:: pytest.Pytester()
:members:
.. autoclass:: _pytest.pytester.RunResult()
.. autoclass:: pytest.RunResult()
:members:
.. autoclass:: _pytest.pytester.LineMatcher()
.. autoclass:: pytest.LineMatcher()
:members:
:special-members: __str__
.. autoclass:: _pytest.pytester.HookRecorder()
.. autoclass:: pytest.HookRecorder()
:members:
.. autoclass:: pytest.RecordedHookCall()
:members:
.. 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
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
Sets the console output style while running tests:
* ``classic``: classic pytest output.

View File

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

View File

@ -54,8 +54,16 @@ def prepare_release_pr(
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:
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:
print(f"{Fore.RED}{e}")
raise SystemExit(1)
@ -80,9 +88,24 @@ def prepare_release_pr(
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
# 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))
run(
cmdline,
@ -107,7 +130,9 @@ def prepare_release_pr(
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")
valid_versions = []
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()
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:
return f"{last_version[0]+1}.0.0{prerelease}"
elif is_feature_release:

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

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

View File

@ -10,7 +10,7 @@ from colorama import Fore
from colorama import init
def announce(version):
def announce(version, template_name):
"""Generates a new release announcement entry in the docs."""
# Get our list of authors
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]")}
template_name = (
"release.minor.rst" if version.endswith(".0") else "release.patch.rst"
)
template_text = (
Path(__file__).parent.joinpath(template_name).read_text(encoding="UTF-8")
)
@ -81,9 +78,9 @@ def check_links():
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."""
announce(version)
announce(version, template_name)
regen(version)
changelog(version, write_out=True)
fix_formatting()
@ -108,9 +105,16 @@ def main():
init(autoreset=True)
parser = argparse.ArgumentParser()
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)
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__":

View File

@ -1,10 +1,14 @@
import datetime
import pathlib
import re
from textwrap import dedent
from textwrap import indent
import packaging.version
import requests
import tabulate
import wcwidth
from tqdm import tqdm
FILE_HEAD = r"""
.. _plugin-list:
@ -14,6 +18,11 @@ Plugin List
PyPI projects that match "pytest-\*" are considered plugins and are listed
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 :: 1 - Planning",
@ -42,10 +51,15 @@ def escape_rst(text: str) -> str:
def iter_plugins():
regex = r">([\d\w-]*)</a>"
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]
if not name.startswith("pytest-"):
continue
response = requests.get(f"https://pypi.org/pypi/{name}/json")
if response.status_code == 404:
# 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")
break
name = f'`{info["name"]} <{info["project_url"]}>`_'
name = f':pypi:`{info["name"]}`'
summary = escape_rst(info["summary"].replace("\n", ""))
yield {
"name": name,
"summary": summary,
"summary": summary.strip(),
"last release": last_release,
"status": status,
"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():
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:
f.write(FILE_HEAD)
f.write(f"This list contains {len(plugins)} plugins.\n\n")
f.write(plugin_table)
f.write("\n")
f.write(".. only:: not latex\n\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__":

View File

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

View File

@ -19,6 +19,7 @@ from typing import Callable
from typing import Dict
from typing import IO
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Sequence
@ -63,7 +64,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
except ValueError:
self.fnpats = ["test_*.py", "*_test.py"]
self.session: Optional[Session] = None
self._rewritten_names: Set[str] = set()
self._rewritten_names: Dict[str, Path] = {}
self._must_rewrite: Set[str] = set()
# 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)
@ -133,7 +134,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
fn = Path(module.__spec__.origin)
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 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:
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(
fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType
@ -333,7 +342,7 @@ else:
try:
_write_pyc_fp(fp, source_stat, co)
os.rename(proc_pyc, os.fspath(pyc))
os.rename(proc_pyc, pyc)
except OSError as e:
state.trace(f"error writing pyc file at {pyc}: {e}")
# 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]:
"""Read and rewrite *fn* and return the code object."""
fn_ = os.fspath(fn)
stat = os.stat(fn_)
with open(fn_, "rb") as f:
source = f.read()
tree = ast.parse(source, filename=fn_)
rewrite_asserts(tree, source, fn_, config)
co = compile(tree, fn_, "exec", dont_inherit=True)
stat = os.stat(fn)
source = fn.read_bytes()
strfn = str(fn)
tree = ast.parse(source, filename=strfn)
rewrite_asserts(tree, source, strfn, config)
co = compile(tree, strfn, "exec", dont_inherit=True)
return stat, co
@ -365,14 +373,14 @@ def _read_pyc(
Return rewritten code if successful or None if not.
"""
try:
fp = open(os.fspath(pyc), "rb")
fp = open(pyc, "rb")
except OSError:
return None
with fp:
# https://www.python.org/dev/peps/pep-0552/
has_flags = sys.version_info >= (3, 7)
try:
stat_result = os.stat(os.fspath(source))
stat_result = os.stat(source)
mtime = int(stat_result.st_mtime)
size = stat_result.st_size
data = fp.read(16 if has_flags else 12)
@ -539,19 +547,11 @@ BINOP_MAP = {
}
def set_location(node, lineno, col_offset):
"""Set node location information recursively."""
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):
_fix(child, lineno, col_offset)
_fix(node, lineno, col_offset)
return node
def traverse_node(node: ast.AST) -> Iterator[ast.AST]:
"""Recursively yield node and all its children in depth-first order."""
yield node
for child in ast.iter_child_nodes(node):
yield from traverse_node(child)
@functools.lru_cache(maxsize=1)
@ -862,7 +862,7 @@ class AssertionRewriter(ast.NodeVisitor):
"assertion is always true, perhaps remove parentheses?"
),
category=None,
filename=os.fspath(self.module_path),
filename=self.module_path,
lineno=assert_.lineno,
)
@ -954,9 +954,10 @@ class AssertionRewriter(ast.NodeVisitor):
variables = [ast.Name(name, ast.Store()) for name in self.variables]
clear = ast.Assign(variables, ast.NameConstant(None))
self.statements.append(clear)
# Fix line numbers.
# Fix locations (line numbers/column offsets).
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
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.
"""
try:
os.makedirs(os.fspath(cache_dir), exist_ok=True)
os.makedirs(cache_dir, exist_ok=True)
except (FileNotFoundError, NotADirectoryError, FileExistsError):
# One of the path components was not a directory:
# - we're in a zip file

View File

@ -3,10 +3,10 @@
Current default behaviour is to truncate assertion explanations at
~8 terminal lines, unless running in "-vv" mode or running on CI.
"""
import os
from typing import List
from typing import Optional
from _pytest.assertion import util
from _pytest.nodes import Item
@ -27,13 +27,7 @@ def truncate_if_required(
def _should_truncate_item(item: Item) -> bool:
"""Whether or not this test item is eligible for truncation."""
verbose = item.config.option.verbose
return verbose < 2 and not _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)
return verbose < 2 and not util.running_on_ci()
def _truncate_explanation(

View File

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

View File

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

View File

@ -13,9 +13,11 @@ import types
import warnings
from functools import lru_cache
from pathlib import Path
from textwrap import dedent
from types import TracebackType
from typing import Any
from typing import Callable
from typing import cast
from typing import Dict
from typing import Generator
from typing import IO
@ -1612,17 +1614,54 @@ def parse_warning_filter(
) -> Tuple[str, str, Type[Warning], str, int]:
"""Parse a warnings filter string.
This is copied from warnings._setoption, but does not apply the filter,
only parses it, and makes the escaping optional.
This is copied from warnings._setoption with the following changes:
* 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(":")
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:
parts.append("")
action_, message, category_, module, lineno_ = (s.strip() for s in parts)
action: str = warnings._getaction(action_) # type: ignore[attr-defined]
category: Type[Warning] = warnings._getcategory(category_) # type: ignore[attr-defined]
try:
action: str = warnings._getaction(action_) # type: ignore[attr-defined]
except warnings._OptionError as e:
raise UsageError(error_template.format(error=str(e)))
try:
category: Type[Warning] = _resolve_warning_category(category_)
except Exception:
exc_info = ExceptionInfo.from_current()
exception_text = exc_info.getrepr(style="native")
raise UsageError(error_template.format(error=exception_text))
if message and escape:
message = re.escape(message)
if module and escape:
@ -1631,14 +1670,38 @@ def parse_warning_filter(
try:
lineno = int(lineno_)
if lineno < 0:
raise ValueError
except (ValueError, OverflowError) as e:
raise warnings._OptionError(f"invalid lineno {lineno_!r}") from e
raise ValueError("number is negative")
except ValueError as e:
raise UsageError(
error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
)
else:
lineno = 0
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(
config_filters: Iterable[str], cmdline_filters: Iterable[str]
) -> None:

View File

@ -185,7 +185,7 @@ class Parser:
* ``paths``: a list of :class:`pathlib.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.
Defaults to ``string`` if ``None`` or not passed.

View File

@ -4,8 +4,9 @@ from pathlib import Path
from typing import Optional
from ..compat import LEGACY_PATH
from ..compat import legacy_path
from ..deprecated import HOOK_LEGACY_PATH_ARG
from _pytest.nodes import _imply_path
from _pytest.nodes import _check_path
# hookname: (Path, LEGACY_PATH)
imply_paths_hooks = {
@ -52,7 +53,15 @@ class PathAwareHookProxy:
),
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[fspath_var] = fspath_value
return hook(**kw)

View File

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

View File

@ -101,6 +101,14 @@ HOOK_LEGACY_PATH_ARG = UnformattedWarning(
"#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(
"Passing None to catch any warning has been deprecated, pass no arguments instead:\n"
" Replace pytest.warns(None) by simply pytest.warns()."

View File

@ -1,6 +1,7 @@
"""Discover and run doctests in modules and test files."""
import bdb
import inspect
import os
import platform
import sys
import traceback
@ -28,7 +29,6 @@ from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprFileLocation
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest.compat import legacy_path
from _pytest.compat import safe_getattr
from _pytest.config import Config
from _pytest.config.argparsing import Parser
@ -371,9 +371,9 @@ class DoctestItem(pytest.Item):
reprlocation_lines.append((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
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]:

View File

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

View File

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

View File

@ -272,12 +272,13 @@ def pytest_ignore_collect(
Stops at first non-None result, see :ref:`firstresult`.
: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.
.. versionchanged:: 6.3.0
.. versionchanged:: 7.0.0
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.
: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`
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`.
: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`
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 Path startpath: The starting dir.
:param LEGACY_PATH startdir: The starting dir.
:param LEGACY_PATH startdir: The starting dir (deprecated).
.. note::
@ -689,9 +693,10 @@ def pytest_report_header(
files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`.
.. versionchanged:: 6.3.0
.. versionchanged:: 7.0.0
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
:param pytest.Config config: The pytest config object.
:param Path startpath: The starting path.
:param LEGACY_PATH startdir: The starting dir.
:param Path startpath: 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.
.. note::
@ -720,9 +725,10 @@ def pytest_report_collectionfinish(
If you want to have your line(s) displayed first, use
:ref:`trylast=True <plugin-hookorder>`.
.. versionchanged:: 6.3.0
.. versionchanged:: 7.0.0
The ``startpath`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter.
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
"""

View File

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

View File

@ -539,6 +539,8 @@ MARK_GEN = MarkGenerator(_ispytest=True)
@final
class NodeKeywords(MutableMapping[str, Any]):
__slots__ = ("node", "parent", "_markers")
def __init__(self, node: "Node") -> None:
self.node = node
self.parent = node.parent
@ -555,21 +557,39 @@ class NodeKeywords(MutableMapping[str, Any]):
def __setitem__(self, key: str, value: Any) -> None:
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:
raise ValueError("cannot delete key in keywords dict")
def __iter__(self) -> Iterator[str]:
seen = self._seen()
return iter(seen)
def _seen(self) -> Set[str]:
seen = set(self._markers)
# Doesn't need to be fast.
yield from self._markers
if self.parent is not None:
seen.update(self.parent.keywords)
return seen
for keyword in self.parent.keywords:
# self._marks and self.parent.keywords can have duplicates.
if keyword not in self._markers:
yield keyword
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:
return f"<NodeKeywords for node {self.node}>"

View File

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

View File

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

View File

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

View File

@ -43,6 +43,7 @@ def assert_outcomes(
xpassed: int = 0,
xfailed: int = 0,
warnings: int = 0,
deselected: int = 0,
) -> None:
"""Assert that the specified outcomes appear with the respective
numbers (0 means it didn't occur) in the text output from a test run."""
@ -56,6 +57,7 @@ def assert_outcomes(
"xpassed": outcomes.get("xpassed", 0),
"xfailed": outcomes.get("xfailed", 0),
"warnings": outcomes.get("warnings", 0),
"deselected": outcomes.get("deselected", 0),
}
expected = {
"passed": passed,
@ -65,5 +67,6 @@ def assert_outcomes(
"xpassed": xpassed,
"xfailed": xfailed,
"warnings": warnings,
"deselected": deselected,
}
assert obtained == expected

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@ import sys
import types
import attr
import py
import pytest
from _pytest.compat import importlib_metadata
@ -515,28 +514,10 @@ class TestInvocationVariants:
assert result.ret == 0
def test_pydoc(self, pytester: Pytester) -> None:
for name in ("py.test", "pytest"):
result = pytester.runpython_c(f"import {name};help({name})")
assert result.ret == 0
s = result.stdout.str()
assert "MarkGenerator" in s
def test_import_star_py_dot_test(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
from py.test import *
#collect
#cmdline
#Item
# assert collect.Item is Item
# assert collect.Collector is Collector
main
skip
xfail
"""
)
result = pytester.runpython(p)
result = pytester.runpython_c("import pytest;help(pytest)")
assert result.ret == 0
s = result.stdout.str()
assert "MarkGenerator" in s
def test_import_star_pytest(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
@ -585,10 +566,6 @@ class TestInvocationVariants:
assert res.ret == 0
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:
with pytest.raises(
TypeError, match="expected to be a list of strings, got: '-h'"

View File

@ -1,6 +1,7 @@
import re
import sys
import warnings
from pathlib import Path
from unittest import mock
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)
# 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():
with pytest.warns(
@ -207,3 +215,16 @@ def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None:
"*Please use pytest_load_initial_conftests hook instead.*",
]
)
def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
mod = pytester.getmodulecol("")
with pytest.warns(
pytest.PytestDeprecationWarning,
match=re.escape("The (fspath: py.path.local) argument to File is deprecated."),
):
pytest.File.from_parent(
parent=mod.parent,
fspath=legacy_path("bla"),
)

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import pytest
from _pytest import outcomes
from _pytest.assertion import truncate
from _pytest.assertion import util
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester
@ -448,6 +449,25 @@ class TestAssert_reprcompare:
assert verbose_expl is not None
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:
expl = callequal([0, 1], [0, 1, 2])
assert expl is not None

View File

@ -111,6 +111,28 @@ class TestAssertionRewrite:
assert imp.col_offset == 0
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:
s = """'PYTEST_DONT_REWRITE'\nassert 14"""
m = rewrite(s)
@ -773,6 +795,35 @@ class TestRewriteOnImport:
)
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:
sub = pytester.mkdir("testing")
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
def fake_mkdir(p, exist_ok=False, *, exc):
assert isinstance(p, str)
assert isinstance(p, Path)
raise exc
monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=FileNotFoundError()))

View File

@ -1210,6 +1210,17 @@ def test_gitignore(pytester: Pytester) -> None:
assert gitignore_path.read_text(encoding="UTF-8") == "custom"
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:
from _pytest.cacheprovider import Cache

View File

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

View File

@ -793,7 +793,7 @@ def test_matchnodes_two_collections_same_file(pytester: Pytester) -> None:
res.stdout.fnmatch_lines(["*1 passed*"])
class TestNodekeywords:
class TestNodeKeywords:
def test_no_under(self, pytester: Pytester) -> None:
modcol = pytester.getmodulecol(
"""
@ -859,6 +859,24 @@ class TestNodekeywords:
reprec = pytester.inline_run("-k " + expression)
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(
test_01_failure="""

View File

@ -595,7 +595,7 @@ class TestConfigAPI:
def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None:
somepath = tmp_path.joinpath("x", "y", "z")
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)
assert (
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
@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:
import warnings
with pytest.raises(warnings._OptionError):
with pytest.raises(pytest.UsageError):
parse_warning_filter(arg, escape=True)

View File

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

View File

@ -105,7 +105,7 @@ def test_hookvalidation_optional(pytester: Pytester) -> None:
def test_traceconfig(pytester: Pytester) -> None:
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:

View File

@ -15,9 +15,8 @@ from _pytest.pytester import Pytester
class TestMark:
@pytest.mark.parametrize("attr", ["mark", "param"])
@pytest.mark.parametrize("modulename", ["py.test", "pytest"])
def test_pytest_exists_in_namespace_all(self, attr: str, modulename: str) -> None:
module = sys.modules[modulename]
def test_pytest_exists_in_namespace_all(self, attr: str) -> None:
module = sys.modules["pytest"]
assert attr in module.__all__ # type: ignore
def test_pytest_mark_notcallable(self) -> None:

View File

@ -6,6 +6,7 @@ from typing import Type
import pytest
from _pytest import nodes
from _pytest.compat import legacy_path
from _pytest.outcomes import OutcomeException
from _pytest.pytester import Pytester
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]
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(
request, tmp_path: Path
) -> None:

View File

@ -161,12 +161,12 @@ class TestPaste:
def test_create_new_paste(self, pastebin, mocked_urlopen) -> None:
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
url, data = mocked_urlopen[0]
assert type(data) is bytes
lexer = "text"
assert url == "https://bpaste.net"
assert url == "https://bpa.st"
assert "lexer=%s" % lexer in data.decode()
assert "code=full-paste-contents" in data.decode()
assert "expiry=1week" in data.decode()

View File

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

View File

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

View File

@ -263,7 +263,7 @@ class TestWarns:
with pytest.warns(RuntimeWarning):
warnings.warn("user", UserWarning)
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',?\)\]."
)
@ -271,7 +271,7 @@ class TestWarns:
with pytest.warns(UserWarning):
warnings.warn("runtime", RuntimeWarning)
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',?\)\]."
)
@ -279,7 +279,7 @@ class TestWarns:
with pytest.warns(UserWarning):
pass
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: \[\]."
)
@ -290,7 +290,7 @@ class TestWarns:
warnings.warn("import", ImportWarning)
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}."
)
excinfo.match(

View File

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