Merge branch 'main' into Improvement-remove-prune_dependency_tree
This commit is contained in:
commit
8b75507215
|
@ -1,44 +1,58 @@
|
|||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
# These tags are protected, see:
|
||||
# https://github.com/pytest-dev/pytest/settings/tag_protection
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version'
|
||||
required: true
|
||||
default: '1.2.3'
|
||||
|
||||
|
||||
# Set permissions at the job level.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
|
||||
timeout-minutes: 10
|
||||
|
||||
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
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
needs: [build]
|
||||
needs: [package]
|
||||
runs-on: ubuntu-latest
|
||||
environment: deploy
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- 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:
|
||||
|
||||
|
@ -55,12 +69,12 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
- name: Install tox
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
|
|
@ -27,7 +27,19 @@ concurrency:
|
|||
permissions: {}
|
||||
|
||||
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:
|
||||
needs: [package]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
|
@ -58,7 +70,6 @@ jobs:
|
|||
"macos-py310",
|
||||
"macos-py312",
|
||||
|
||||
"docs",
|
||||
"doctesting",
|
||||
"plugins",
|
||||
]
|
||||
|
@ -149,10 +160,6 @@ jobs:
|
|||
os: ubuntu-latest
|
||||
tox_env: "plugins"
|
||||
|
||||
- name: "docs"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "docs"
|
||||
- name: "doctesting"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
|
@ -165,6 +172,12 @@ jobs:
|
|||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- name: Set up Python ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
|
@ -178,11 +191,13 @@ jobs:
|
|||
|
||||
- name: Test without 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
|
||||
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
|
||||
if: "matrix.use_coverage"
|
||||
|
@ -196,10 +211,3 @@ jobs:
|
|||
fail_ci_if_error: true
|
||||
files: ./coverage.xml
|
||||
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
|
||||
|
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.8"
|
||||
python-version: "3.11"
|
||||
cache: pip
|
||||
- name: requests-cache
|
||||
uses: actions/cache@v3
|
||||
|
|
|
@ -5,7 +5,7 @@ repos:
|
|||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: 1.15.0
|
||||
rev: 1.16.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==23.7.0]
|
||||
|
@ -56,7 +56,7 @@ repos:
|
|||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.4.1
|
||||
rev: v1.5.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
|
|
2
AUTHORS
2
AUTHORS
|
@ -170,6 +170,7 @@ Ian Lesperance
|
|||
Ilya Konstantinov
|
||||
Ionuț Turturică
|
||||
Isaac Virshup
|
||||
Israel Fruchter
|
||||
Itxaso Aizpurua
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
|
@ -336,6 +337,7 @@ Samuele Pedroni
|
|||
Sanket Duthade
|
||||
Sankt Petersbug
|
||||
Saravanan Padmanaban
|
||||
Sean Malloy
|
||||
Segev Finer
|
||||
Serhii Mozghovyi
|
||||
Seth Junot
|
||||
|
|
|
@ -50,7 +50,7 @@ Fix bugs
|
|||
--------
|
||||
|
||||
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.
|
||||
|
||||
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going
|
||||
|
|
|
@ -133,14 +133,11 @@ Releasing
|
|||
|
||||
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
|
||||
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
|
||||
#. After all tests pass and the PR has been approved, trigger the ``deploy`` job
|
||||
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml.
|
||||
|
||||
git fetch upstream
|
||||
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
|
||||
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>`_.
|
||||
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
|
||||
and tag the repository.
|
||||
|
||||
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -1 +0,0 @@
|
|||
Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1,2 @@
|
|||
Corrected the spelling of ``Config.ArgsSource.INVOCATION_DIR``.
|
||||
The previous spelling ``INCOVATION_DIR`` remains as an alias.
|
|
@ -0,0 +1 @@
|
|||
pluggy>=1.3.0 is now required. This adds typing to :class:`~pytest.PytestPluginManager`.
|
|
@ -0,0 +1 @@
|
|||
Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.
|
|
@ -0,0 +1 @@
|
|||
Removes unhelpful error message from assertion rewrite mechanism when exceptions raised in __iter__ methods, and instead treats them as un-iterable.
|
|
@ -6,6 +6,7 @@ Release announcements
|
|||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.4.1
|
||||
release-7.4.0
|
||||
release-7.3.2
|
||||
release-7.3.1
|
||||
|
|
|
@ -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
|
|
@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
cachedir: .pytest_cache
|
||||
rootdir: /home/sweet/project
|
||||
collected 0 items
|
||||
cache -- .../_pytest/cacheprovider.py:528
|
||||
cache -- .../_pytest/cacheprovider.py:532
|
||||
Return a cache object that can persist state between testing sessions.
|
||||
|
||||
cache.get(key, default)
|
||||
|
|
|
@ -28,6 +28,23 @@ with advance notice in the **Deprecations** section of releases.
|
|||
|
||||
.. 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)
|
||||
=========================
|
||||
|
||||
|
|
|
@ -554,13 +554,13 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
|||
E AssertionError: assert False
|
||||
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 '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>()
|
||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>()
|
||||
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0006>()
|
||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef0029>()
|
||||
|
||||
failure_demo.py:235: AssertionError
|
||||
_____________________ TestMoreErrors.test_global_func ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002a>
|
||||
|
||||
def test_global_func(self):
|
||||
> 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
|
||||
_______________________ TestMoreErrors.test_instance _______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
||||
|
||||
def test_instance(self):
|
||||
self.x = 6 * 7
|
||||
> assert self.x != 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
|
||||
_______________________ TestMoreErrors.test_compare ________________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
||||
|
||||
def test_compare(self):
|
||||
> 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
|
||||
_____________________ TestMoreErrors.test_try_finally ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
||||
|
||||
def test_try_finally(self):
|
||||
x = 1
|
||||
|
@ -603,7 +603,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
|||
failure_demo.py:250: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_single_line ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002e>
|
||||
|
||||
def test_single_line(self):
|
||||
class A:
|
||||
|
@ -618,7 +618,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
|||
failure_demo.py:261: AssertionError
|
||||
____________________ TestCustomAssertMsg.test_multiline ____________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
||||
|
||||
def test_multiline(self):
|
||||
class A:
|
||||
|
@ -637,7 +637,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
|||
failure_demo.py:268: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_custom_repr ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
||||
|
||||
def test_custom_repr(self):
|
||||
class JSON:
|
||||
|
|
|
@ -34,7 +34,7 @@ a function/method call.
|
|||
|
||||
**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
|
||||
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
|
||||
something should be green, we'd say ``assert thing == "green"``.
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
|||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.4.0
|
||||
pytest 7.4.1
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
|
|
@ -176,14 +176,21 @@ with more recent files coming first.
|
|||
Behavior when no tests failed in the last run
|
||||
---------------------------------------------
|
||||
|
||||
When no tests failed in the last run, or when no cached ``lastfailed`` data was
|
||||
found, ``pytest`` can be configured either to run all of the tests or no tests,
|
||||
using the ``--last-failed-no-failures`` option, which takes one of the following values:
|
||||
The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
|
||||
Determines whether to execute tests when there are no previously (known)
|
||||
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
|
||||
|
||||
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior)
|
||||
pytest --last-failed --last-failed-no-failures none # run no tests and exit
|
||||
pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior)
|
||||
pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully
|
||||
|
||||
The new config.cache object
|
||||
--------------------------------
|
||||
|
|
|
@ -51,8 +51,8 @@ Running this would result in a passed test except for the last
|
|||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
p.write_text(CONTENT, encoding="utf-8")
|
||||
assert p.read_text(encoding="utf-8") == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -237,7 +237,7 @@ pytest.mark.xfail
|
|||
|
||||
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
|
||||
:param condition:
|
||||
|
@ -249,10 +249,10 @@ Marks a test function as *expected to fail*.
|
|||
:keyword Type[Exception] raises:
|
||||
Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test.
|
||||
: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).
|
||||
: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
|
||||
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
|
||||
|
@ -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
|
||||
a new release of a library fixes a known bug).
|
||||
|
||||
Defaults to :confval:`xfail_strict`, which is ``False`` by default.
|
||||
|
||||
|
||||
Custom marks
|
||||
~~~~~~~~~~~~
|
||||
|
@ -978,10 +980,10 @@ TestShortLogReport
|
|||
.. autoclass:: pytest.TestShortLogReport()
|
||||
: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
|
||||
~~~~~
|
||||
|
@ -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
|
||||
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
|
||||
collection unless ``‑‑collect‑in‑virtualenv`` is given. Note also that
|
||||
``norecursedirs`` takes precedence over ``‑‑collect‑in‑virtualenv``; e.g. if
|
||||
collection unless ``--collect-in-virtualenv`` is given. Note also that
|
||||
``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 *must* override ``norecursedirs`` in addition to using the
|
||||
``‑‑collect‑in‑virtualenv`` flag.
|
||||
``--collect-in-virtualenv`` flag.
|
||||
|
||||
|
||||
.. confval:: python_classes
|
||||
|
@ -1890,8 +1892,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
|||
tests. Optional argument: glob (default: '*').
|
||||
--cache-clear Remove all cache contents at start of test run
|
||||
--lfnf={all,none}, --last-failed-no-failures={all,none}
|
||||
Which tests to run with no previously (known)
|
||||
failures
|
||||
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.
|
||||
--sw, --stepwise Exit on test failure and continue from last failing
|
||||
test next time
|
||||
--sw-skip, --stepwise-skip
|
||||
|
|
|
@ -17,7 +17,12 @@ python_classes = ["Test", "Acceptance"]
|
|||
python_functions = ["test"]
|
||||
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
|
||||
testpaths = ["testing"]
|
||||
norecursedirs = ["testing/example_scripts"]
|
||||
norecursedirs = [
|
||||
"testing/example_scripts",
|
||||
".*",
|
||||
"build",
|
||||
"dist",
|
||||
]
|
||||
xfail_strict = true
|
||||
filterwarnings = [
|
||||
"error",
|
||||
|
|
|
@ -44,6 +44,7 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
|||
)
|
||||
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||
"logassert",
|
||||
"nuts",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ py_modules = py
|
|||
install_requires =
|
||||
iniconfig
|
||||
packaging
|
||||
pluggy>=1.2.0,<2.0
|
||||
pluggy>=1.3.0,<2.0
|
||||
colorama;sys_platform=="win32"
|
||||
exceptiongroup>=1.0.0rc8;python_version<"3.11"
|
||||
tomli>=1.0.0;python_version<"3.11"
|
||||
|
|
|
@ -132,7 +132,7 @@ def isiterable(obj: Any) -> bool:
|
|||
try:
|
||||
iter(obj)
|
||||
return not istext(obj)
|
||||
except TypeError:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
@ -499,7 +499,11 @@ def pytest_addoption(parser: Parser) -> None:
|
|||
dest="last_failed_no_failures",
|
||||
choices=("all", "none"),
|
||||
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.",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -314,15 +314,24 @@ def safe_isclass(obj: object) -> bool:
|
|||
|
||||
|
||||
def get_user_id() -> int | None:
|
||||
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
|
||||
# win32 does not have a getuid() function.
|
||||
# On Emscripten, getuid() is a stub that always returns 0.
|
||||
if sys.platform in ("win32", "emscripten"):
|
||||
"""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.
|
||||
# Emscripten has a return 0 stub.
|
||||
return None
|
||||
# getuid shouldn't fail, but cpython defines such a case.
|
||||
# Let's hope for the best.
|
||||
uid = os.getuid()
|
||||
return uid if uid != -1 else None
|
||||
else:
|
||||
# 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()
|
||||
return uid if uid != ERROR else None
|
||||
|
||||
|
||||
# Perform exhaustiveness checking.
|
||||
|
|
|
@ -38,7 +38,9 @@ from typing import TYPE_CHECKING
|
|||
from typing import Union
|
||||
|
||||
from pluggy import HookimplMarker
|
||||
from pluggy import HookimplOpts
|
||||
from pluggy import HookspecMarker
|
||||
from pluggy import HookspecOpts
|
||||
from pluggy import PluginManager
|
||||
|
||||
import _pytest._code
|
||||
|
@ -440,15 +442,17 @@ class PytestPluginManager(PluginManager):
|
|||
# Used to know when we are importing conftests after the pytest_configure stage.
|
||||
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_",
|
||||
# so we avoid accessing possibly non-readable attributes
|
||||
# (see issue #1073).
|
||||
if not name.startswith("pytest_"):
|
||||
return
|
||||
return None
|
||||
# Ignore names which can not be hooks.
|
||||
if name == "pytest_plugins":
|
||||
return
|
||||
return None
|
||||
|
||||
opts = super().parse_hookimpl_opts(plugin, name)
|
||||
if opts is not None:
|
||||
|
@ -457,18 +461,18 @@ class PytestPluginManager(PluginManager):
|
|||
method = getattr(plugin, name)
|
||||
# Consider only actual functions for hooks (#3775).
|
||||
if not inspect.isroutine(method):
|
||||
return
|
||||
return None
|
||||
# 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")
|
||||
)
|
||||
|
||||
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)
|
||||
if opts is None:
|
||||
method = getattr(module_or_class, name)
|
||||
if name.startswith("pytest_"):
|
||||
opts = _get_legacy_hook_marks(
|
||||
opts = _get_legacy_hook_marks( # type: ignore[assignment]
|
||||
method,
|
||||
"spec",
|
||||
("firstresult", "historic"),
|
||||
|
@ -953,7 +957,8 @@ class Config:
|
|||
#: Command line arguments.
|
||||
ARGS = enum.auto()
|
||||
#: Invocation directory.
|
||||
INCOVATION_DIR = enum.auto()
|
||||
INVOCATION_DIR = enum.auto()
|
||||
INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
|
||||
#: 'testpaths' configuration value.
|
||||
TESTPATHS = enum.auto()
|
||||
|
||||
|
@ -1066,9 +1071,10 @@ class Config:
|
|||
fin()
|
||||
|
||||
def get_terminal_writer(self) -> TerminalWriter:
|
||||
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
|
||||
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
|
||||
"terminalreporter"
|
||||
)
|
||||
assert terminalreporter is not None
|
||||
return terminalreporter._tw
|
||||
|
||||
def pytest_cmdline_parse(
|
||||
|
@ -1278,7 +1284,7 @@ class Config:
|
|||
else:
|
||||
result = []
|
||||
if not result:
|
||||
source = Config.ArgsSource.INCOVATION_DIR
|
||||
source = Config.ArgsSource.INVOCATION_DIR
|
||||
result = [str(invocation_dir)]
|
||||
return result, source
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ from _pytest.compat import safe_getattr
|
|||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.fixtures import TopRequest
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.outcomes import OutcomeException
|
||||
|
@ -261,7 +261,7 @@ class DoctestItem(Item):
|
|||
self.runner = runner
|
||||
self.dtest = dtest
|
||||
self.obj = None
|
||||
self.fixture_request: Optional[FixtureRequest] = None
|
||||
self.fixture_request: Optional[TopRequest] = None
|
||||
|
||||
@classmethod
|
||||
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."""
|
||||
|
||||
def func() -> None:
|
||||
|
@ -582,7 +582,7 @@ def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
|
|||
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
|
||||
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()
|
||||
return fixture_request
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import abc
|
||||
import dataclasses
|
||||
import functools
|
||||
import inspect
|
||||
|
@ -313,26 +314,32 @@ class FuncFixtureInfo:
|
|||
name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]]
|
||||
|
||||
|
||||
class FixtureRequest:
|
||||
"""A request for a fixture from a test or fixture function.
|
||||
class FixtureRequest(abc.ABC):
|
||||
"""The type of the ``request`` fixture.
|
||||
|
||||
A request object gives access to the requesting test context and has
|
||||
an optional ``param`` attribute in case the fixture is parametrized
|
||||
indirectly.
|
||||
A request object gives access to the requesting test context and has a
|
||||
``param`` attribute in case the fixture is parametrized.
|
||||
"""
|
||||
|
||||
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)
|
||||
#: Fixture for which this request is being performed.
|
||||
self.fixturename: Optional[str] = None
|
||||
self._pyfuncitem = pyfuncitem
|
||||
self._fixturemanager = pyfuncitem.session._fixturemanager
|
||||
self._scope = Scope.Function
|
||||
self.fixturename: Final = fixturename
|
||||
self._pyfuncitem: Final = pyfuncitem
|
||||
# The FixtureDefs for each fixture name requested by this item.
|
||||
# Starts from the statically-known fixturedefs resolved during
|
||||
# collection. Dynamically requested fixtures (using
|
||||
# `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
|
||||
# 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.
|
||||
|
@ -342,10 +349,10 @@ class FixtureRequest:
|
|||
# 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
|
||||
# last to first.
|
||||
self._arg2index: Dict[str, int] = {}
|
||||
self._arg2index: Final = arg2index
|
||||
# The evaluated argnames so far, mapping to the FixtureDef they resolved
|
||||
# to.
|
||||
self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
|
||||
self._fixture_defs: Final = fixture_defs
|
||||
# Notes on the type of `param`:
|
||||
# -`request.param` is only defined in parametrized fixtures, and will raise
|
||||
# AttributeError otherwise. Python typing has no notion of "undefined", so
|
||||
|
@ -356,6 +363,15 @@ class FixtureRequest:
|
|||
# for now just using Any.
|
||||
self.param: Any
|
||||
|
||||
@property
|
||||
def _fixturemanager(self) -> "FixtureManager":
|
||||
return self._pyfuncitem.session._fixturemanager
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def _scope(self) -> Scope:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def scope(self) -> _ScopeName:
|
||||
"""Scope string, one of "function", "class", "module", "package", "session"."""
|
||||
|
@ -369,25 +385,10 @@ class FixtureRequest:
|
|||
return result
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def node(self):
|
||||
"""Underlying collection node (depends on current request scope)."""
|
||||
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:
|
||||
# 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
|
||||
raise NotImplementedError()
|
||||
|
||||
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
|
||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||
|
@ -473,11 +474,11 @@ class FixtureRequest:
|
|||
"""Pytest session object."""
|
||||
return self._pyfuncitem.session # type: ignore[no-any-return]
|
||||
|
||||
@abc.abstractmethod
|
||||
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."""
|
||||
# XXX usually this method is shadowed by fixturedef specific ones.
|
||||
self.node.addfinalizer(finalizer)
|
||||
raise NotImplementedError()
|
||||
|
||||
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
||||
"""Apply a marker to a single test function invocation.
|
||||
|
@ -498,13 +499,6 @@ class FixtureRequest:
|
|||
"""
|
||||
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:
|
||||
"""Dynamically run a named fixture function.
|
||||
|
||||
|
@ -638,6 +632,98 @@ class FixtureRequest:
|
|||
finalizer = functools.partial(fixturedef.finish, request=subrequest)
|
||||
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(
|
||||
self,
|
||||
argname: str,
|
||||
|
@ -672,44 +758,7 @@ class FixtureRequest:
|
|||
)
|
||||
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:
|
||||
"""Add finalizer/teardown function to be called without arguments after
|
||||
the last test within the requesting test context finished execution."""
|
||||
self._fixturedef.addfinalizer(finalizer)
|
||||
|
||||
def _schedule_finalizers(
|
||||
|
|
|
@ -12,6 +12,7 @@ from _pytest.config import Config
|
|||
from _pytest.config import ExitCode
|
||||
from _pytest.config import PrintHelp
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
|
||||
class HelpAction(Action):
|
||||
|
@ -161,7 +162,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
|||
def showhelp(config: Config) -> None:
|
||||
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.write(config._parser.optparser.format_help())
|
||||
tw.line()
|
||||
|
|
|
@ -502,6 +502,10 @@ class LogXML:
|
|||
# Local hack to handle xdist report order.
|
||||
workernode = getattr(report, "node", None)
|
||||
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:
|
||||
reporter.finalize()
|
||||
|
||||
|
@ -599,9 +603,6 @@ class LogXML:
|
|||
reporter = self._opentestcase(report)
|
||||
reporter.write_captured_output(report)
|
||||
|
||||
for propname, propvalue in report.user_properties:
|
||||
reporter.add_property(propname, str(propvalue))
|
||||
|
||||
self.finalize(report)
|
||||
report_wid = getattr(report, "worker_id", None)
|
||||
report_ii = getattr(report, "item_index", None)
|
||||
|
|
|
@ -659,6 +659,8 @@ class LoggingPlugin:
|
|||
)
|
||||
if self._log_cli_enabled():
|
||||
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")
|
||||
# if capturemanager plugin is disabled, live logging still works.
|
||||
self.log_cli_handler: Union[
|
||||
|
|
|
@ -461,7 +461,9 @@ if TYPE_CHECKING:
|
|||
*conditions: Union[str, bool],
|
||||
reason: str = ...,
|
||||
run: bool = ...,
|
||||
raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ...,
|
||||
raises: Union[
|
||||
None, Type[BaseException], Tuple[Type[BaseException], ...]
|
||||
] = ...,
|
||||
strict: bool = ...,
|
||||
) -> MarkDecorator:
|
||||
...
|
||||
|
|
|
@ -233,6 +233,9 @@ def xfail(reason: str = "") -> NoReturn:
|
|||
|
||||
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:
|
||||
The message to show the user as reason for the xfail.
|
||||
|
||||
|
|
|
@ -751,7 +751,7 @@ class Pytester:
|
|||
|
||||
def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
|
||||
"""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)
|
||||
return reprec
|
||||
|
||||
|
@ -829,7 +829,7 @@ class Pytester:
|
|||
return self._makefile(ext, args, kwargs)
|
||||
|
||||
def makeconftest(self, source: str) -> Path:
|
||||
"""Write a contest.py file.
|
||||
"""Write a conftest.py file.
|
||||
|
||||
:param source: The contents.
|
||||
:returns: The conftest.py file.
|
||||
|
|
|
@ -1817,7 +1817,7 @@ class Function(PyobjMixin, nodes.Item):
|
|||
|
||||
def _initrequest(self) -> None:
|
||||
self.funcargs: Dict[str, object] = {}
|
||||
self._request = fixtures.FixtureRequest(self, _ispytest=True)
|
||||
self._request = fixtures.TopRequest(self, _ispytest=True)
|
||||
|
||||
@property
|
||||
def function(self):
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# PYTHON_ARGCOMPLETE_OK
|
||||
"""pytest: unit and functional testing with Python."""
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from _pytest import __version__
|
||||
from _pytest import version_tuple
|
||||
from _pytest._code import ExceptionInfo
|
||||
|
@ -165,11 +167,12 @@ __all__ = [
|
|||
"yield_fixture",
|
||||
]
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
if name == "Instance":
|
||||
# The import emits a deprecation warning.
|
||||
from _pytest.python import Instance
|
||||
def __getattr__(name: str) -> object:
|
||||
if name == "Instance":
|
||||
# The import emits a deprecation warning.
|
||||
from _pytest.python import Instance
|
||||
|
||||
return Instance
|
||||
raise AttributeError(f"module {__name__} has no attribute {name}")
|
||||
return Instance
|
||||
raise AttributeError(f"module {__name__} has no attribute {name}")
|
||||
|
|
|
@ -15,7 +15,7 @@ from py.path import local
|
|||
def ignore_encoding_warning():
|
||||
with warnings.catch_warnings():
|
||||
with contextlib.suppress(NameError): # new in 3.10
|
||||
warnings.simplefilter("ignore", EncodingWarning)
|
||||
warnings.simplefilter("ignore", EncodingWarning) # type: ignore [name-defined] # noqa: F821
|
||||
yield
|
||||
|
||||
|
||||
|
|
|
@ -272,7 +272,7 @@ def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
|
|||
pytest.PytestDeprecationWarning,
|
||||
match=re.escape("The pytest.Instance collector type is deprecated"),
|
||||
):
|
||||
pytest.Instance
|
||||
pytest.Instance # type:ignore[attr-defined]
|
||||
|
||||
with pytest.warns(
|
||||
pytest.PytestDeprecationWarning,
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
anyio[curio,trio]==3.7.1
|
||||
anyio[curio,trio]==4.0.0
|
||||
django==4.2.4
|
||||
pytest-asyncio==0.21.1
|
||||
pytest-bdd==6.1.1
|
||||
pytest-cov==4.1.0
|
||||
pytest-django==4.5.2
|
||||
pytest-flakes==4.0.5
|
||||
pytest-html==3.2.0
|
||||
pytest-html==4.0.0
|
||||
pytest-mock==3.11.1
|
||||
pytest-rerunfailures==12.0
|
||||
pytest-sugar==0.9.7
|
||||
pytest-trio==0.7.0
|
||||
pytest-twisted==1.14.0
|
||||
twisted==22.8.0
|
||||
twisted==23.8.0
|
||||
pytest-xvfb==3.0.0
|
||||
|
|
|
@ -4,10 +4,9 @@ import textwrap
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from _pytest import fixtures
|
||||
from _pytest.compat import getfuncargnames
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.fixtures import TopRequest
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest.pytester import get_public_names
|
||||
from _pytest.pytester import Pytester
|
||||
|
@ -659,7 +658,7 @@ class TestRequestBasic:
|
|||
"""
|
||||
)
|
||||
assert isinstance(item, Function)
|
||||
req = fixtures.FixtureRequest(item, _ispytest=True)
|
||||
req = TopRequest(item, _ispytest=True)
|
||||
assert req.function == item.obj
|
||||
assert req.keywords == item.keywords
|
||||
assert hasattr(req.module, "test_func")
|
||||
|
@ -701,9 +700,7 @@ class TestRequestBasic:
|
|||
(item1,) = pytester.genitems([modcol])
|
||||
assert isinstance(item1, Function)
|
||||
assert item1.name == "test_method"
|
||||
arg2fixturedefs = fixtures.FixtureRequest(
|
||||
item1, _ispytest=True
|
||||
)._arg2fixturedefs
|
||||
arg2fixturedefs = TopRequest(item1, _ispytest=True)._arg2fixturedefs
|
||||
assert len(arg2fixturedefs) == 1
|
||||
assert arg2fixturedefs["something"][0].argname == "something"
|
||||
|
||||
|
@ -969,7 +966,7 @@ class TestRequestBasic:
|
|||
modcol = pytester.getmodulecol("def test_somefunc(): pass")
|
||||
(item,) = pytester.genitems([modcol])
|
||||
assert isinstance(item, Function)
|
||||
req = fixtures.FixtureRequest(item, _ispytest=True)
|
||||
req = TopRequest(item, _ispytest=True)
|
||||
assert req.path == modcol.path
|
||||
|
||||
def test_request_fixturenames(self, pytester: Pytester) -> None:
|
||||
|
@ -1128,7 +1125,7 @@ class TestRequestMarking:
|
|||
"""
|
||||
)
|
||||
assert isinstance(item1, Function)
|
||||
req1 = fixtures.FixtureRequest(item1, _ispytest=True)
|
||||
req1 = TopRequest(item1, _ispytest=True)
|
||||
assert "xfail" not in item1.keywords
|
||||
req1.applymarker(pytest.mark.xfail)
|
||||
assert "xfail" in item1.keywords
|
||||
|
@ -4036,7 +4033,7 @@ class TestScopeOrdering:
|
|||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
assert isinstance(items[0], Function)
|
||||
request = FixtureRequest(items[0], _ispytest=True)
|
||||
request = TopRequest(items[0], _ispytest=True)
|
||||
assert request.fixturenames == "m1 f1".split()
|
||||
|
||||
def test_func_closure_with_native_fixtures(
|
||||
|
@ -4085,7 +4082,7 @@ class TestScopeOrdering:
|
|||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
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
|
||||
assert (
|
||||
request.fixturenames
|
||||
|
@ -4113,7 +4110,7 @@ class TestScopeOrdering:
|
|||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
assert isinstance(items[0], Function)
|
||||
request = FixtureRequest(items[0], _ispytest=True)
|
||||
request = TopRequest(items[0], _ispytest=True)
|
||||
assert request.fixturenames == "m1 f1".split()
|
||||
|
||||
def test_func_closure_scopes_reordered(self, pytester: Pytester) -> None:
|
||||
|
@ -4147,7 +4144,7 @@ class TestScopeOrdering:
|
|||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
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()
|
||||
|
||||
def test_func_closure_same_scope_closer_root_first(
|
||||
|
@ -4190,7 +4187,7 @@ class TestScopeOrdering:
|
|||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
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()
|
||||
|
||||
def test_func_closure_all_scopes_complex(self, pytester: Pytester) -> None:
|
||||
|
@ -4235,7 +4232,7 @@ class TestScopeOrdering:
|
|||
)
|
||||
items, _ = pytester.inline_genitems()
|
||||
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()
|
||||
|
||||
def test_multiple_packages(self, pytester: Pytester) -> None:
|
||||
|
|
|
@ -685,6 +685,25 @@ class TestAssertionRewrite:
|
|||
assert msg is not None
|
||||
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 f() -> None:
|
||||
assert "%test" == "test" # type: ignore[comparison-overlap]
|
||||
|
|
|
@ -507,6 +507,24 @@ class TestParseIni:
|
|||
result = pytester.runpytest("--foo=1")
|
||||
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:
|
||||
def test_parsing_again_fails(self, pytester: Pytester) -> None:
|
||||
|
|
|
@ -1228,6 +1228,36 @@ def test_record_property(pytester: Pytester, run_and_parse: RunAndParse) -> None
|
|||
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(
|
||||
pytester: Pytester, run_and_parse: RunAndParse
|
||||
) -> None:
|
||||
|
|
|
@ -2,6 +2,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.fixtures import TopRequest
|
||||
from _pytest.legacypath import TempdirFactory
|
||||
from _pytest.legacypath import Testdir
|
||||
|
||||
|
@ -91,7 +92,7 @@ def test_fixturerequest_getmodulepath(pytester: pytest.Pytester) -> None:
|
|||
modcol = pytester.getmodulecol("def test_somefunc(): pass")
|
||||
(item,) = pytester.genitems([modcol])
|
||||
assert isinstance(item, pytest.Function)
|
||||
req = pytest.FixtureRequest(item, _ispytest=True)
|
||||
req = TopRequest(item, _ispytest=True)
|
||||
assert req.path == modcol.path
|
||||
assert req.fspath == modcol.fspath # type: ignore[attr-defined]
|
||||
|
||||
|
|
|
@ -291,7 +291,8 @@ class TestParser:
|
|||
|
||||
def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
|
||||
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:
|
||||
encoding = locale.getpreferredencoding(False)
|
||||
try:
|
||||
|
|
|
@ -242,8 +242,12 @@ class TestPytestPluginManager:
|
|||
mod = types.ModuleType("temp")
|
||||
mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"]
|
||||
pytestpm.consider_module(mod)
|
||||
assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
|
||||
assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2"
|
||||
p1 = pytestpm.get_plugin("pytest_p1")
|
||||
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(
|
||||
self, pytester: Pytester, _config_for_test: Config
|
||||
|
@ -336,6 +340,7 @@ class TestPytestPluginManager:
|
|||
len2 = len(pytestpm.get_plugins())
|
||||
assert len1 == len2
|
||||
plugin1 = pytestpm.get_plugin("pytest_hello")
|
||||
assert plugin1 is not None
|
||||
assert plugin1.__name__.endswith("pytest_hello")
|
||||
plugin2 = pytestpm.get_plugin("pytest_hello")
|
||||
assert plugin2 is plugin1
|
||||
|
@ -351,6 +356,7 @@ class TestPytestPluginManager:
|
|||
pluginname = "pkg.plug"
|
||||
pytestpm.import_plugin(pluginname)
|
||||
mod = pytestpm.get_plugin("pkg.plug")
|
||||
assert mod is not None
|
||||
assert mod.x == 3
|
||||
|
||||
def test_consider_conftest_deps(
|
||||
|
|
Loading…
Reference in New Issue