Merge branch 'main' into Improvement-remove-prune_dependency_tree

This commit is contained in:
Sadra Barikbin 2023-09-05 10:42:30 +03:30
commit 8b75507215
54 changed files with 830 additions and 383 deletions

View File

@ -1,44 +1,58 @@
name: deploy name: deploy
on: on:
push: workflow_dispatch:
tags: inputs:
# These tags are protected, see: version:
# https://github.com/pytest-dev/pytest/settings/tag_protection description: 'Release version'
- "[0-9]+.[0-9]+.[0-9]+" required: true
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" default: '1.2.3'
# Set permissions at the job level. # Set permissions at the job level.
permissions: {} permissions: {}
jobs: jobs:
build: package:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Build and Check Package - name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5 uses: hynek/build-and-inspect-python-package@v1.5
deploy: deploy:
if: github.repository == 'pytest-dev/pytest' if: github.repository == 'pytest-dev/pytest'
needs: [build] needs: [package]
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: deploy
timeout-minutes: 30 timeout-minutes: 30
permissions: permissions:
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v3
- name: Download Package - name: Download Package
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: Packages name: Packages
path: dist path: dist
- name: Publish package to PyPI - name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.8 uses: pypa/gh-action-pypi-publish@v1.8.10
- name: Push tag
run: |
git config user.name "pytest bot"
git config user.email "pytestbot@gmail.com"
git tag --annotate --message=v${{ github.event.inputs.version }} v${{ github.event.inputs.version }} ${{ github.sha }}
git push origin v${{ github.event.inputs.version }}
release-notes: release-notes:
@ -55,12 +69,12 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.11" python-version: "3.11"
- name: Install tox - name: Install tox
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip

View File

@ -27,7 +27,19 @@ concurrency:
permissions: {} permissions: {}
jobs: jobs:
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5
build: build:
needs: [package]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 45 timeout-minutes: 45
permissions: permissions:
@ -58,7 +70,6 @@ jobs:
"macos-py310", "macos-py310",
"macos-py312", "macos-py312",
"docs",
"doctesting", "doctesting",
"plugins", "plugins",
] ]
@ -149,10 +160,6 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
tox_env: "plugins" tox_env: "plugins"
- name: "docs"
python: "3.8"
os: ubuntu-latest
tox_env: "docs"
- name: "doctesting" - name: "doctesting"
python: "3.8" python: "3.8"
os: ubuntu-latest os: ubuntu-latest
@ -165,6 +172,12 @@ jobs:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Download Package
uses: actions/download-artifact@v3
with:
name: Packages
path: dist
- name: Set up Python ${{ matrix.python }} - name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
@ -178,11 +191,13 @@ jobs:
- name: Test without coverage - name: Test without coverage
if: "! matrix.use_coverage" if: "! matrix.use_coverage"
run: "tox -e ${{ matrix.tox_env }}" shell: bash
run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`
- name: Test with coverage - name: Test with coverage
if: "matrix.use_coverage" if: "matrix.use_coverage"
run: "tox -e ${{ matrix.tox_env }}-coverage" shell: bash
run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz`
- name: Generate coverage report - name: Generate coverage report
if: "matrix.use_coverage" if: "matrix.use_coverage"
@ -196,10 +211,3 @@ jobs:
fail_ci_if_error: true fail_ci_if_error: true
files: ./coverage.xml files: ./coverage.xml
verbose: true verbose: true
check-package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5

View File

@ -27,7 +27,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.8" python-version: "3.11"
cache: pip cache: pip
- name: requests-cache - name: requests-cache
uses: actions/cache@v3 uses: actions/cache@v3

View File

@ -5,7 +5,7 @@ repos:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs - repo: https://github.com/asottile/blacken-docs
rev: 1.15.0 rev: 1.16.0
hooks: hooks:
- id: blacken-docs - id: blacken-docs
additional_dependencies: [black==23.7.0] additional_dependencies: [black==23.7.0]
@ -56,7 +56,7 @@ repos:
hooks: hooks:
- id: python-use-type-annotations - id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.4.1 rev: v1.5.1
hooks: hooks:
- id: mypy - id: mypy
files: ^(src/|testing/) files: ^(src/|testing/)

View File

@ -170,6 +170,7 @@ Ian Lesperance
Ilya Konstantinov Ilya Konstantinov
Ionuț Turturică Ionuț Turturică
Isaac Virshup Isaac Virshup
Israel Fruchter
Itxaso Aizpurua Itxaso Aizpurua
Iwan Briquemont Iwan Briquemont
Jaap Broekhuizen Jaap Broekhuizen
@ -336,6 +337,7 @@ Samuele Pedroni
Sanket Duthade Sanket Duthade
Sankt Petersbug Sankt Petersbug
Saravanan Padmanaban Saravanan Padmanaban
Sean Malloy
Segev Finer Segev Finer
Serhii Mozghovyi Serhii Mozghovyi
Seth Junot Seth Junot

View File

@ -50,7 +50,7 @@ Fix bugs
-------- --------
Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_. Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
See also the `"status: easy" issues <https://github.com/pytest-dev/pytest/labels/status%3A%20easy>`_ See also the `"good first issue" issues <https://github.com/pytest-dev/pytest/labels/good%20first%20issue>`_
that are friendly to new contributors. that are friendly to new contributors.
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going :ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going

View File

@ -133,14 +133,11 @@ Releasing
Both automatic and manual processes described above follow the same steps from this point onward. Both automatic and manual processes described above follow the same steps from this point onward.
#. After all tests pass and the PR has been approved, tag the release commit #. After all tests pass and the PR has been approved, trigger the ``deploy`` job
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI:: in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml.
git fetch upstream This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH and tag the repository.
git push upstream MAJOR.MINOR.PATCH
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch. #. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.

View File

@ -1,2 +0,0 @@
Fixed that fake intermediate modules generated by ``--import-mode=importlib`` would not include the
child modules as attributes of the parent modules.

View File

@ -1 +0,0 @@
Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.

View File

@ -1,2 +0,0 @@
Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
to be imported more than once, causing problems with modules that have import side effects.

View File

@ -0,0 +1,5 @@
(This entry is meant to assist plugins which access private pytest internals to instantiate ``FixtureRequest`` objects.)
:class:`~pytest.FixtureRequest` is now an abstract class which can't be instantiated directly.
A new concrete ``TopRequest`` subclass of ``FixtureRequest`` has been added for the ``request`` fixture in test functions,
as counterpart to the existing ``SubRequest`` subclass for the ``request`` fixture in fixture functions.

View File

@ -0,0 +1,2 @@
Corrected the spelling of ``Config.ArgsSource.INVOCATION_DIR``.
The previous spelling ``INCOVATION_DIR`` remains as an alias.

