Merge branch 'main' of github.com:king-alexander/pytest
|
@ -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
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -7,6 +7,10 @@ python:
|
|||
- method: pip
|
||||
path: .
|
||||
|
||||
build:
|
||||
apt_packages:
|
||||
- inkscape
|
||||
|
||||
formats:
|
||||
- epub
|
||||
- pdf
|
||||
|
|
2
AUTHORS
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
The PDF documentation’s list of plugins doesn’t run off the page anymore.
|
|
@ -0,0 +1,8 @@
|
|||
The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
||||
|
||||
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
||||
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
|
||||
|
||||
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
||||
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
||||
Note: pytest was not able to provide a deprecation period for this change.
|
|
@ -0,0 +1,3 @@
|
|||
``py.path.local`` arguments for hooks have been deprecated. See :ref:`the deprecation note <legacy-path-hooks-deprecated>` for full details.
|
||||
|
||||
``py.path.local`` arguments to Node constructors have been deprecated. See :ref:`the deprecation note <node-ctor-fspath-deprecation>` for full details.
|
|
@ -8,5 +8,6 @@ Directly constructing the following classes is now deprecated:
|
|||
- ``_pytest._code.ExceptionInfo``
|
||||
- ``_pytest.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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Improved error messages when parsing warning filters.
|
||||
|
||||
Previously pytest would show an internal traceback, which besides ugly sometimes would hide the cause
|
||||
of the problem (for example an ``ImportError`` while importing a specific warning type).
|
|
@ -1,2 +1,2 @@
|
|||
The test selection options ``pytest -k`` and ``pytest -m`` now support matching
|
||||
names containing forwardslash (``\/``)characters.
|
||||
names containing forward slash (``/``) characters.
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Included the module of the class in the error message about direct
|
||||
node construction (without using ``from_parent``).
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
Full diffs are now always shown for equality assertions of iterables when
|
||||
`CI` or ``BUILD_NUMBER`` is found in the environment, even when ``-v`` isn't
|
||||
used.
|
|
@ -0,0 +1,2 @@
|
|||
:class:`RunResult <_pytest.pytester.RunResult>` method :meth:`assert_outcomes <_pytest.pytester.RunResult.assert_outcomes>` now accepts a
|
||||
``deselected`` argument to assert the total number of deselected tests.
|
|
@ -0,0 +1 @@
|
|||
Fixed the URL used by ``--pastebin`` to use `bpa.st <http://bpa.st>`__.
|
|
@ -0,0 +1 @@
|
|||
The end line number and end column offset are now properly set for rewritten assert statements.
|
|
@ -0,0 +1 @@
|
|||
Support for the ``files`` API from ``importlib.resources`` within rewritten files.
|
|
@ -0,0 +1 @@
|
|||
:meth:`pytest.Cache.set` now preserves key order when saving dicts.
|
|
@ -0,0 +1 @@
|
|||
Remove incorrect docs about ``confcutdir`` being a configuration option: it can only be set through the ``--confcutdir`` command-line option.
|
|
@ -23,14 +23,13 @@ a full list of details. A few feature highlights:
|
|||
called if the corresponding setup method succeeded.
|
||||
|
||||
- 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
|
||||
|
|
|
@ -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>`_)
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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`:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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>`_.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>`_
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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``
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -4,3 +4,4 @@ pygments-pytest>=2.2.0
|
|||
sphinx-removed-in>=0.2.0
|
||||
sphinx>=3.1,<4
|
||||
sphinxcontrib-trio
|
||||
sphinxcontrib-svg2pdfconverter
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
pytest-{version}
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the {version} prerelease!
|
||||
|
||||
This is a prerelease, not intended for production use, but to test the upcoming features and improvements
|
||||
in order to catch any major problems before the final version is released to the major public.
|
||||
|
||||
We appreciate your help testing this out before the final release, making sure to report any
|
||||
regressions to our issue tracker:
|
||||
|
||||
https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
When doing so, please include the string ``[prerelease]`` in the title.
|
||||
|
||||
You can upgrade from PyPI via:
|
||||
|
||||
pip install pytest=={version}
|
||||
|
||||
Users are encouraged to take a look at the CHANGELOG carefully:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
Thanks to all the contributors to this release:
|
||||
|
||||
{contributors}
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
|
@ -10,7 +10,7 @@ from colorama import Fore
|
|||
from colorama import init
|
||||
|
||||
|
||||
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__":
|
||||
|
|
|
@ -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__":
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()."
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ expression: expr? EOF
|
|||
expr: and_expr ('or' and_expr)*
|
||||
and_expr: not_expr ('and' not_expr)*
|
||||
not_expr: 'not' not_expr | '(' expr ')' | ident
|
||||
ident: (\w|:|\+|-|\.|\[|\]|\\|\/)+
|
||||
ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
|
||||
|
||||
The semantics are:
|
||||
|
||||
|
@ -88,7 +88,7 @@ class Scanner:
|
|||
yield Token(TokenType.RPAREN, ")", pos)
|
||||
pos += 1
|
||||
else:
|
||||
match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|\/)+", input[pos:])
|
||||
match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
|
||||
if match:
|
||||
value = match.group(0)
|
||||
if value == "or":
|
||||
|
|
|
@ -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}>"
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'"
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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="""
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
@ -1112,7 +1111,7 @@ def test_pytest_param_id_allows_none_or_string(s) -> None:
|
|||
assert pytest.param(id=s)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expr", ("NOT internal_err", "NOT (internal_err)"))
|
||||
@pytest.mark.parametrize("expr", ("NOT internal_err", "NOT (internal_err)", "bogus="))
|
||||
def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None:
|
||||
foo = pytester.makepyfile(
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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__,
|
||||
)
|
||||
)
|
||||
|
|