Merge branch 'main' into Improvement-remove-prune_dependency_tree
This commit is contained in:
commit
c72507cad9
|
@ -36,8 +36,10 @@ jobs:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Download Package
|
- name: Download Package
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -21,7 +21,7 @@ repos:
|
||||||
exclude: _pytest/(debugging|hookspec).py
|
exclude: _pytest/(debugging|hookspec).py
|
||||||
language_version: python3
|
language_version: python3
|
||||||
- repo: https://github.com/PyCQA/autoflake
|
- repo: https://github.com/PyCQA/autoflake
|
||||||
rev: v2.2.0
|
rev: v2.2.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: autoflake
|
- id: autoflake
|
||||||
name: autoflake
|
name: autoflake
|
||||||
|
|
3
AUTHORS
3
AUTHORS
|
@ -143,6 +143,7 @@ Feng Ma
|
||||||
Florian Bruhin
|
Florian Bruhin
|
||||||
Florian Dahlitz
|
Florian Dahlitz
|
||||||
Floris Bruynooghe
|
Floris Bruynooghe
|
||||||
|
Fraser Stark
|
||||||
Gabriel Landau
|
Gabriel Landau
|
||||||
Gabriel Reis
|
Gabriel Reis
|
||||||
Garvit Shubham
|
Garvit Shubham
|
||||||
|
@ -348,6 +349,7 @@ Simon Holesch
|
||||||
Simon Kerr
|
Simon Kerr
|
||||||
Skylar Downes
|
Skylar Downes
|
||||||
Srinivas Reddy Thatiparthy
|
Srinivas Reddy Thatiparthy
|
||||||
|
Stefaan Lippens
|
||||||
Stefan Farmbauer
|
Stefan Farmbauer
|
||||||
Stefan Scherfke
|
Stefan Scherfke
|
||||||
Stefan Zimmermann
|
Stefan Zimmermann
|
||||||
|
@ -381,6 +383,7 @@ Tor Colvin
|
||||||
Trevor Bekolay
|
Trevor Bekolay
|
||||||
Tushar Sadhwani
|
Tushar Sadhwani
|
||||||
Tyler Goodlet
|
Tyler Goodlet
|
||||||
|
Tyler Smart
|
||||||
Tzu-ping Chung
|
Tzu-ping Chung
|
||||||
Vasily Kuznetsov
|
Vasily Kuznetsov
|
||||||
Victor Maryama
|
Victor Maryama
|
||||||
|
|
|
@ -134,7 +134,8 @@ 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, trigger the ``deploy`` job
|
#. 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.
|
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
|
||||||
|
as source.
|
||||||
|
|
||||||
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
|
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
|
||||||
and tag the repository.
|
and tag the repository.
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Test functions returning a value other than None will now issue a :class:`pytest.PytestWarning` instead of :class:`pytest.PytestRemovedIn8Warning`, meaning this will stay a warning instead of becoming an error in the future.
|
|
@ -1,2 +1 @@
|
||||||
Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27
|
Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27 <https://devguide.python.org/versions/>`__.
|
||||||
<https://devguide.python.org/versions/>`__.
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.
|
|
|
@ -6,6 +6,7 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-7.4.2
|
||||||
release-7.4.1
|
release-7.4.1
|
||||||
release-7.4.0
|
release-7.4.0
|
||||||
release-7.3.2
|
release-7.3.2
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
pytest-7.4.2
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.4.2 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
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -105,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert captured.out == "hello\n"
|
assert captured.out == "hello\n"
|
||||||
|
|
||||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
|
doctest_namespace [session scope] -- .../_pytest/doctest.py:757
|
||||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||||
namespace of doctests.
|
namespace of doctests.
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,31 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 7.4.2 (2023-09-07)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#11237 <https://github.com/pytest-dev/pytest/issues/11237>`_: Fix doctest collection of `functools.cached_property` objects.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11306 <https://github.com/pytest-dev/pytest/issues/11306>`_: Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11367 <https://github.com/pytest-dev/pytest/issues/11367>`_: Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11394 <https://github.com/pytest-dev/pytest/issues/11394>`_: Fixed crash when parsing long command line arguments that might be interpreted as files.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- `#11391 <https://github.com/pytest-dev/pytest/issues/11391>`_: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing.
|
||||||
|
|
||||||
|
|
||||||
pytest 7.4.1 (2023-09-02)
|
pytest 7.4.1 (2023-09-02)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|
|
@ -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 0xdeadbeef0006>()
|
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>()
|
||||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef0029>()
|
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>()
|
||||||
|
|
||||||
failure_demo.py:235: AssertionError
|
failure_demo.py:235: AssertionError
|
||||||
_____________________ TestMoreErrors.test_global_func ______________________
|
_____________________ TestMoreErrors.test_global_func ______________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002a>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
||||||
|
|
||||||
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 0xdeadbeef002b>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
||||||
|
|
||||||
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 0xdeadbeef002b>.x
|
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>.x
|
||||||
|
|
||||||
failure_demo.py:242: AssertionError
|
failure_demo.py:242: AssertionError
|
||||||
_______________________ TestMoreErrors.test_compare ________________________
|
_______________________ TestMoreErrors.test_compare ________________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
||||||
|
|
||||||
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 0xdeadbeef002d>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e>
|
||||||
|
|
||||||
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 0xdeadbeef002e>
|
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
||||||
|
|
||||||
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 0xdeadbeef002f>
|
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
||||||
|
|
||||||
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 0xdeadbeef0030>
|
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031>
|
||||||
|
|
||||||
def test_custom_repr(self):
|
def test_custom_repr(self):
|
||||||
class JSON:
|
class JSON:
|
||||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 7.4.1
|
pytest 7.4.2
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
.. sidebar:: Next Open Trainings
|
.. sidebar:: Next Open Trainings
|
||||||
|
|
||||||
- `pytest: Professionelles Testen (nicht nur) für Python <https://workshoptage.ch/workshops/2023/pytest-professionelles-testen-nicht-nur-fuer-python-2/>`_, at `Workshoptage 2023 <https://workshoptage.ch/>`_, **September 5th**, `OST <https://www.ost.ch/en>`_ Campus **Rapperswil, Switzerland**
|
|
||||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
|
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
|
||||||
|
|
||||||
Also see :doc:`previous talks and blogposts <talks>`.
|
Also see :doc:`previous talks and blogposts <talks>`.
|
||||||
|
|
|
@ -3,14 +3,26 @@
|
||||||
|
|
||||||
.. _plugin-list:
|
.. _plugin-list:
|
||||||
|
|
||||||
Plugin List
|
Pytest Plugin List
|
||||||
===========
|
==================
|
||||||
|
|
||||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||||
automatically together with a manually-maintained list in `the source
|
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||||
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
|
||||||
Packages classified as inactive are excluded.
|
Packages classified as inactive are excluded.
|
||||||
|
|
||||||
|
For detailed insights into how this list is generated,
|
||||||
|
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Please be aware that this list is not a curated collection of projects
|
||||||
|
and does not undergo a systematic review process.
|
||||||
|
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
|
||||||
|
|
||||||
|
Do not presume any endorsement from the ``pytest`` project or its developers,
|
||||||
|
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
|
||||||
|
|
||||||
|
|
||||||
.. The following conditional uses a different format for this list when
|
.. The following conditional uses a different format for this list when
|
||||||
creating a PDF, because otherwise the table gets far too wide for the
|
creating a PDF, because otherwise the table gets far too wide for the
|
||||||
page.
|
page.
|
||||||
|
|
|
@ -31,10 +31,16 @@ class InvalidFeatureRelease(Exception):
|
||||||
SLUG = "pytest-dev/pytest"
|
SLUG = "pytest-dev/pytest"
|
||||||
|
|
||||||
PR_BODY = """\
|
PR_BODY = """\
|
||||||
Created automatically from manual trigger.
|
Created by the [prepare release pr](https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml)
|
||||||
|
workflow.
|
||||||
|
|
||||||
Once all builds pass and it has been **approved** by one or more maintainers, the build
|
Once all builds pass and it has been **approved** by one or more maintainers,
|
||||||
can be released by pushing a tag `{version}` to this repository.
|
start the [deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters:
|
||||||
|
|
||||||
|
* `Use workflow from`: `release-{version}`.
|
||||||
|
* `Release version`: `{version}`.
|
||||||
|
|
||||||
|
After the `deploy` workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,26 @@ FILE_HEAD = r"""
|
||||||
|
|
||||||
.. _plugin-list:
|
.. _plugin-list:
|
||||||
|
|
||||||
Plugin List
|
Pytest Plugin List
|
||||||
===========
|
==================
|
||||||
|
|
||||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||||
automatically together with a manually-maintained list in `the source
|
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||||
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
|
||||||
Packages classified as inactive are excluded.
|
Packages classified as inactive are excluded.
|
||||||
|
|
||||||
|
For detailed insights into how this list is generated,
|
||||||
|
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Please be aware that this list is not a curated collection of projects
|
||||||
|
and does not undergo a systematic review process.
|
||||||
|
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
|
||||||
|
|
||||||
|
Do not presume any endorsement from the ``pytest`` project or its developers,
|
||||||
|
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
|
||||||
|
|
||||||
|
|
||||||
.. The following conditional uses a different format for this list when
|
.. The following conditional uses a different format for this list when
|
||||||
creating a PDF, because otherwise the table gets far too wide for the
|
creating a PDF, because otherwise the table gets far too wide for the
|
||||||
page.
|
page.
|
||||||
|
|
|
@ -59,6 +59,7 @@ from _pytest.pathlib import bestrelpath
|
||||||
from _pytest.pathlib import import_path
|
from _pytest.pathlib import import_path
|
||||||
from _pytest.pathlib import ImportMode
|
from _pytest.pathlib import ImportMode
|
||||||
from _pytest.pathlib import resolve_package_path
|
from _pytest.pathlib import resolve_package_path
|
||||||
|
from _pytest.pathlib import safe_exists
|
||||||
from _pytest.stash import Stash
|
from _pytest.stash import Stash
|
||||||
from _pytest.warning_types import PytestConfigWarning
|
from _pytest.warning_types import PytestConfigWarning
|
||||||
from _pytest.warning_types import warn_explicit_for
|
from _pytest.warning_types import warn_explicit_for
|
||||||
|
@ -562,12 +563,8 @@ class PytestPluginManager(PluginManager):
|
||||||
anchor = absolutepath(current / path)
|
anchor = absolutepath(current / path)
|
||||||
|
|
||||||
# Ensure we do not break if what appears to be an anchor
|
# Ensure we do not break if what appears to be an anchor
|
||||||
# is in fact a very long option (#10169).
|
# is in fact a very long option (#10169, #11394).
|
||||||
try:
|
if safe_exists(anchor):
|
||||||
anchor_exists = anchor.exists()
|
|
||||||
except OSError: # pragma: no cover
|
|
||||||
anchor_exists = False
|
|
||||||
if anchor_exists:
|
|
||||||
self._try_load_conftest(anchor, importmode, rootpath)
|
self._try_load_conftest(anchor, importmode, rootpath)
|
||||||
foundanchor = True
|
foundanchor = True
|
||||||
if not foundanchor:
|
if not foundanchor:
|
||||||
|
|
|
@ -15,6 +15,7 @@ from .exceptions import UsageError
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import commonpath
|
from _pytest.pathlib import commonpath
|
||||||
|
from _pytest.pathlib import safe_exists
|
||||||
|
|
||||||
|
|
||||||
def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
|
def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
|
||||||
|
@ -147,14 +148,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
|
||||||
return path
|
return path
|
||||||
return path.parent
|
return path.parent
|
||||||
|
|
||||||
def safe_exists(path: Path) -> bool:
|
|
||||||
# This can throw on paths that contain characters unrepresentable at the OS level,
|
|
||||||
# or with invalid syntax on Windows (https://bugs.python.org/issue35306)
|
|
||||||
try:
|
|
||||||
return path.exists()
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# These look like paths but may not exist
|
# These look like paths but may not exist
|
||||||
possible_paths = (
|
possible_paths = (
|
||||||
absolutepath(get_file_part_from_node_id(arg))
|
absolutepath(get_file_part_from_node_id(arg))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""Discover and run doctests in modules and test files."""
|
"""Discover and run doctests in modules and test files."""
|
||||||
import bdb
|
import bdb
|
||||||
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
@ -254,14 +255,20 @@ class DoctestItem(Item):
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
parent: "Union[DoctestTextfile, DoctestModule]",
|
parent: "Union[DoctestTextfile, DoctestModule]",
|
||||||
runner: Optional["doctest.DocTestRunner"] = None,
|
runner: "doctest.DocTestRunner",
|
||||||
dtest: Optional["doctest.DocTest"] = None,
|
dtest: "doctest.DocTest",
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(name, parent)
|
super().__init__(name, parent)
|
||||||
self.runner = runner
|
self.runner = runner
|
||||||
self.dtest = dtest
|
self.dtest = dtest
|
||||||
|
|
||||||
|
# Stuff needed for fixture support.
|
||||||
self.obj = None
|
self.obj = None
|
||||||
self.fixture_request: Optional[TopRequest] = None
|
fm = self.session._fixturemanager
|
||||||
|
fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None)
|
||||||
|
self._fixtureinfo = fixtureinfo
|
||||||
|
self.fixturenames = fixtureinfo.names_closure
|
||||||
|
self._initrequest()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent( # type: ignore
|
def from_parent( # type: ignore
|
||||||
|
@ -276,19 +283,18 @@ class DoctestItem(Item):
|
||||||
"""The public named constructor."""
|
"""The public named constructor."""
|
||||||
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||||
|
|
||||||
|
def _initrequest(self) -> None:
|
||||||
|
self.funcargs: Dict[str, object] = {}
|
||||||
|
self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type]
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
if self.dtest is not None:
|
self._request._fillfixtures()
|
||||||
self.fixture_request = _setup_fixtures(self)
|
globs = dict(getfixture=self._request.getfixturevalue)
|
||||||
globs = dict(getfixture=self.fixture_request.getfixturevalue)
|
for name, value in self._request.getfixturevalue("doctest_namespace").items():
|
||||||
for name, value in self.fixture_request.getfixturevalue(
|
|
||||||
"doctest_namespace"
|
|
||||||
).items():
|
|
||||||
globs[name] = value
|
globs[name] = value
|
||||||
self.dtest.globs.update(globs)
|
self.dtest.globs.update(globs)
|
||||||
|
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
assert self.dtest is not None
|
|
||||||
assert self.runner is not None
|
|
||||||
_check_all_skipped(self.dtest)
|
_check_all_skipped(self.dtest)
|
||||||
self._disable_output_capturing_for_darwin()
|
self._disable_output_capturing_for_darwin()
|
||||||
failures: List["doctest.DocTestFailure"] = []
|
failures: List["doctest.DocTestFailure"] = []
|
||||||
|
@ -375,7 +381,6 @@ class DoctestItem(Item):
|
||||||
return ReprFailDoctest(reprlocation_lines)
|
return ReprFailDoctest(reprlocation_lines)
|
||||||
|
|
||||||
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
|
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
|
||||||
assert self.dtest is not None
|
|
||||||
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
|
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
|
||||||
|
|
||||||
|
|
||||||
|
@ -395,8 +400,8 @@ def _get_flag_lookup() -> Dict[str, int]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_optionflags(parent):
|
def get_optionflags(config: Config) -> int:
|
||||||
optionflags_str = parent.config.getini("doctest_optionflags")
|
optionflags_str = config.getini("doctest_optionflags")
|
||||||
flag_lookup_table = _get_flag_lookup()
|
flag_lookup_table = _get_flag_lookup()
|
||||||
flag_acc = 0
|
flag_acc = 0
|
||||||
for flag in optionflags_str:
|
for flag in optionflags_str:
|
||||||
|
@ -404,8 +409,8 @@ def get_optionflags(parent):
|
||||||
return flag_acc
|
return flag_acc
|
||||||
|
|
||||||
|
|
||||||
def _get_continue_on_failure(config):
|
def _get_continue_on_failure(config: Config) -> bool:
|
||||||
continue_on_failure = config.getvalue("doctest_continue_on_failure")
|
continue_on_failure: bool = config.getvalue("doctest_continue_on_failure")
|
||||||
if continue_on_failure:
|
if continue_on_failure:
|
||||||
# We need to turn off this if we use pdb since we should stop at
|
# We need to turn off this if we use pdb since we should stop at
|
||||||
# the first failure.
|
# the first failure.
|
||||||
|
@ -428,7 +433,7 @@ class DoctestTextfile(Module):
|
||||||
name = self.path.name
|
name = self.path.name
|
||||||
globs = {"__name__": "__main__"}
|
globs = {"__name__": "__main__"}
|
||||||
|
|
||||||
optionflags = get_optionflags(self)
|
optionflags = get_optionflags(self.config)
|
||||||
|
|
||||||
runner = _get_runner(
|
runner = _get_runner(
|
||||||
verbose=False,
|
verbose=False,
|
||||||
|
@ -536,6 +541,23 @@ class DoctestModule(Module):
|
||||||
tests, obj, name, module, source_lines, globs, seen
|
tests, obj, name, module, source_lines, globs, seen
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if sys.version_info < (3, 13):
|
||||||
|
|
||||||
|
def _from_module(self, module, object):
|
||||||
|
"""`cached_property` objects are never considered a part
|
||||||
|
of the 'current module'. As such they are skipped by doctest.
|
||||||
|
Here we override `_from_module` to check the underlying
|
||||||
|
function instead. https://github.com/python/cpython/issues/107995
|
||||||
|
"""
|
||||||
|
if isinstance(object, functools.cached_property):
|
||||||
|
object = object.func
|
||||||
|
|
||||||
|
# Type ignored because this is a private function.
|
||||||
|
return super()._from_module(module, object) # type: ignore[misc]
|
||||||
|
|
||||||
|
else: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
if self.path.name == "conftest.py":
|
if self.path.name == "conftest.py":
|
||||||
module = self.config.pluginmanager._importconftest(
|
module = self.config.pluginmanager._importconftest(
|
||||||
self.path,
|
self.path,
|
||||||
|
@ -556,7 +578,7 @@ class DoctestModule(Module):
|
||||||
raise
|
raise
|
||||||
# Uses internal doctest module parsing mechanism.
|
# Uses internal doctest module parsing mechanism.
|
||||||
finder = MockAwareDocTestFinder()
|
finder = MockAwareDocTestFinder()
|
||||||
optionflags = get_optionflags(self)
|
optionflags = get_optionflags(self.config)
|
||||||
runner = _get_runner(
|
runner = _get_runner(
|
||||||
verbose=False,
|
verbose=False,
|
||||||
optionflags=optionflags,
|
optionflags=optionflags,
|
||||||
|
@ -571,22 +593,6 @@ class DoctestModule(Module):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _setup_fixtures(doctest_item: DoctestItem) -> TopRequest:
|
|
||||||
"""Used by DoctestTextfile and DoctestItem to setup fixture information."""
|
|
||||||
|
|
||||||
def func() -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
doctest_item.funcargs = {} # type: ignore[attr-defined]
|
|
||||||
fm = doctest_item.session._fixturemanager
|
|
||||||
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
|
|
||||||
node=doctest_item, func=func, cls=None, funcargs=False
|
|
||||||
)
|
|
||||||
fixture_request = TopRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
|
|
||||||
fixture_request._fillfixtures()
|
|
||||||
return fixture_request
|
|
||||||
|
|
||||||
|
|
||||||
def _init_checker_class() -> Type["doctest.OutputChecker"]:
|
def _init_checker_class() -> Type["doctest.OutputChecker"]:
|
||||||
import doctest
|
import doctest
|
||||||
import re
|
import re
|
||||||
|
|
|
@ -8,6 +8,7 @@ from collections import defaultdict
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import AbstractSet
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
@ -210,16 +211,14 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
|
||||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
|
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
|
||||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
|
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
|
||||||
for scope in HIGH_SCOPES:
|
for scope in HIGH_SCOPES:
|
||||||
d: Dict[nodes.Item, Dict[FixtureArgKey, None]] = {}
|
scoped_argkeys_cache = argkeys_cache[scope] = {}
|
||||||
argkeys_cache[scope] = d
|
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque)
|
||||||
item_d: Dict[FixtureArgKey, Deque[nodes.Item]] = defaultdict(deque)
|
|
||||||
items_by_argkey[scope] = item_d
|
|
||||||
for item in items:
|
for item in items:
|
||||||
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
|
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
|
||||||
if keys:
|
if keys:
|
||||||
d[item] = keys
|
scoped_argkeys_cache[item] = keys
|
||||||
for key in keys:
|
for key in keys:
|
||||||
item_d[key].append(item)
|
scoped_items_by_argkey[key].append(item)
|
||||||
items_dict = dict.fromkeys(items, None)
|
items_dict = dict.fromkeys(items, None)
|
||||||
return list(
|
return list(
|
||||||
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
|
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
|
||||||
|
@ -380,7 +379,7 @@ class FixtureRequest(abc.ABC):
|
||||||
@property
|
@property
|
||||||
def fixturenames(self) -> List[str]:
|
def fixturenames(self) -> List[str]:
|
||||||
"""Names of all active fixtures in this request."""
|
"""Names of all active fixtures in this request."""
|
||||||
result = list(self._pyfuncitem._fixtureinfo.names_closure)
|
result = list(self._pyfuncitem.fixturenames)
|
||||||
result.extend(set(self._fixture_defs).difference(result))
|
result.extend(set(self._fixture_defs).difference(result))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -660,8 +659,7 @@ class TopRequest(FixtureRequest):
|
||||||
|
|
||||||
def _fillfixtures(self) -> None:
|
def _fillfixtures(self) -> None:
|
||||||
item = self._pyfuncitem
|
item = self._pyfuncitem
|
||||||
fixturenames = getattr(item, "fixturenames", self.fixturenames)
|
for argname in item.fixturenames:
|
||||||
for argname in fixturenames:
|
|
||||||
if argname not in item.funcargs:
|
if argname not in item.funcargs:
|
||||||
item.funcargs[argname] = self.getfixturevalue(argname)
|
item.funcargs[argname] = self.getfixturevalue(argname)
|
||||||
|
|
||||||
|
@ -767,7 +765,10 @@ class SubRequest(FixtureRequest):
|
||||||
# If the executing fixturedef was not explicitly requested in the argument list (via
|
# If the executing fixturedef was not explicitly requested in the argument list (via
|
||||||
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished
|
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished
|
||||||
# first.
|
# first.
|
||||||
if fixturedef.argname not in self.fixturenames:
|
if (
|
||||||
|
fixturedef.argname not in self._fixture_defs
|
||||||
|
and fixturedef.argname not in self._pyfuncitem.fixturenames
|
||||||
|
):
|
||||||
fixturedef.addfinalizer(
|
fixturedef.addfinalizer(
|
||||||
functools.partial(self._fixturedef.finish, request=self)
|
functools.partial(self._fixturedef.finish, request=self)
|
||||||
)
|
)
|
||||||
|
@ -1374,7 +1375,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
|
def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
|
||||||
"""Return all direct parametrization arguments of a node, so we don't
|
"""Return all direct parametrization arguments of a node, so we don't
|
||||||
mistake them for fixtures.
|
mistake them for fixtures.
|
||||||
|
|
||||||
|
@ -1383,21 +1384,20 @@ def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
|
||||||
These things are done later as well when dealing with parametrization
|
These things are done later as well when dealing with parametrization
|
||||||
so this could be improved.
|
so this could be improved.
|
||||||
"""
|
"""
|
||||||
parametrize_argnames: List[str] = []
|
parametrize_argnames: Set[str] = set()
|
||||||
for marker in node.iter_markers(name="parametrize"):
|
for marker in node.iter_markers(name="parametrize"):
|
||||||
if not marker.kwargs.get("indirect", False):
|
if not marker.kwargs.get("indirect", False):
|
||||||
p_argnames, _ = ParameterSet._parse_parametrize_args(
|
p_argnames, _ = ParameterSet._parse_parametrize_args(
|
||||||
*marker.args, **marker.kwargs
|
*marker.args, **marker.kwargs
|
||||||
)
|
)
|
||||||
parametrize_argnames.extend(p_argnames)
|
parametrize_argnames.update(p_argnames)
|
||||||
|
|
||||||
return parametrize_argnames
|
return parametrize_argnames
|
||||||
|
|
||||||
|
|
||||||
def deduplicate_names(seq: Iterable[str]) -> Tuple[str, ...]:
|
def deduplicate_names(*seqs: Iterable[str]) -> Tuple[str, ...]:
|
||||||
"""De-duplicate the sequence of names while keeping the original order."""
|
"""De-duplicate the sequence of names while keeping the original order."""
|
||||||
# Ideally we would use a set, but it does not preserve insertion order.
|
# Ideally we would use a set, but it does not preserve insertion order.
|
||||||
return tuple(dict.fromkeys(seq))
|
return tuple(dict.fromkeys(name for seq in seqs for name in seq))
|
||||||
|
|
||||||
|
|
||||||
class FixtureManager:
|
class FixtureManager:
|
||||||
|
@ -1452,13 +1452,12 @@ class FixtureManager:
|
||||||
def getfixtureinfo(
|
def getfixtureinfo(
|
||||||
self,
|
self,
|
||||||
node: nodes.Item,
|
node: nodes.Item,
|
||||||
func: Callable[..., object],
|
func: Optional[Callable[..., object]],
|
||||||
cls: Optional[type],
|
cls: Optional[type],
|
||||||
funcargs: bool = True,
|
|
||||||
) -> FuncFixtureInfo:
|
) -> FuncFixtureInfo:
|
||||||
"""Calculate the :class:`FuncFixtureInfo` for an item.
|
"""Calculate the :class:`FuncFixtureInfo` for an item.
|
||||||
|
|
||||||
If ``funcargs`` is false, or if the item sets an attribute
|
If ``func`` is None, or if the item sets an attribute
|
||||||
``nofuncargs = True``, then ``func`` is not examined at all.
|
``nofuncargs = True``, then ``func`` is not examined at all.
|
||||||
|
|
||||||
:param node:
|
:param node:
|
||||||
|
@ -1467,34 +1466,26 @@ class FixtureManager:
|
||||||
The item's function.
|
The item's function.
|
||||||
:param cls:
|
:param cls:
|
||||||
If the function is a method, the method's class.
|
If the function is a method, the method's class.
|
||||||
:param funcargs:
|
|
||||||
Whether to look into func's parameters as fixture requests.
|
|
||||||
"""
|
"""
|
||||||
if funcargs and not getattr(node, "nofuncargs", False):
|
if func is not None and not getattr(node, "nofuncargs", False):
|
||||||
argnames = getfuncargnames(func, name=node.name, cls=cls)
|
argnames = getfuncargnames(func, name=node.name, cls=cls)
|
||||||
else:
|
else:
|
||||||
argnames = ()
|
argnames = ()
|
||||||
|
usefixturesnames = self._getusefixturesnames(node)
|
||||||
|
autousenames = self._getautousenames(node.nodeid)
|
||||||
|
initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
|
||||||
|
|
||||||
usefixtures = tuple(
|
direct_parametrize_args = _get_direct_parametrize_args(node)
|
||||||
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
|
|
||||||
)
|
|
||||||
initialnames = deduplicate_names(
|
|
||||||
tuple(self._getautousenames(node.nodeid)) + usefixtures + argnames
|
|
||||||
)
|
|
||||||
|
|
||||||
names_closure, arg2fixturedefs = self.getfixtureclosure(
|
names_closure, arg2fixturedefs = self.getfixtureclosure(
|
||||||
node,
|
parentnode=node,
|
||||||
initialnames,
|
initialnames=initialnames,
|
||||||
None,
|
arg2fixturedefs=None,
|
||||||
ignore_args=_get_direct_parametrize_args(node),
|
ignore_args=direct_parametrize_args,
|
||||||
)
|
|
||||||
return FuncFixtureInfo(
|
|
||||||
argnames,
|
|
||||||
initialnames,
|
|
||||||
names_closure,
|
|
||||||
arg2fixturedefs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
||||||
|
|
||||||
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
||||||
nodeid = None
|
nodeid = None
|
||||||
try:
|
try:
|
||||||
|
@ -1524,12 +1515,17 @@ class FixtureManager:
|
||||||
if basenames:
|
if basenames:
|
||||||
yield from basenames
|
yield from basenames
|
||||||
|
|
||||||
|
def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]:
|
||||||
|
"""Return the names of usefixtures fixtures applicable to node."""
|
||||||
|
for mark in node.iter_markers(name="usefixtures"):
|
||||||
|
yield from mark.args
|
||||||
|
|
||||||
def getfixtureclosure(
|
def getfixtureclosure(
|
||||||
self,
|
self,
|
||||||
parentnode: nodes.Node,
|
parentnode: nodes.Node,
|
||||||
initialnames: Tuple[str, ...],
|
initialnames: Tuple[str, ...],
|
||||||
arg2fixturedefs: Union[Dict[str, Sequence[FixtureDef[Any]]], None],
|
arg2fixturedefs: Union[Dict[str, Sequence[FixtureDef[Any]]], None],
|
||||||
ignore_args: Sequence[str] = (),
|
ignore_args: AbstractSet[str],
|
||||||
) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
|
) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
|
||||||
# Collect the closure of all fixtures, starting with the given
|
# Collect the closure of all fixtures, starting with the given
|
||||||
# initialnames containing function arguments, `usefixture` markers
|
# initialnames containing function arguments, `usefixture` markers
|
||||||
|
@ -1539,12 +1535,12 @@ class FixtureManager:
|
||||||
# not have to re-discover fixturedefs again for each fixturename
|
# not have to re-discover fixturedefs again for each fixturename
|
||||||
# (discovering matching fixtures for a given name/node is expensive).
|
# (discovering matching fixtures for a given name/node is expensive).
|
||||||
|
|
||||||
fixturenames_closure = initialnames
|
|
||||||
|
|
||||||
if arg2fixturedefs is None:
|
if arg2fixturedefs is None:
|
||||||
arg2fixturedefs = {}
|
arg2fixturedefs = {}
|
||||||
lastlen = -1
|
|
||||||
parentid = parentnode.nodeid
|
parentid = parentnode.nodeid
|
||||||
|
fixturenames_closure = list(initialnames)
|
||||||
|
|
||||||
|
lastlen = -1
|
||||||
while lastlen != len(fixturenames_closure):
|
while lastlen != len(fixturenames_closure):
|
||||||
lastlen = len(fixturenames_closure)
|
lastlen = len(fixturenames_closure)
|
||||||
for argname in fixturenames_closure:
|
for argname in fixturenames_closure:
|
||||||
|
@ -1557,9 +1553,9 @@ class FixtureManager:
|
||||||
else:
|
else:
|
||||||
fixturedefs = arg2fixturedefs[argname]
|
fixturedefs = arg2fixturedefs[argname]
|
||||||
if fixturedefs and not isinstance(fixturedefs[-1], IdentityFixture):
|
if fixturedefs and not isinstance(fixturedefs[-1], IdentityFixture):
|
||||||
fixturenames_closure = deduplicate_names(
|
for arg in fixturedefs[-1].argnames:
|
||||||
fixturenames_closure + arg2fixturedefs[argname][-1].argnames
|
if arg not in fixturenames_closure:
|
||||||
)
|
fixturenames_closure.append(arg)
|
||||||
|
|
||||||
def sort_by_scope(arg_name: str) -> Scope:
|
def sort_by_scope(arg_name: str) -> Scope:
|
||||||
try:
|
try:
|
||||||
|
@ -1569,10 +1565,8 @@ class FixtureManager:
|
||||||
else:
|
else:
|
||||||
return fixturedefs[-1]._scope
|
return fixturedefs[-1]._scope
|
||||||
|
|
||||||
return (
|
fixturenames_closure.sort(key=sort_by_scope, reverse=True)
|
||||||
sorted(fixturenames_closure, key=sort_by_scope, reverse=True),
|
return fixturenames_closure, arg2fixturedefs
|
||||||
arg2fixturedefs,
|
|
||||||
)
|
|
||||||
|
|
||||||
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
|
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
|
||||||
"""Generate new tests based on parametrized fixtures used by the given metafunc"""
|
"""Generate new tests based on parametrized fixtures used by the given metafunc"""
|
||||||
|
|
|
@ -36,6 +36,7 @@ from _pytest.outcomes import exit
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import bestrelpath
|
from _pytest.pathlib import bestrelpath
|
||||||
from _pytest.pathlib import fnmatch_ex
|
from _pytest.pathlib import fnmatch_ex
|
||||||
|
from _pytest.pathlib import safe_exists
|
||||||
from _pytest.pathlib import visit
|
from _pytest.pathlib import visit
|
||||||
from _pytest.reports import CollectReport
|
from _pytest.reports import CollectReport
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
|
@ -888,7 +889,7 @@ def resolve_collection_argument(
|
||||||
strpath = search_pypath(strpath)
|
strpath = search_pypath(strpath)
|
||||||
fspath = invocation_path / strpath
|
fspath = invocation_path / strpath
|
||||||
fspath = absolutepath(fspath)
|
fspath = absolutepath(fspath)
|
||||||
if not fspath.exists():
|
if not safe_exists(fspath):
|
||||||
msg = (
|
msg = (
|
||||||
"module or package not found: {arg} (missing __init__.py?)"
|
"module or package not found: {arg} (missing __init__.py?)"
|
||||||
if as_pypath
|
if as_pypath
|
||||||
|
|
|
@ -623,6 +623,10 @@ def module_name_from_path(path: Path, root: Path) -> str:
|
||||||
# Use the parts for the relative path to the root path.
|
# Use the parts for the relative path to the root path.
|
||||||
path_parts = relative_path.parts
|
path_parts = relative_path.parts
|
||||||
|
|
||||||
|
# Module name for packages do not contain the __init__ file.
|
||||||
|
if path_parts[-1] == "__init__":
|
||||||
|
path_parts = path_parts[:-1]
|
||||||
|
|
||||||
return ".".join(path_parts)
|
return ".".join(path_parts)
|
||||||
|
|
||||||
|
|
||||||
|
@ -769,3 +773,13 @@ def bestrelpath(directory: Path, dest: Path) -> str:
|
||||||
# Forward from base to dest.
|
# Forward from base to dest.
|
||||||
*reldest.parts,
|
*reldest.parts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_exists(p: Path) -> bool:
|
||||||
|
"""Like Path.exists(), but account for input arguments that might be too long (#11394)."""
|
||||||
|
try:
|
||||||
|
return p.exists()
|
||||||
|
except (ValueError, OSError):
|
||||||
|
# ValueError: stat: path too long for Windows
|
||||||
|
# OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect
|
||||||
|
return False
|
||||||
|
|
|
@ -475,7 +475,9 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
clscol = self.getparent(Class)
|
clscol = self.getparent(Class)
|
||||||
cls = clscol and clscol.obj or None
|
cls = clscol and clscol.obj or None
|
||||||
|
|
||||||
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
|
definition: FunctionDefinition = FunctionDefinition.from_parent(
|
||||||
|
self, name=name, callobj=funcobj
|
||||||
|
)
|
||||||
fixtureinfo = definition._fixtureinfo
|
fixtureinfo = definition._fixtureinfo
|
||||||
|
|
||||||
metafunc = Metafunc(
|
metafunc = Metafunc(
|
||||||
|
@ -1128,9 +1130,9 @@ class CallSpec2:
|
||||||
# arg name -> arg index.
|
# arg name -> arg index.
|
||||||
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
|
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
|
||||||
# Used for sorting parametrized resources.
|
# Used for sorting parametrized resources.
|
||||||
_arg2scope: Dict[str, Scope] = dataclasses.field(default_factory=dict)
|
_arg2scope: Mapping[str, Scope] = dataclasses.field(default_factory=dict)
|
||||||
# Parts which will be added to the item's name in `[..]` separated by "-".
|
# Parts which will be added to the item's name in `[..]` separated by "-".
|
||||||
_idlist: List[str] = dataclasses.field(default_factory=list)
|
_idlist: Sequence[str] = dataclasses.field(default_factory=tuple)
|
||||||
# Marks which will be applied to the item.
|
# Marks which will be applied to the item.
|
||||||
marks: List[Mark] = dataclasses.field(default_factory=list)
|
marks: List[Mark] = dataclasses.field(default_factory=list)
|
||||||
|
|
||||||
|
@ -1146,7 +1148,7 @@ class CallSpec2:
|
||||||
) -> "CallSpec2":
|
) -> "CallSpec2":
|
||||||
params = self.params.copy()
|
params = self.params.copy()
|
||||||
indices = self.indices.copy()
|
indices = self.indices.copy()
|
||||||
arg2scope = self._arg2scope.copy()
|
arg2scope = dict(self._arg2scope)
|
||||||
for arg, val in zip(argnames, valset):
|
for arg, val in zip(argnames, valset):
|
||||||
if arg in params:
|
if arg in params:
|
||||||
raise ValueError(f"duplicate parametrization of {arg!r}")
|
raise ValueError(f"duplicate parametrization of {arg!r}")
|
||||||
|
@ -1546,12 +1548,8 @@ class Metafunc:
|
||||||
|
|
||||||
def update_dependency_tree(self) -> None:
|
def update_dependency_tree(self) -> None:
|
||||||
definition = self.definition
|
definition = self.definition
|
||||||
(
|
fm = cast(nodes.Node, definition.parent).session._fixturemanager
|
||||||
fixture_closure,
|
fixture_closure, _ = fm.getfixtureclosure(
|
||||||
_,
|
|
||||||
) = cast(
|
|
||||||
nodes.Node, definition.parent
|
|
||||||
).session._fixturemanager.getfixtureclosure(
|
|
||||||
definition,
|
definition,
|
||||||
definition._fixtureinfo.initialnames,
|
definition._fixtureinfo.initialnames,
|
||||||
definition._fixtureinfo.name2fixturedefs,
|
definition._fixtureinfo.name2fixturedefs,
|
||||||
|
@ -1813,9 +1811,8 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
self.keywords.update(keywords)
|
self.keywords.update(keywords)
|
||||||
|
|
||||||
if fixtureinfo is None:
|
if fixtureinfo is None:
|
||||||
fixtureinfo = self.session._fixturemanager.getfixtureinfo(
|
fm = self.session._fixturemanager
|
||||||
self, self.obj, self.cls, funcargs=True
|
fixtureinfo = fm.getfixtureinfo(self, self.obj, self.cls)
|
||||||
)
|
|
||||||
self._fixtureinfo: FuncFixtureInfo = fixtureinfo
|
self._fixtureinfo: FuncFixtureInfo = fixtureinfo
|
||||||
self.fixturenames = fixtureinfo.names_closure
|
self.fixturenames = fixtureinfo.names_closure
|
||||||
self._initrequest()
|
self._initrequest()
|
||||||
|
|
|
@ -61,7 +61,7 @@ class PytestRemovedIn9Warning(PytestDeprecationWarning):
|
||||||
__module__ = "pytest"
|
__module__ = "pytest"
|
||||||
|
|
||||||
|
|
||||||
class PytestReturnNotNoneWarning(PytestRemovedIn8Warning):
|
class PytestReturnNotNoneWarning(PytestWarning):
|
||||||
"""Warning emitted when a test function is returning value other than None."""
|
"""Warning emitted when a test function is returning value other than None."""
|
||||||
|
|
||||||
__module__ = "pytest"
|
__module__ = "pytest"
|
||||||
|
|
|
@ -4704,8 +4704,8 @@ def test_dont_recompute_dependency_tree_if_no_dynamic_parametrize(pytester: Pyte
|
||||||
reprec.assertoutcome(passed=5)
|
reprec.assertoutcome(passed=5)
|
||||||
|
|
||||||
|
|
||||||
def test_deduplicate_names(pytester: Pytester) -> None:
|
def test_deduplicate_names() -> None:
|
||||||
items = deduplicate_names("abacd")
|
items = deduplicate_names("abacd")
|
||||||
assert items == ("a", "b", "c", "d")
|
assert items == ("a", "b", "c", "d")
|
||||||
items = deduplicate_names(items + ("g", "f", "g", "e", "b"))
|
items = deduplicate_names(items, ("g", "f", "g", "e", "b"))
|
||||||
assert items == ("a", "b", "c", "d", "g", "f", "e")
|
assert items == ("a", "b", "c", "d", "g", "f", "e")
|
||||||
|
|
|
@ -482,6 +482,24 @@ class TestDoctests:
|
||||||
reprec = pytester.inline_run(p, "--doctest-modules")
|
reprec = pytester.inline_run(p, "--doctest-modules")
|
||||||
reprec.assertoutcome(failed=1)
|
reprec.assertoutcome(failed=1)
|
||||||
|
|
||||||
|
def test_doctest_cached_property(self, pytester: Pytester):
|
||||||
|
p = pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import functools
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
@functools.cached_property
|
||||||
|
def foo(self):
|
||||||
|
'''
|
||||||
|
>>> assert False, "Tacos!"
|
||||||
|
'''
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest(p, "--doctest-modules")
|
||||||
|
result.assert_outcomes(failed=1)
|
||||||
|
assert "Tacos!" in result.stdout.str()
|
||||||
|
|
||||||
def test_doctestmodule_external_and_issue116(self, pytester: Pytester):
|
def test_doctestmodule_external_and_issue116(self, pytester: Pytester):
|
||||||
p = pytester.mkpydir("hello")
|
p = pytester.mkpydir("hello")
|
||||||
p.joinpath("__init__.py").write_text(
|
p.joinpath("__init__.py").write_text(
|
||||||
|
|
|
@ -262,3 +262,34 @@ def test_module_full_path_without_drive(pytester: Pytester) -> None:
|
||||||
"* 1 passed in *",
|
"* 1 passed in *",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_very_long_cmdline_arg(pytester: Pytester) -> None:
|
||||||
|
"""
|
||||||
|
Regression test for #11394.
|
||||||
|
|
||||||
|
Note: we could not manage to actually reproduce the error with this code, we suspect
|
||||||
|
GitHub runners are configured to support very long paths, however decided to leave
|
||||||
|
the test in place in case this ever regresses in the future.
|
||||||
|
"""
|
||||||
|
pytester.makeconftest(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption("--long-list", dest="long_list", action="store", default="all", help="List of things")
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def specified_feeds(request):
|
||||||
|
list_string = request.config.getoption("--long-list")
|
||||||
|
return list_string.split(',')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_foo(specified_feeds):
|
||||||
|
assert len(specified_feeds) == 100_000
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest("--long-list", ",".join(["helloworld"] * 100_000))
|
||||||
|
result.stdout.fnmatch_lines("* 1 passed *")
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import errno
|
||||||
import os.path
|
import os.path
|
||||||
import pickle
|
import pickle
|
||||||
import sys
|
import sys
|
||||||
|
@ -18,11 +19,13 @@ from _pytest.pathlib import fnmatch_ex
|
||||||
from _pytest.pathlib import get_extended_length_path_str
|
from _pytest.pathlib import get_extended_length_path_str
|
||||||
from _pytest.pathlib import get_lock_path
|
from _pytest.pathlib import get_lock_path
|
||||||
from _pytest.pathlib import import_path
|
from _pytest.pathlib import import_path
|
||||||
|
from _pytest.pathlib import ImportMode
|
||||||
from _pytest.pathlib import ImportPathMismatchError
|
from _pytest.pathlib import ImportPathMismatchError
|
||||||
from _pytest.pathlib import insert_missing_modules
|
from _pytest.pathlib import insert_missing_modules
|
||||||
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
||||||
from _pytest.pathlib import module_name_from_path
|
from _pytest.pathlib import module_name_from_path
|
||||||
from _pytest.pathlib import resolve_package_path
|
from _pytest.pathlib import resolve_package_path
|
||||||
|
from _pytest.pathlib import safe_exists
|
||||||
from _pytest.pathlib import symlink_or_skip
|
from _pytest.pathlib import symlink_or_skip
|
||||||
from _pytest.pathlib import visit
|
from _pytest.pathlib import visit
|
||||||
from _pytest.tmpdir import TempPathFactory
|
from _pytest.tmpdir import TempPathFactory
|
||||||
|
@ -585,6 +588,10 @@ class TestImportLibMode:
|
||||||
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
|
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
|
||||||
assert result == "home.foo.test_foo"
|
assert result == "home.foo.test_foo"
|
||||||
|
|
||||||
|
# Importing __init__.py files should return the package as module name.
|
||||||
|
result = module_name_from_path(tmp_path / "src/app/__init__.py", tmp_path)
|
||||||
|
assert result == "src.app"
|
||||||
|
|
||||||
def test_insert_missing_modules(
|
def test_insert_missing_modules(
|
||||||
self, monkeypatch: MonkeyPatch, tmp_path: Path
|
self, monkeypatch: MonkeyPatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -615,3 +622,72 @@ class TestImportLibMode:
|
||||||
assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
|
assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
|
||||||
assert modules["xxx"].tests is modules["xxx.tests"]
|
assert modules["xxx"].tests is modules["xxx.tests"]
|
||||||
assert modules["xxx.tests"].foo is modules["xxx.tests.foo"]
|
assert modules["xxx.tests"].foo is modules["xxx.tests.foo"]
|
||||||
|
|
||||||
|
def test_importlib_package(self, monkeypatch: MonkeyPatch, tmp_path: Path):
|
||||||
|
"""
|
||||||
|
Importing a package using --importmode=importlib should not import the
|
||||||
|
package's __init__.py file more than once (#11306).
|
||||||
|
"""
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
monkeypatch.syspath_prepend(tmp_path)
|
||||||
|
|
||||||
|
package_name = "importlib_import_package"
|
||||||
|
tmp_path.joinpath(package_name).mkdir()
|
||||||
|
init = tmp_path.joinpath(f"{package_name}/__init__.py")
|
||||||
|
init.write_text(
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
from .singleton import Singleton
|
||||||
|
|
||||||
|
instance = Singleton()
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="ascii",
|
||||||
|
)
|
||||||
|
singleton = tmp_path.joinpath(f"{package_name}/singleton.py")
|
||||||
|
singleton.write_text(
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
class Singleton:
|
||||||
|
INSTANCES = []
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.INSTANCES.append(self)
|
||||||
|
if len(self.INSTANCES) > 1:
|
||||||
|
raise RuntimeError("Already initialized")
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="ascii",
|
||||||
|
)
|
||||||
|
|
||||||
|
mod = import_path(init, root=tmp_path, mode=ImportMode.importlib)
|
||||||
|
assert len(mod.instance.INSTANCES) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_safe_exists(tmp_path: Path) -> None:
|
||||||
|
d = tmp_path.joinpath("some_dir")
|
||||||
|
d.mkdir()
|
||||||
|
assert safe_exists(d) is True
|
||||||
|
|
||||||
|
f = tmp_path.joinpath("some_file")
|
||||||
|
f.touch()
|
||||||
|
assert safe_exists(f) is True
|
||||||
|
|
||||||
|
# Use unittest.mock() as a context manager to have a very narrow
|
||||||
|
# patch lifetime.
|
||||||
|
p = tmp_path.joinpath("some long filename" * 100)
|
||||||
|
with unittest.mock.patch.object(
|
||||||
|
Path,
|
||||||
|
"exists",
|
||||||
|
autospec=True,
|
||||||
|
side_effect=OSError(errno.ENAMETOOLONG, "name too long"),
|
||||||
|
):
|
||||||
|
assert safe_exists(p) is False
|
||||||
|
|
||||||
|
with unittest.mock.patch.object(
|
||||||
|
Path,
|
||||||
|
"exists",
|
||||||
|
autospec=True,
|
||||||
|
side_effect=ValueError("name too long"),
|
||||||
|
):
|
||||||
|
assert safe_exists(p) is False
|
||||||
|
|
Loading…
Reference in New Issue