View File

@ -0,0 +1 @@
pluggy>=1.3.0 is now required. This adds typing to :class:`~pytest.PytestPluginManager`.

View File

@ -0,0 +1 @@
Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.

View File

@ -0,0 +1 @@
Removes unhelpful error message from assertion rewrite mechanism when exceptions raised in __iter__ methods, and instead treats them as un-iterable.

View File

@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-7.4.1
release-7.4.0 release-7.4.0
release-7.3.2 release-7.3.2
release-7.3.1 release-7.3.1

View File

@ -0,0 +1,20 @@
pytest-7.4.1
=======================================
pytest 7.4.1 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Bruno Oliveira
* Florian Bruhin
* Ran Benita
Happy testing,
The pytest Development Team

View File

@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
cachedir: .pytest_cache cachedir: .pytest_cache
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 0 items collected 0 items
cache -- .../_pytest/cacheprovider.py:528 cache -- .../_pytest/cacheprovider.py:532
Return a cache object that can persist state between testing sessions. Return a cache object that can persist state between testing sessions.
cache.get(key, default) cache.get(key, default)

View File

@ -28,6 +28,23 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. towncrier release notes start
pytest 7.4.1 (2023-09-02)
=========================
Bug Fixes
---------
- `#10337 <https://github.com/pytest-dev/pytest/issues/10337>`_: Fixed bug where fake intermediate modules generated by ``--import-mode=importlib`` would not include the
child modules as attributes of the parent modules.
- `#10702 <https://github.com/pytest-dev/pytest/issues/10702>`_: Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
- `#10811 <https://github.com/pytest-dev/pytest/issues/10811>`_: Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
to be imported more than once, causing problems with modules that have import side effects.
pytest 7.4.0 (2023-06-23) pytest 7.4.0 (2023-06-23)
========================= =========================

View File

@ -554,13 +554,13 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E AssertionError: assert False E AssertionError: assert False
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456') E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>() E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0006>()
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>() E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef0029>()
failure_demo.py:235: AssertionError failure_demo.py:235: AssertionError
_____________________ TestMoreErrors.test_global_func ______________________ _____________________ TestMoreErrors.test_global_func ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b> self = <failure_demo.TestMoreErrors object at 0xdeadbeef002a>
def test_global_func(self): def test_global_func(self):
> assert isinstance(globf(42), float) > assert isinstance(globf(42), float)
@ -571,18 +571,18 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:238: AssertionError failure_demo.py:238: AssertionError
_______________________ TestMoreErrors.test_instance _______________________ _______________________ TestMoreErrors.test_instance _______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c> self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
def test_instance(self): def test_instance(self):
self.x = 6 * 7 self.x = 6 * 7
> assert self.x != 42 > assert self.x != 42
E assert 42 != 42 E assert 42 != 42
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>.x E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>.x
failure_demo.py:242: AssertionError failure_demo.py:242: AssertionError
_______________________ TestMoreErrors.test_compare ________________________ _______________________ TestMoreErrors.test_compare ________________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d> self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
def test_compare(self): def test_compare(self):
> assert globf(10) < 5 > assert globf(10) < 5
@ -592,7 +592,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:245: AssertionError failure_demo.py:245: AssertionError
_____________________ TestMoreErrors.test_try_finally ______________________ _____________________ TestMoreErrors.test_try_finally ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e> self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
def test_try_finally(self): def test_try_finally(self):
x = 1 x = 1
@ -603,7 +603,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:250: AssertionError failure_demo.py:250: AssertionError
___________________ TestCustomAssertMsg.test_single_line ___________________ ___________________ TestCustomAssertMsg.test_single_line ___________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f> self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002e>
def test_single_line(self): def test_single_line(self):
class A: class A:
@ -618,7 +618,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:261: AssertionError failure_demo.py:261: AssertionError
____________________ TestCustomAssertMsg.test_multiline ____________________ ____________________ TestCustomAssertMsg.test_multiline ____________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030> self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
def test_multiline(self): def test_multiline(self):
class A: class A:
@ -637,7 +637,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:268: AssertionError failure_demo.py:268: AssertionError
___________________ TestCustomAssertMsg.test_custom_repr ___________________ ___________________ TestCustomAssertMsg.test_custom_repr ___________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031> self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
def test_custom_repr(self): def test_custom_repr(self):
class JSON: class JSON:

View File

@ -34,7 +34,7 @@ a function/method call.
**Assert** is where we look at that resulting state and check if it looks how **Assert** is where we look at that resulting state and check if it looks how
we'd expect after the dust has settled. It's where we gather evidence to say the we'd expect after the dust has settled. It's where we gather evidence to say the
behavior does or does not aligns with what we expect. The ``assert`` in our test behavior does or does not align with what we expect. The ``assert`` in our test
is where we take that measurement/observation and apply our judgement to it. If is where we take that measurement/observation and apply our judgement to it. If
something should be green, we'd say ``assert thing == "green"``. something should be green, we'd say ``assert thing == "green"``.

View File

@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash .. code-block:: bash
$ pytest --version $ pytest --version
pytest 7.4.0 pytest 7.4.1
.. _`simpletest`: .. _`simpletest`:

View File

@ -176,14 +176,21 @@ with more recent files coming first.
Behavior when no tests failed in the last run Behavior when no tests failed in the last run
--------------------------------------------- ---------------------------------------------
When no tests failed in the last run, or when no cached ``lastfailed`` data was The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
found, ``pytest`` can be configured either to run all of the tests or no tests, Determines whether to execute tests when there are no previously (known)
using the ``--last-failed-no-failures`` option, which takes one of the following values: failures or when no cached ``lastfailed`` data was found.
There are two options:
* ``all``: when there are no known test failures, runs all tests (the full test suite). This is the default.
* ``none``: when there are no known test failures, just emits a message stating this and exit successfully.
Example:
.. code-block:: bash .. code-block:: bash
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior) pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior)
pytest --last-failed --last-failed-no-failures none # run no tests and exit pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully
The new config.cache object The new config.cache object
-------------------------------- --------------------------------

View File

@ -51,8 +51,8 @@ Running this would result in a passed test except for the last
d = tmp_path / "sub" d = tmp_path / "sub"
d.mkdir() d.mkdir()
p = d / "hello.txt" p = d / "hello.txt"
p.write_text(CONTENT) p.write_text(CONTENT, encoding="utf-8")
assert p.read_text() == CONTENT assert p.read_text(encoding="utf-8") == CONTENT
assert len(list(tmp_path.iterdir())) == 1 assert len(list(tmp_path.iterdir())) == 1
> assert 0 > assert 0
E assert 0 E assert 0

File diff suppressed because it is too large Load Diff

View File

@ -237,7 +237,7 @@ pytest.mark.xfail
Marks a test function as *expected to fail*. Marks a test function as *expected to fail*.
.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=False) .. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=xfail_strict)
:type condition: bool or str :type condition: bool or str
:param condition: :param condition:
@ -249,10 +249,10 @@ Marks a test function as *expected to fail*.
:keyword Type[Exception] raises: :keyword Type[Exception] raises:
Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test. Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test.
:keyword bool run: :keyword bool run:
If the test function should actually be executed. If ``False``, the function will always xfail and will Whether the test function should actually be executed. If ``False``, the function will always xfail and will
not be executed (useful if a function is segfaulting). not be executed (useful if a function is segfaulting).
:keyword bool strict: :keyword bool strict:
* If ``False`` (the default) the function will be shown in the terminal output as ``xfailed`` if it fails * If ``False`` the function will be shown in the terminal output as ``xfailed`` if it fails
and as ``xpass`` if it passes. In both cases this will not cause the test suite to fail as a whole. This and as ``xpass`` if it passes. In both cases this will not cause the test suite to fail as a whole. This
is particularly useful to mark *flaky* tests (tests that fail at random) to be tackled later. is particularly useful to mark *flaky* tests (tests that fail at random) to be tackled later.
* If ``True``, the function will be shown in the terminal output as ``xfailed`` if it fails, but if it * If ``True``, the function will be shown in the terminal output as ``xfailed`` if it fails, but if it
@ -260,6 +260,8 @@ Marks a test function as *expected to fail*.
that are always failing and there should be a clear indication if they unexpectedly start to pass (for example that are always failing and there should be a clear indication if they unexpectedly start to pass (for example
a new release of a library fixes a known bug). a new release of a library fixes a known bug).
Defaults to :confval:`xfail_strict`, which is ``False`` by default.
Custom marks Custom marks
~~~~~~~~~~~~ ~~~~~~~~~~~~
@ -978,10 +980,10 @@ TestShortLogReport
.. autoclass:: pytest.TestShortLogReport() .. autoclass:: pytest.TestShortLogReport()
:members: :members:
_Result Result
~~~~~~~ ~~~~~~~
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for more information. Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`Result in the pluggy documentation <pluggy.Result>` for more information.
Stash Stash
~~~~~ ~~~~~
@ -1638,11 +1640,11 @@ passed multiple times. The expected format is ``name=value``. For example::
Additionally, ``pytest`` will attempt to intelligently identify and ignore a Additionally, ``pytest`` will attempt to intelligently identify and ignore a
virtualenv by the presence of an activation script. Any directory deemed to virtualenv by the presence of an activation script. Any directory deemed to
be the root of a virtual environment will not be considered during test be the root of a virtual environment will not be considered during test
collection unless ``collectinvirtualenv`` is given. Note also that collection unless ``--collect-in-virtualenv`` is given. Note also that
``norecursedirs`` takes precedence over ``collectinvirtualenv``; e.g. if ``norecursedirs`` takes precedence over ``--collect-in-virtualenv``; e.g. if
you intend to run tests in a virtualenv with a base directory that matches you intend to run tests in a virtualenv with a base directory that matches
``'.*'`` you *must* override ``norecursedirs`` in addition to using the ``'.*'`` you *must* override ``norecursedirs`` in addition to using the
``collectinvirtualenv`` flag. ``--collect-in-virtualenv`` flag.
.. confval:: python_classes .. confval:: python_classes
@ -1890,8 +1892,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
tests. Optional argument: glob (default: '*'). tests. Optional argument: glob (default: '*').
--cache-clear Remove all cache contents at start of test run --cache-clear Remove all cache contents at start of test run
--lfnf={all,none}, --last-failed-no-failures={all,none} --lfnf={all,none}, --last-failed-no-failures={all,none}
Which tests to run with no previously (known) With ``--lf``, determines whether to execute tests
failures when there are no previously (known) failures or
when no cached ``lastfailed`` data was found.
``all`` (the default) runs the full test suite
again. ``none`` just emits a message about no known
failures and exits successfully.
--sw, --stepwise Exit on test failure and continue from last failing --sw, --stepwise Exit on test failure and continue from last failing
test next time test next time
--sw-skip, --stepwise-skip --sw-skip, --stepwise-skip

View File

@ -17,7 +17,12 @@ python_classes = ["Test", "Acceptance"]
python_functions = ["test"] python_functions = ["test"]
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting". # NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
testpaths = ["testing"] testpaths = ["testing"]
norecursedirs = ["testing/example_scripts"] norecursedirs = [
"testing/example_scripts",
".*",
"build",
"dist",
]
xfail_strict = true xfail_strict = true
filterwarnings = [ filterwarnings = [
"error", "error",

View File

@ -44,6 +44,7 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
) )
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
"logassert", "logassert",
"nuts",
} }

View File

@ -46,7 +46,7 @@ py_modules = py
install_requires = install_requires =
iniconfig iniconfig
packaging packaging
pluggy>=1.2.0,<2.0 pluggy>=1.3.0,<2.0
colorama;sys_platform=="win32" colorama;sys_platform=="win32"
exceptiongroup>=1.0.0rc8;python_version<"3.11" exceptiongroup>=1.0.0rc8;python_version<"3.11"
tomli>=1.0.0;python_version<"3.11" tomli>=1.0.0;python_version<"3.11"

View File

@ -132,7 +132,7 @@ def isiterable(obj: Any) -> bool:
try: try:
iter(obj) iter(obj)
return not istext(obj) return not istext(obj)
except TypeError: except Exception:
return False return False

View File

@ -499,7 +499,11 @@ def pytest_addoption(parser: Parser) -> None:
dest="last_failed_no_failures", dest="last_failed_no_failures",
choices=("all", "none"), choices=("all", "none"),
default="all", default="all",
help="Which tests to run with no previously (known) failures", help="With ``--lf``, determines whether to execute tests when there "
"are no previously (known) failures or when no "
"cached ``lastfailed`` data was found. "
"``all`` (the default) runs the full test suite again. "
"``none`` just emits a message about no known failures and exits successfully.",
) )

View File

@ -314,15 +314,24 @@ def safe_isclass(obj: object) -> bool:
def get_user_id() -> int | None: def get_user_id() -> int | None:
"""Return the current user id, or None if we cannot get it reliably on the current platform.""" """Return the current process's real user id or None if it could not be
determined.
:return: The user id or None if it could not be determined.
"""
# mypy follows the version and platform checking expectation of PEP 484:
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
# Containment checks are too complex for mypy v1.5.0 and cause failure.
if sys.platform == "win32" or sys.platform == "emscripten":
# win32 does not have a getuid() function. # win32 does not have a getuid() function.
# On Emscripten, getuid() is a stub that always returns 0. # Emscripten has a return 0 stub.
if sys.platform in ("win32", "emscripten"):
return None return None
# getuid shouldn't fail, but cpython defines such a case. else:
# Let's hope for the best. # On other platforms, a return value of -1 is assumed to indicate that
# the current process's real user id could not be determined.
ERROR = -1
uid = os.getuid() uid = os.getuid()
return uid if uid != -1 else None return uid if uid != ERROR else None
# Perform exhaustiveness checking. # Perform exhaustiveness checking.

View File

@ -38,7 +38,9 @@ from typing import TYPE_CHECKING
from typing import Union from typing import Union
from pluggy import HookimplMarker from pluggy import HookimplMarker
from pluggy import HookimplOpts
from pluggy import HookspecMarker from pluggy import HookspecMarker
from pluggy import HookspecOpts
from pluggy import PluginManager from pluggy import PluginManager
import _pytest._code import _pytest._code
@ -440,15 +442,17 @@ class PytestPluginManager(PluginManager):
# Used to know when we are importing conftests after the pytest_configure stage. # Used to know when we are importing conftests after the pytest_configure stage.
self._configured = False self._configured = False
def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str): def parse_hookimpl_opts(
self, plugin: _PluggyPlugin, name: str
) -> Optional[HookimplOpts]:
# pytest hooks are always prefixed with "pytest_", # pytest hooks are always prefixed with "pytest_",
# so we avoid accessing possibly non-readable attributes # so we avoid accessing possibly non-readable attributes
# (see issue #1073). # (see issue #1073).
if not name.startswith("pytest_"): if not name.startswith("pytest_"):
return return None
# Ignore names which can not be hooks. # Ignore names which can not be hooks.
if name == "pytest_plugins": if name == "pytest_plugins":
return return None
opts = super().parse_hookimpl_opts(plugin, name) opts = super().parse_hookimpl_opts(plugin, name)
if opts is not None: if opts is not None:
@ -457,18 +461,18 @@ class PytestPluginManager(PluginManager):
method = getattr(plugin, name) method = getattr(plugin, name)
# Consider only actual functions for hooks (#3775). # Consider only actual functions for hooks (#3775).
if not inspect.isroutine(method): if not inspect.isroutine(method):
return return None
# Collect unmarked hooks as long as they have the `pytest_' prefix. # Collect unmarked hooks as long as they have the `pytest_' prefix.
return _get_legacy_hook_marks( return _get_legacy_hook_marks( # type: ignore[return-value]
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper") method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
) )
def parse_hookspec_opts(self, module_or_class, name: str): def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]:
opts = super().parse_hookspec_opts(module_or_class, name) opts = super().parse_hookspec_opts(module_or_class, name)
if opts is None: if opts is None:
method = getattr(module_or_class, name) method = getattr(module_or_class, name)
if name.startswith("pytest_"): if name.startswith("pytest_"):
opts = _get_legacy_hook_marks( opts = _get_legacy_hook_marks( # type: ignore[assignment]
method, method,
"spec", "spec",
("firstresult", "historic"), ("firstresult", "historic"),
@ -953,7 +957,8 @@ class Config:
#: Command line arguments. #: Command line arguments.
ARGS = enum.auto() ARGS = enum.auto()
#: Invocation directory. #: Invocation directory.
INCOVATION_DIR = enum.auto() INVOCATION_DIR = enum.auto()
INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
#: 'testpaths' configuration value. #: 'testpaths' configuration value.
TESTPATHS = enum.auto() TESTPATHS = enum.auto()
@ -1066,9 +1071,10 @@ class Config:
fin() fin()
def get_terminal_writer(self) -> TerminalWriter: def get_terminal_writer(self) -> TerminalWriter:
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin( terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
"terminalreporter" "terminalreporter"
) )
assert terminalreporter is not None
return terminalreporter._tw return terminalreporter._tw
def pytest_cmdline_parse( def pytest_cmdline_parse(
@ -1278,7 +1284,7 @@ class Config:
else: else:
result = [] result = []
if not result: if not result:
source = Config.ArgsSource.INCOVATION_DIR source = Config.ArgsSource.INVOCATION_DIR
result = [str(invocation_dir)] result = [str(invocation_dir)]
return result, source return result, source

View File

@ -32,7 +32,7 @@ from _pytest.compat import safe_getattr
from _pytest.config import Config from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import TopRequest
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.outcomes import OutcomeException from _pytest.outcomes import OutcomeException
@ -261,7 +261,7 @@ class DoctestItem(Item):
self.runner = runner self.runner = runner
self.dtest = dtest self.dtest = dtest
self.obj = None self.obj = None
self.fixture_request: Optional[FixtureRequest] = None self.fixture_request: Optional[TopRequest] = None
@classmethod @classmethod
def from_parent( # type: ignore def from_parent( # type: ignore
@ -571,7 +571,7 @@ class DoctestModule(Module):
) )
def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest: def _setup_fixtures(doctest_item: DoctestItem) -> TopRequest:
"""Used by DoctestTextfile and DoctestItem to setup fixture information.""" """Used by DoctestTextfile and DoctestItem to setup fixture information."""
def func() -> None: def func() -> None:
@ -582,7 +582,7 @@ def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined] doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
node=doctest_item, func=func, cls=None, funcargs=False node=doctest_item, func=func, cls=None, funcargs=False
) )
fixture_request = FixtureRequest(doctest_item, _ispytest=True) # type: ignore[arg-type] fixture_request = TopRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
fixture_request._fillfixtures() fixture_request._fillfixtures()
return fixture_request return fixture_request

View File

@ -1,3 +1,4 @@
import abc
import dataclasses import dataclasses
import functools import functools
import inspect import inspect
@ -313,26 +314,32 @@ class FuncFixtureInfo:
name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]] name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]]
class FixtureRequest: class FixtureRequest(abc.ABC):
"""A request for a fixture from a test or fixture function. """The type of the ``request`` fixture.
A request object gives access to the requesting test context and has A request object gives access to the requesting test context and has a
an optional ``param`` attribute in case the fixture is parametrized ``param`` attribute in case the fixture is parametrized.
indirectly.
""" """
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None: def __init__(
self,
pyfuncitem: "Function",
fixturename: Optional[str],
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
arg2index: Dict[str, int],
fixture_defs: Dict[str, "FixtureDef[Any]"],
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest) check_ispytest(_ispytest)
#: Fixture for which this request is being performed. #: Fixture for which this request is being performed.
self.fixturename: Optional[str] = None self.fixturename: Final = fixturename
self._pyfuncitem = pyfuncitem self._pyfuncitem: Final = pyfuncitem
self._fixturemanager = pyfuncitem.session._fixturemanager
self._scope = Scope.Function
# The FixtureDefs for each fixture name requested by this item. # The FixtureDefs for each fixture name requested by this item.
# Starts from the statically-known fixturedefs resolved during # Starts from the statically-known fixturedefs resolved during
# collection. Dynamically requested fixtures (using # collection. Dynamically requested fixtures (using
# `request.getfixturevalue("foo")`) are added dynamically. # `request.getfixturevalue("foo")`) are added dynamically.
self._arg2fixturedefs = pyfuncitem._fixtureinfo.name2fixturedefs.copy() self._arg2fixturedefs: Final = arg2fixturedefs
# A fixture may override another fixture with the same name, e.g. a fixture # A fixture may override another fixture with the same name, e.g. a fixture
# in a module can override a fixture in a conftest, a fixture in a class can # in a module can override a fixture in a conftest, a fixture in a class can
# override a fixture in the module, and so on. # override a fixture in the module, and so on.
@ -342,10 +349,10 @@ class FixtureRequest:
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from # The fixturedefs list in _arg2fixturedefs for a given name is ordered from
# furthest to closest, so we use negative indexing -1, -2, ... to go from # furthest to closest, so we use negative indexing -1, -2, ... to go from
# last to first. # last to first.
self._arg2index: Dict[str, int] = {} self._arg2index: Final = arg2index
# The evaluated argnames so far, mapping to the FixtureDef they resolved # The evaluated argnames so far, mapping to the FixtureDef they resolved
# to. # to.
self._fixture_defs: Dict[str, FixtureDef[Any]] = {} self._fixture_defs: Final = fixture_defs
# Notes on the type of `param`: # Notes on the type of `param`:
# -`request.param` is only defined in parametrized fixtures, and will raise # -`request.param` is only defined in parametrized fixtures, and will raise
# AttributeError otherwise. Python typing has no notion of "undefined", so # AttributeError otherwise. Python typing has no notion of "undefined", so
@ -356,6 +363,15 @@ class FixtureRequest:
# for now just using Any. # for now just using Any.
self.param: Any self.param: Any
@property
def _fixturemanager(self) -> "FixtureManager":
return self._pyfuncitem.session._fixturemanager
@property
@abc.abstractmethod
def _scope(self) -> Scope:
raise NotImplementedError()
@property @property
def scope(self) -> _ScopeName: def scope(self) -> _ScopeName:
"""Scope string, one of "function", "class", "module", "package", "session".""" """Scope string, one of "function", "class", "module", "package", "session"."""
@ -369,25 +385,10 @@ class FixtureRequest:
return result return result
@property @property
@abc.abstractmethod
def node(self): def node(self):
"""Underlying collection node (depends on current request scope).""" """Underlying collection node (depends on current request scope)."""
scope = self._scope raise NotImplementedError()
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package:
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on SubRequest (a subclass).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]": def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None) fixturedefs = self._arg2fixturedefs.get(argname, None)
@ -473,11 +474,11 @@ class FixtureRequest:
"""Pytest session object.""" """Pytest session object."""
return self._pyfuncitem.session # type: ignore[no-any-return] return self._pyfuncitem.session # type: ignore[no-any-return]
@abc.abstractmethod
def addfinalizer(self, finalizer: Callable[[], object]) -> None: def addfinalizer(self, finalizer: Callable[[], object]) -> None:
"""Add finalizer/teardown function to be called without arguments after """Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution.""" the last test within the requesting test context finished execution."""
# XXX usually this method is shadowed by fixturedef specific ones. raise NotImplementedError()
self.node.addfinalizer(finalizer)
def applymarker(self, marker: Union[str, MarkDecorator]) -> None: def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
"""Apply a marker to a single test function invocation. """Apply a marker to a single test function invocation.
@ -498,13 +499,6 @@ class FixtureRequest:
""" """
raise self._fixturemanager.FixtureLookupError(None, self, msg) raise self._fixturemanager.FixtureLookupError(None, self, msg)
def _fillfixtures(self) -> None:
item = self._pyfuncitem
fixturenames = getattr(item, "fixturenames", self.fixturenames)
for argname in fixturenames:
if argname not in item.funcargs:
item.funcargs[argname] = self.getfixturevalue(argname)
def getfixturevalue(self, argname: str) -> Any: def getfixturevalue(self, argname: str) -> Any:
"""Dynamically run a named fixture function. """Dynamically run a named fixture function.
@ -638,6 +632,98 @@ class FixtureRequest:
finalizer = functools.partial(fixturedef.finish, request=subrequest) finalizer = functools.partial(fixturedef.finish, request=subrequest)
subrequest.node.addfinalizer(finalizer) subrequest.node.addfinalizer(finalizer)
@final
class TopRequest(FixtureRequest):
"""The type of the ``request`` fixture in a test function."""
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
super().__init__(
fixturename=None,
pyfuncitem=pyfuncitem,
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
arg2index={},
fixture_defs={},
_ispytest=_ispytest,
)
@property
def _scope(self) -> Scope:
return Scope.Function
@property
def node(self):
return self._pyfuncitem
def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node)
def _fillfixtures(self) -> None:
item = self._pyfuncitem
fixturenames = getattr(item, "fixturenames", self.fixturenames)
for argname in fixturenames:
if argname not in item.funcargs:
item.funcargs[argname] = self.getfixturevalue(argname)
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
self.node.addfinalizer(finalizer)
@final
class SubRequest(FixtureRequest):
"""The type of the ``request`` fixture in a fixture function requested
(transitively) by a test function."""
def __init__(
self,
request: FixtureRequest,
scope: Scope,
param: Any,
param_index: int,
fixturedef: "FixtureDef[object]",
*,
_ispytest: bool = False,
) -> None:
super().__init__(
pyfuncitem=request._pyfuncitem,
fixturename=fixturedef.argname,
fixture_defs=request._fixture_defs,
arg2fixturedefs=request._arg2fixturedefs,
arg2index=request._arg2index,
_ispytest=_ispytest,
)
self._parent_request: Final[FixtureRequest] = request
self._scope_field: Final = scope
self._fixturedef: Final = fixturedef
if param is not NOTSET:
self.param = param
self.param_index: Final = param_index
def __repr__(self) -> str:
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
@property
def _scope(self) -> Scope:
return self._scope_field
@property
def node(self):
scope = self._scope
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package:
node = get_scope_package(self._pyfuncitem, self._fixturedef)
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
def _check_scope( def _check_scope(
self, self,
argname: str, argname: str,
@ -672,44 +758,7 @@ class FixtureRequest:
) )
return lines return lines
def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node)
@final
class SubRequest(FixtureRequest):
"""A sub request for handling getting a fixture from a test function/fixture."""
def __init__(
self,
request: "FixtureRequest",
scope: Scope,
param: Any,
param_index: int,
fixturedef: "FixtureDef[object]",
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self._parent_request = request
self.fixturename = fixturedef.argname
if param is not NOTSET:
self.param = param
self.param_index = param_index
self._scope = scope
self._fixturedef = fixturedef
self._pyfuncitem = request._pyfuncitem
self._fixture_defs = request._fixture_defs
self._arg2fixturedefs = request._arg2fixturedefs
self._arg2index = request._arg2index
self._fixturemanager = request._fixturemanager
def __repr__(self) -> str:
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
def addfinalizer(self, finalizer: Callable[[], object]) -> None: def addfinalizer(self, finalizer: Callable[[], object]) -> None:
"""Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution."""
self._fixturedef.addfinalizer(finalizer) self._fixturedef.addfinalizer(finalizer)
def _schedule_finalizers( def _schedule_finalizers(

View File

@ -12,6 +12,7 @@ from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import PrintHelp from _pytest.config import PrintHelp
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.terminal import TerminalReporter
class HelpAction(Action): class HelpAction(Action):
@ -161,7 +162,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def showhelp(config: Config) -> None: def showhelp(config: Config) -> None:
import textwrap import textwrap
reporter = config.pluginmanager.get_plugin("terminalreporter") reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin(
"terminalreporter"
)
assert reporter is not None
tw = reporter._tw tw = reporter._tw
tw.write(config._parser.optparser.format_help()) tw.write(config._parser.optparser.format_help())
tw.line() tw.line()

View File

@ -502,6 +502,10 @@ class LogXML:
# Local hack to handle xdist report order. # Local hack to handle xdist report order.
workernode = getattr(report, "node", None) workernode = getattr(report, "node", None)
reporter = self.node_reporters.pop((nodeid, workernode)) reporter = self.node_reporters.pop((nodeid, workernode))
for propname, propvalue in report.user_properties:
reporter.add_property(propname, str(propvalue))
if reporter is not None: if reporter is not None:
reporter.finalize() reporter.finalize()
@ -599,9 +603,6 @@ class LogXML:
reporter = self._opentestcase(report) reporter = self._opentestcase(report)
reporter.write_captured_output(report) reporter.write_captured_output(report)
for propname, propvalue in report.user_properties:
reporter.add_property(propname, str(propvalue))
self.finalize(report) self.finalize(report)
report_wid = getattr(report, "worker_id", None) report_wid = getattr(report, "worker_id", None)
report_ii = getattr(report, "item_index", None) report_ii = getattr(report, "item_index", None)

View File

@ -659,6 +659,8 @@ class LoggingPlugin:
) )
if self._log_cli_enabled(): if self._log_cli_enabled():
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
# Guaranteed by `_log_cli_enabled()`.
assert terminal_reporter is not None
capture_manager = config.pluginmanager.get_plugin("capturemanager") capture_manager = config.pluginmanager.get_plugin("capturemanager")
# if capturemanager plugin is disabled, live logging still works. # if capturemanager plugin is disabled, live logging still works.
self.log_cli_handler: Union[ self.log_cli_handler: Union[

View File

@ -461,7 +461,9 @@ if TYPE_CHECKING:
*conditions: Union[str, bool], *conditions: Union[str, bool],
reason: str = ..., reason: str = ...,
run: bool = ..., run: bool = ...,
raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ..., raises: Union[
None, Type[BaseException], Tuple[Type[BaseException], ...]
] = ...,
strict: bool = ..., strict: bool = ...,
) -> MarkDecorator: ) -> MarkDecorator:
... ...

View File

@ -233,6 +233,9 @@ def xfail(reason: str = "") -> NoReturn:
This function should be called only during testing (setup, call or teardown). This function should be called only during testing (setup, call or teardown).
No other code is executed after using ``xfail()`` (it is implemented
internally by raising an exception).
:param reason: :param reason:
The message to show the user as reason for the xfail. The message to show the user as reason for the xfail.

View File

@ -751,7 +751,7 @@ class Pytester:
def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder: def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
"""Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`.""" """Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`."""
pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined]
self._request.addfinalizer(reprec.finish_recording) self._request.addfinalizer(reprec.finish_recording)
return reprec return reprec
@ -829,7 +829,7 @@ class Pytester:
return self._makefile(ext, args, kwargs) return self._makefile(ext, args, kwargs)
def makeconftest(self, source: str) -> Path: def makeconftest(self, source: str) -> Path:
"""Write a contest.py file. """Write a conftest.py file.
:param source: The contents. :param source: The contents.
:returns: The conftest.py file. :returns: The conftest.py file.

View File

@ -1817,7 +1817,7 @@ class Function(PyobjMixin, nodes.Item):
def _initrequest(self) -> None: def _initrequest(self) -> None:
self.funcargs: Dict[str, object] = {} self.funcargs: Dict[str, object] = {}
self._request = fixtures.FixtureRequest(self, _ispytest=True) self._request = fixtures.TopRequest(self, _ispytest=True)
@property @property
def function(self): def function(self):

View File

@ -1,5 +1,7 @@
# PYTHON_ARGCOMPLETE_OK # PYTHON_ARGCOMPLETE_OK
"""pytest: unit and functional testing with Python.""" """pytest: unit and functional testing with Python."""
from typing import TYPE_CHECKING
from _pytest import __version__ from _pytest import __version__
from _pytest import version_tuple from _pytest import version_tuple
from _pytest._code import ExceptionInfo from _pytest._code import ExceptionInfo
@ -165,8 +167,9 @@ __all__ = [
"yield_fixture", "yield_fixture",
] ]
if not TYPE_CHECKING:
def __getattr__(name: str) -> object: def __getattr__(name: str) -> object:
if name == "Instance": if name == "Instance":
# The import emits a deprecation warning. # The import emits a deprecation warning.
from _pytest.python import Instance from _pytest.python import Instance

View File

@ -15,7 +15,7 @@ from py.path import local
def ignore_encoding_warning(): def ignore_encoding_warning():
with warnings.catch_warnings(): with warnings.catch_warnings():
with contextlib.suppress(NameError): # new in 3.10 with contextlib.suppress(NameError): # new in 3.10
warnings.simplefilter("ignore", EncodingWarning) warnings.simplefilter("ignore", EncodingWarning) # type: ignore [name-defined] # noqa: F821
yield yield

View File

@ -272,7 +272,7 @@ def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
pytest.PytestDeprecationWarning, pytest.PytestDeprecationWarning,
match=re.escape("The pytest.Instance collector type is deprecated"), match=re.escape("The pytest.Instance collector type is deprecated"),
): ):
pytest.Instance pytest.Instance # type:ignore[attr-defined]
with pytest.warns( with pytest.warns(
pytest.PytestDeprecationWarning, pytest.PytestDeprecationWarning,

View File

@ -1,15 +1,15 @@
anyio[curio,trio]==3.7.1 anyio[curio,trio]==4.0.0
django==4.2.4 django==4.2.4
pytest-asyncio==0.21.1 pytest-asyncio==0.21.1
pytest-bdd==6.1.1 pytest-bdd==6.1.1
pytest-cov==4.1.0 pytest-cov==4.1.0
pytest-django==4.5.2 pytest-django==4.5.2
pytest-flakes==4.0.5 pytest-flakes==4.0.5
pytest-html==3.2.0 pytest-html==4.0.0
pytest-mock==3.11.1 pytest-mock==3.11.1
pytest-rerunfailures==12.0 pytest-rerunfailures==12.0
pytest-sugar==0.9.7 pytest-sugar==0.9.7
pytest-trio==0.7.0 pytest-trio==0.7.0
pytest-twisted==1.14.0 pytest-twisted==1.14.0
twisted==22.8.0 twisted==23.8.0
pytest-xvfb==3.0.0 pytest-xvfb==3.0.0

View File

@ -4,10 +4,9 @@ import textwrap
from pathlib import Path from pathlib import Path
import pytest import pytest
from _pytest import fixtures
from _pytest.compat import getfuncargnames from _pytest.compat import getfuncargnames
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import TopRequest
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import get_public_names from _pytest.pytester import get_public_names
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
@ -659,7 +658,7 @@ class TestRequestBasic:
""" """
) )
assert isinstance(item, Function) assert isinstance(item, Function)
req = fixtures.FixtureRequest(item, _ispytest=True) req = TopRequest(item, _ispytest=True)
assert req.function == item.obj assert req.function == item.obj
assert req.keywords == item.keywords assert req.keywords == item.keywords
assert hasattr(req.module, "test_func") assert hasattr(req.module, "test_func")
@ -701,9 +700,7 @@ class TestRequestBasic:
(item1,) = pytester.genitems([modcol]) (item1,) = pytester.genitems([modcol])
assert isinstance(item1, Function) assert isinstance(item1, Function)
assert item1.name == "test_method" assert item1.name == "test_method"
arg2fixturedefs = fixtures.FixtureRequest( arg2fixturedefs = TopRequest(item1, _ispytest=True)._arg2fixturedefs
item1, _ispytest=True
)._arg2fixturedefs
assert len(arg2fixturedefs) == 1 assert len(arg2fixturedefs) == 1
assert arg2fixturedefs["something"][0].argname == "something" assert arg2fixturedefs["something"][0].argname == "something"
@ -969,7 +966,7 @@ class TestRequestBasic:
modcol = pytester.getmodulecol("def test_somefunc(): pass") modcol = pytester.getmodulecol("def test_somefunc(): pass")
(item,) = pytester.genitems([modcol]) (item,) = pytester.genitems([modcol])
assert isinstance(item, Function) assert isinstance(item, Function)
req = fixtures.FixtureRequest(item, _ispytest=True) req = TopRequest(item, _ispytest=True)
assert req.path == modcol.path assert req.path == modcol.path
def test_request_fixturenames(self, pytester: Pytester) -> None: def test_request_fixturenames(self, pytester: Pytester) -> None:
@ -1128,7 +1125,7 @@ class TestRequestMarking:
""" """
) )
assert isinstance(item1, Function) assert isinstance(item1, Function)
req1 = fixtures.FixtureRequest(item1, _ispytest=True) req1 = TopRequest(item1, _ispytest=True)
assert "xfail" not in item1.keywords assert "xfail" not in item1.keywords
req1.applymarker(pytest.mark.xfail) req1.applymarker(pytest.mark.xfail)
assert "xfail" in item1.keywords assert "xfail" in item1.keywords
@ -4036,7 +4033,7 @@ class TestScopeOrdering:
) )
items, _ = pytester.inline_genitems() items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function) assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True) request = TopRequest(items[0], _ispytest=True)
assert request.fixturenames == "m1 f1".split() assert request.fixturenames == "m1 f1".split()
def test_func_closure_with_native_fixtures( def test_func_closure_with_native_fixtures(
@ -4085,7 +4082,7 @@ class TestScopeOrdering:
) )
items, _ = pytester.inline_genitems() items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function) assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True) request = TopRequest(items[0], _ispytest=True)
# order of fixtures based on their scope and position in the parameter list # order of fixtures based on their scope and position in the parameter list
assert ( assert (
request.fixturenames request.fixturenames
@ -4113,7 +4110,7 @@ class TestScopeOrdering:
) )
items, _ = pytester.inline_genitems() items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function) assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True) request = TopRequest(items[0], _ispytest=True)
assert request.fixturenames == "m1 f1".split() assert request.fixturenames == "m1 f1".split()
def test_func_closure_scopes_reordered(self, pytester: Pytester) -> None: def test_func_closure_scopes_reordered(self, pytester: Pytester) -> None:
@ -4147,7 +4144,7 @@ class TestScopeOrdering:
) )
items, _ = pytester.inline_genitems() items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function) assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True) request = TopRequest(items[0], _ispytest=True)
assert request.fixturenames == "s1 m1 c1 f2 f1".split() assert request.fixturenames == "s1 m1 c1 f2 f1".split()
def test_func_closure_same_scope_closer_root_first( def test_func_closure_same_scope_closer_root_first(
@ -4190,7 +4187,7 @@ class TestScopeOrdering:
) )
items, _ = pytester.inline_genitems() items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function) assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True) request = TopRequest(items[0], _ispytest=True)
assert request.fixturenames == "p_sub m_conf m_sub m_test f1".split() assert request.fixturenames == "p_sub m_conf m_sub m_test f1".split()
def test_func_closure_all_scopes_complex(self, pytester: Pytester) -> None: def test_func_closure_all_scopes_complex(self, pytester: Pytester) -> None:
@ -4235,7 +4232,7 @@ class TestScopeOrdering:
) )
items, _ = pytester.inline_genitems() items, _ = pytester.inline_genitems()
assert isinstance(items[0], Function) assert isinstance(items[0], Function)
request = FixtureRequest(items[0], _ispytest=True) request = TopRequest(items[0], _ispytest=True)
assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split() assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split()
def test_multiple_packages(self, pytester: Pytester) -> None: def test_multiple_packages(self, pytester: Pytester) -> None:

View File

@ -685,6 +685,25 @@ class TestAssertionRewrite:
assert msg is not None assert msg is not None
assert "<MY42 object> < 0" in msg assert "<MY42 object> < 0" in msg
def test_assert_handling_raise_in__iter__(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""\
class A:
def __iter__(self):
raise ValueError()
def __eq__(self, o: object) -> bool:
return self is o
def __repr__(self):
return "<A object>"
assert A() == A()
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*E*assert <A object> == <A object>"])
def test_formatchar(self) -> None: def test_formatchar(self) -> None:
def f() -> None: def f() -> None:
assert "%test" == "test" # type: ignore[comparison-overlap] assert "%test" == "test" # type: ignore[comparison-overlap]

View File

@ -507,6 +507,24 @@ class TestParseIni:
result = pytester.runpytest("--foo=1") result = pytester.runpytest("--foo=1")
result.stdout.fnmatch_lines("* no tests ran in *") result.stdout.fnmatch_lines("* no tests ran in *")
def test_args_source_args(self, pytester: Pytester):
config = pytester.parseconfig("--", "test_filename.py")
assert config.args_source == Config.ArgsSource.ARGS
def test_args_source_invocation_dir(self, pytester: Pytester):
config = pytester.parseconfig()
assert config.args_source == Config.ArgsSource.INVOCATION_DIR
def test_args_source_testpaths(self, pytester: Pytester):
pytester.makeini(
"""
[pytest]
testpaths=*
"""
)
config = pytester.parseconfig()
assert config.args_source == Config.ArgsSource.TESTPATHS
class TestConfigCmdlineParsing: class TestConfigCmdlineParsing:
def test_parsing_again_fails(self, pytester: Pytester) -> None: def test_parsing_again_fails(self, pytester: Pytester) -> None:

View File

@ -1228,6 +1228,36 @@ def test_record_property(pytester: Pytester, run_and_parse: RunAndParse) -> None
result.stdout.fnmatch_lines(["*= 1 passed in *"]) result.stdout.fnmatch_lines(["*= 1 passed in *"])
def test_record_property_on_test_and_teardown_failure(
pytester: Pytester, run_and_parse: RunAndParse
) -> None:
pytester.makepyfile(
"""
import pytest
@pytest.fixture
def other(record_property):
record_property("bar", 1)
yield
assert 0
def test_record(record_property, other):
record_property("foo", "<1")
assert 0
"""
)
result, dom = run_and_parse()
node = dom.find_first_by_tag("testsuite")
tnodes = node.find_by_tag("testcase")
for tnode in tnodes:
psnode = tnode.find_first_by_tag("properties")
assert psnode, f"testcase didn't had expected properties:\n{tnode}"
pnodes = psnode.find_by_tag("property")
pnodes[0].assert_attr(name="bar", value="1")
pnodes[1].assert_attr(name="foo", value="<1")
result.stdout.fnmatch_lines(["*= 1 failed, 1 error *"])
def test_record_property_same_name( def test_record_property_same_name(
pytester: Pytester, run_and_parse: RunAndParse pytester: Pytester, run_and_parse: RunAndParse
) -> None: ) -> None:

View File

@ -2,6 +2,7 @@ from pathlib import Path
import pytest import pytest
from _pytest.compat import LEGACY_PATH from _pytest.compat import LEGACY_PATH
from _pytest.fixtures import TopRequest
from _pytest.legacypath import TempdirFactory from _pytest.legacypath import TempdirFactory
from _pytest.legacypath import Testdir from _pytest.legacypath import Testdir
@ -91,7 +92,7 @@ def test_fixturerequest_getmodulepath(pytester: pytest.Pytester) -> None:
modcol = pytester.getmodulecol("def test_somefunc(): pass") modcol = pytester.getmodulecol("def test_somefunc(): pass")
(item,) = pytester.genitems([modcol]) (item,) = pytester.genitems([modcol])
assert isinstance(item, pytest.Function) assert isinstance(item, pytest.Function)
req = pytest.FixtureRequest(item, _ispytest=True) req = TopRequest(item, _ispytest=True)
assert req.path == modcol.path assert req.path == modcol.path
assert req.fspath == modcol.fspath # type: ignore[attr-defined] assert req.fspath == modcol.fspath # type: ignore[attr-defined]

View File

@ -291,7 +291,8 @@ class TestParser:
def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
try: try:
encoding = locale.getencoding() # New in Python 3.11, ignores utf-8 mode # New in Python 3.11, ignores utf-8 mode
encoding = locale.getencoding() # type: ignore[attr-defined]
except AttributeError: except AttributeError:
encoding = locale.getpreferredencoding(False) encoding = locale.getpreferredencoding(False)
try: try:

View File

@ -242,8 +242,12 @@ class TestPytestPluginManager:
mod = types.ModuleType("temp") mod = types.ModuleType("temp")
mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"] mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"]
pytestpm.consider_module(mod) pytestpm.consider_module(mod)
assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1" p1 = pytestpm.get_plugin("pytest_p1")
assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2" assert p1 is not None
assert p1.__name__ == "pytest_p1"
p2 = pytestpm.get_plugin("pytest_p2")
assert p2 is not None
assert p2.__name__ == "pytest_p2"
def test_consider_module_import_module( def test_consider_module_import_module(
self, pytester: Pytester, _config_for_test: Config self, pytester: Pytester, _config_for_test: Config
@ -336,6 +340,7 @@ class TestPytestPluginManager:
len2 = len(pytestpm.get_plugins()) len2 = len(pytestpm.get_plugins())
assert len1 == len2 assert len1 == len2
plugin1 = pytestpm.get_plugin("pytest_hello") plugin1 = pytestpm.get_plugin("pytest_hello")
assert plugin1 is not None
assert plugin1.__name__.endswith("pytest_hello") assert plugin1.__name__.endswith("pytest_hello")
plugin2 = pytestpm.get_plugin("pytest_hello") plugin2 = pytestpm.get_plugin("pytest_hello")
assert plugin2 is plugin1 assert plugin2 is plugin1
@ -351,6 +356,7 @@ class TestPytestPluginManager:
pluginname = "pkg.plug" pluginname = "pkg.plug"
pytestpm.import_plugin(pluginname) pytestpm.import_plugin(pluginname)
mod = pytestpm.get_plugin("pkg.plug") mod = pytestpm.get_plugin("pkg.plug")
assert mod is not None
assert mod.x == 3 assert mod.x == 3
def test_consider_conftest_deps( def test_consider_conftest_deps(

View File

@ -12,6 +12,7 @@ envlist =
pypy3 pypy3
py38-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib} py38-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib}
doctesting doctesting
doctesting-coverage
plugins plugins
py38-freeze py38-freeze
docs docs