Merge branch 'main' into Improvement-remove-prune_dependency_tree
This commit is contained in:
commit
c72507cad9
|
@ -36,8 +36,10 @@ jobs:
|
|||
timeout-minutes: 30
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
|
|
|
@ -21,7 +21,7 @@ repos:
|
|||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.2.0
|
||||
rev: v2.2.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
|
|
3
AUTHORS
3
AUTHORS
|
@ -143,6 +143,7 @@ Feng Ma
|
|||
Florian Bruhin
|
||||
Florian Dahlitz
|
||||
Floris Bruynooghe
|
||||
Fraser Stark
|
||||
Gabriel Landau
|
||||
Gabriel Reis
|
||||
Garvit Shubham
|
||||
|
@ -348,6 +349,7 @@ Simon Holesch
|
|||
Simon Kerr
|
||||
Skylar Downes
|
||||
Srinivas Reddy Thatiparthy
|
||||
Stefaan Lippens
|
||||
Stefan Farmbauer
|
||||
Stefan Scherfke
|
||||
Stefan Zimmermann
|
||||
|
@ -381,6 +383,7 @@ Tor Colvin
|
|||
Trevor Bekolay
|
||||
Tushar Sadhwani
|
||||
Tyler Goodlet
|
||||
Tyler Smart
|
||||
Tzu-ping Chung
|
||||
Vasily Kuznetsov
|
||||
Victor Maryama
|
||||
|
|
|
@ -134,7 +134,8 @@ 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, 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
|
||||
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
|
||||
<https://devguide.python.org/versions/>`__.
|
||||
Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27 <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
|
||||
|
||||
|
||||
release-7.4.2
|
||||
release-7.4.1
|
||||
release-7.4.0
|
||||
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()
|
||||
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
|
||||
namespace of doctests.
|
||||
|
||||
|
|
|
@ -28,6 +28,31 @@ with advance notice in the **Deprecations** section of releases.
|
|||
|
||||
.. 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)
|
||||
=========================
|
||||
|
||||
|
|
|
@ -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 0xdeadbeef0006>()
|
||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef0029>()
|
||||
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>()
|
||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>()
|
||||
|
||||
failure_demo.py:235: AssertionError
|
||||
_____________________ TestMoreErrors.test_global_func ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002a>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
||||
|
||||
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 0xdeadbeef002b>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
||||
|
||||
def test_instance(self):
|
||||
self.x = 6 * 7
|
||||
> assert self.x != 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
|
||||
_______________________ TestMoreErrors.test_compare ________________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
||||
|
||||
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 0xdeadbeef002d>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e>
|
||||
|
||||
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 0xdeadbeef002e>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
||||
|
||||
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 0xdeadbeef002f>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
||||
|
||||
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 0xdeadbeef0030>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031>
|
||||
|
||||
def test_custom_repr(self):
|
||||
class JSON:
|
||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
|||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.4.1
|
||||
pytest 7.4.2
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
.. 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**
|
||||
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
|
|
@ -3,14 +3,26 @@
|
|||
|
||||
.. _plugin-list:
|
||||
|
||||
Plugin List
|
||||
===========
|
||||
Pytest Plugin List
|
||||
==================
|
||||
|
||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
||||
automatically together with a manually-maintained list in `the source
|
||||
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||
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
|
||||
creating a PDF, because otherwise the table gets far too wide for the
|
||||
page.
|
||||
|
|
|
@ -31,10 +31,16 @@ class InvalidFeatureRelease(Exception):
|
|||
SLUG = "pytest-dev/pytest"
|
||||
|
||||
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
|
||||
can be released by pushing a tag `{version}` to this repository.
|
||||
Once all builds pass and it has been **approved** by one or more maintainers,
|
||||
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
|
||||
===========
|
||||
Pytest Plugin List
|
||||
==================
|
||||
|
||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
||||
automatically together with a manually-maintained list in `the source
|
||||
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||
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
|
||||
creating a PDF, because otherwise the table gets far too wide for the
|
||||
page.
|
||||
|
|
|
@ -59,6 +59,7 @@ from _pytest.pathlib import bestrelpath
|
|||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportMode
|
||||
from _pytest.pathlib import resolve_package_path
|
||||
from _pytest.pathlib import safe_exists
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import warn_explicit_for
|
||||
|
@ -562,12 +563,8 @@ class PytestPluginManager(PluginManager):
|
|||
anchor = absolutepath(current / path)
|
||||
|
||||
# Ensure we do not break if what appears to be an anchor
|
||||
# is in fact a very long option (#10169).
|
||||
try:
|
||||
anchor_exists = anchor.exists()
|
||||
except OSError: # pragma: no cover
|
||||
anchor_exists = False
|
||||
if anchor_exists:
|
||||
# is in fact a very long option (#10169, #11394).
|
||||
if safe_exists(anchor):
|
||||
self._try_load_conftest(anchor, importmode, rootpath)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
|
|
|
@ -15,6 +15,7 @@ from .exceptions import UsageError
|
|||
from _pytest.outcomes import fail
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import commonpath
|
||||
from _pytest.pathlib import safe_exists
|
||||
|
||||
|
||||
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.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
|
||||
possible_paths = (
|
||||
absolutepath(get_file_part_from_node_id(arg))
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Discover and run doctests in modules and test files."""
|
||||
import bdb
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
|
@ -254,14 +255,20 @@ class DoctestItem(Item):
|
|||
self,
|
||||
name: str,
|
||||
parent: "Union[DoctestTextfile, DoctestModule]",
|
||||
runner: Optional["doctest.DocTestRunner"] = None,
|
||||
dtest: Optional["doctest.DocTest"] = None,
|
||||
runner: "doctest.DocTestRunner",
|
||||
dtest: "doctest.DocTest",
|
||||
) -> None:
|
||||
super().__init__(name, parent)
|
||||
self.runner = runner
|
||||
self.dtest = dtest
|
||||
|
||||
# Stuff needed for fixture support.
|
||||
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
|
||||
def from_parent( # type: ignore
|
||||
|
@ -276,19 +283,18 @@ class DoctestItem(Item):
|
|||
"""The public named constructor."""
|
||||
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:
|
||||
if self.dtest is not None:
|
||||
self.fixture_request = _setup_fixtures(self)
|
||||
globs = dict(getfixture=self.fixture_request.getfixturevalue)
|
||||
for name, value in self.fixture_request.getfixturevalue(
|
||||
"doctest_namespace"
|
||||
).items():
|
||||
globs[name] = value
|
||||
self.dtest.globs.update(globs)
|
||||
self._request._fillfixtures()
|
||||
globs = dict(getfixture=self._request.getfixturevalue)
|
||||
for name, value in self._request.getfixturevalue("doctest_namespace").items():
|
||||
globs[name] = value
|
||||
self.dtest.globs.update(globs)
|
||||
|
||||
def runtest(self) -> None:
|
||||
assert self.dtest is not None
|
||||
assert self.runner is not None
|
||||
_check_all_skipped(self.dtest)
|
||||
self._disable_output_capturing_for_darwin()
|
||||
failures: List["doctest.DocTestFailure"] = []
|
||||
|
@ -375,7 +381,6 @@ class DoctestItem(Item):
|
|||
return ReprFailDoctest(reprlocation_lines)
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -395,8 +400,8 @@ def _get_flag_lookup() -> Dict[str, int]:
|
|||
)
|
||||
|
||||
|
||||
def get_optionflags(parent):
|
||||
optionflags_str = parent.config.getini("doctest_optionflags")
|
||||
def get_optionflags(config: Config) -> int:
|
||||
optionflags_str = config.getini("doctest_optionflags")
|
||||
flag_lookup_table = _get_flag_lookup()
|
||||
flag_acc = 0
|
||||
for flag in optionflags_str:
|
||||
|
@ -404,8 +409,8 @@ def get_optionflags(parent):
|
|||
return flag_acc
|
||||
|
||||
|
||||
def _get_continue_on_failure(config):
|
||||
continue_on_failure = config.getvalue("doctest_continue_on_failure")
|
||||
def _get_continue_on_failure(config: Config) -> bool:
|
||||
continue_on_failure: bool = config.getvalue("doctest_continue_on_failure")
|
||||
if continue_on_failure:
|
||||
# We need to turn off this if we use pdb since we should stop at
|
||||
# the first failure.
|
||||
|
@ -428,7 +433,7 @@ class DoctestTextfile(Module):
|
|||
name = self.path.name
|
||||
globs = {"__name__": "__main__"}
|
||||
|
||||
optionflags = get_optionflags(self)
|
||||
optionflags = get_optionflags(self.config)
|
||||
|
||||
runner = _get_runner(
|
||||
verbose=False,
|
||||
|
@ -536,6 +541,23 @@ class DoctestModule(Module):
|
|||
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":
|
||||
module = self.config.pluginmanager._importconftest(
|
||||
self.path,
|
||||
|
@ -556,7 +578,7 @@ class DoctestModule(Module):
|
|||
raise
|
||||
# Uses internal doctest module parsing mechanism.
|
||||
finder = MockAwareDocTestFinder()
|
||||
optionflags = get_optionflags(self)
|
||||
optionflags = get_optionflags(self.config)
|
||||
runner = _get_runner(
|
||||
verbose=False,
|
||||
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"]:
|
||||
import doctest
|
||||
import re
|
||||
|
|
|
@ -8,6 +8,7 @@ from collections import defaultdict
|
|||
from collections import deque
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import AbstractSet
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
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]]] = {}
|
||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
|
||||
for scope in HIGH_SCOPES:
|
||||
d: Dict[nodes.Item, Dict[FixtureArgKey, None]] = {}
|
||||
argkeys_cache[scope] = d
|
||||
item_d: Dict[FixtureArgKey, Deque[nodes.Item]] = defaultdict(deque)
|
||||
items_by_argkey[scope] = item_d
|
||||
scoped_argkeys_cache = argkeys_cache[scope] = {}
|
||||
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque)
|
||||
for item in items:
|
||||
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
|
||||
if keys:
|
||||
d[item] = keys
|
||||
scoped_argkeys_cache[item] = keys
|
||||
for key in keys:
|
||||
item_d[key].append(item)
|
||||
scoped_items_by_argkey[key].append(item)
|
||||
items_dict = dict.fromkeys(items, None)
|
||||
return list(
|
||||
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
|
||||
|
@ -380,7 +379,7 @@ class FixtureRequest(abc.ABC):
|
|||
@property
|
||||
def fixturenames(self) -> List[str]:
|
||||
"""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))
|
||||
return result
|
||||
|
||||
|
@ -660,8 +659,7 @@ class TopRequest(FixtureRequest):
|
|||
|
||||
def _fillfixtures(self) -> None:
|
||||
item = self._pyfuncitem
|
||||
fixturenames = getattr(item, "fixturenames", self.fixturenames)
|
||||
for argname in fixturenames:
|
||||
for argname in item.fixturenames:
|
||||
if argname not in item.funcargs:
|
||||
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
|
||||
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished
|
||||
# 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(
|
||||
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
|
||||
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
|
||||
so this could be improved.
|
||||
"""
|
||||
parametrize_argnames: List[str] = []
|
||||
parametrize_argnames: Set[str] = set()
|
||||
for marker in node.iter_markers(name="parametrize"):
|
||||
if not marker.kwargs.get("indirect", False):
|
||||
p_argnames, _ = ParameterSet._parse_parametrize_args(
|
||||
*marker.args, **marker.kwargs
|
||||
)
|
||||
parametrize_argnames.extend(p_argnames)
|
||||
|
||||
parametrize_argnames.update(p_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."""
|
||||
# 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:
|
||||
|
@ -1452,13 +1452,12 @@ class FixtureManager:
|
|||
def getfixtureinfo(
|
||||
self,
|
||||
node: nodes.Item,
|
||||
func: Callable[..., object],
|
||||
func: Optional[Callable[..., object]],
|
||||
cls: Optional[type],
|
||||
funcargs: bool = True,
|
||||
) -> FuncFixtureInfo:
|
||||
"""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.
|
||||
|
||||
:param node:
|
||||
|
@ -1467,34 +1466,26 @@ class FixtureManager:
|
|||
The item's function.
|
||||
:param cls:
|
||||
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)
|
||||
else:
|
||||
argnames = ()
|
||||
usefixturesnames = self._getusefixturesnames(node)
|
||||
autousenames = self._getautousenames(node.nodeid)
|
||||
initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
|
||||
|
||||
usefixtures = tuple(
|
||||
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
|
||||
)
|
||||
initialnames = deduplicate_names(
|
||||
tuple(self._getautousenames(node.nodeid)) + usefixtures + argnames
|
||||
)
|
||||
direct_parametrize_args = _get_direct_parametrize_args(node)
|
||||
|
||||
names_closure, arg2fixturedefs = self.getfixtureclosure(
|
||||
node,
|
||||
initialnames,
|
||||
None,
|
||||
ignore_args=_get_direct_parametrize_args(node),
|
||||
)
|
||||
return FuncFixtureInfo(
|
||||
argnames,
|
||||
initialnames,
|
||||
names_closure,
|
||||
arg2fixturedefs,
|
||||
parentnode=node,
|
||||
initialnames=initialnames,
|
||||
arg2fixturedefs=None,
|
||||
ignore_args=direct_parametrize_args,
|
||||
)
|
||||
|
||||
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
||||
|
||||
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
||||
nodeid = None
|
||||
try:
|
||||
|
@ -1524,12 +1515,17 @@ class FixtureManager:
|
|||
if 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(
|
||||
self,
|
||||
parentnode: nodes.Node,
|
||||
initialnames: Tuple[str, ...],
|
||||
arg2fixturedefs: Union[Dict[str, Sequence[FixtureDef[Any]]], None],
|
||||
ignore_args: Sequence[str] = (),
|
||||
ignore_args: AbstractSet[str],
|
||||
) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
|
||||
# Collect the closure of all fixtures, starting with the given
|
||||
# initialnames containing function arguments, `usefixture` markers
|
||||
|
@ -1539,12 +1535,12 @@ class FixtureManager:
|
|||
# not have to re-discover fixturedefs again for each fixturename
|
||||
# (discovering matching fixtures for a given name/node is expensive).
|
||||
|
||||
fixturenames_closure = initialnames
|
||||
|
||||
if arg2fixturedefs is None:
|
||||
arg2fixturedefs = {}
|
||||
lastlen = -1
|
||||
parentid = parentnode.nodeid
|
||||
fixturenames_closure = list(initialnames)
|
||||
|
||||
lastlen = -1
|
||||
while lastlen != len(fixturenames_closure):
|
||||
lastlen = len(fixturenames_closure)
|
||||
for argname in fixturenames_closure:
|
||||
|
@ -1557,9 +1553,9 @@ class FixtureManager:
|
|||
else:
|
||||
fixturedefs = arg2fixturedefs[argname]
|
||||
if fixturedefs and not isinstance(fixturedefs[-1], IdentityFixture):
|
||||
fixturenames_closure = deduplicate_names(
|
||||
fixturenames_closure + arg2fixturedefs[argname][-1].argnames
|
||||
)
|
||||
for arg in fixturedefs[-1].argnames:
|
||||
if arg not in fixturenames_closure:
|
||||
fixturenames_closure.append(arg)
|
||||
|
||||
def sort_by_scope(arg_name: str) -> Scope:
|
||||
try:
|
||||
|
@ -1569,10 +1565,8 @@ class FixtureManager:
|
|||
else:
|
||||
return fixturedefs[-1]._scope
|
||||
|
||||
return (
|
||||
sorted(fixturenames_closure, key=sort_by_scope, reverse=True),
|
||||
arg2fixturedefs,
|
||||
)
|
||||
fixturenames_closure.sort(key=sort_by_scope, reverse=True)
|
||||
return fixturenames_closure, arg2fixturedefs
|
||||
|
||||
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
|
||||
"""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 bestrelpath
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import safe_exists
|
||||
from _pytest.pathlib import visit
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
|
@ -888,7 +889,7 @@ def resolve_collection_argument(
|
|||
strpath = search_pypath(strpath)
|
||||
fspath = invocation_path / strpath
|
||||
fspath = absolutepath(fspath)
|
||||
if not fspath.exists():
|
||||
if not safe_exists(fspath):
|
||||
msg = (
|
||||
"module or package not found: {arg} (missing __init__.py?)"
|
||||
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.
|
||||
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)
|
||||
|
||||
|
||||
|
@ -769,3 +773,13 @@ def bestrelpath(directory: Path, dest: Path) -> str:
|
|||
# Forward from base to dest.
|
||||
*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)
|
||||
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
|
||||
|
||||
metafunc = Metafunc(
|
||||
|
@ -1128,9 +1130,9 @@ class CallSpec2:
|
|||
# arg name -> arg index.
|
||||
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
|
||||
# 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 "-".
|
||||
_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: List[Mark] = dataclasses.field(default_factory=list)
|
||||
|
||||
|
@ -1146,7 +1148,7 @@ class CallSpec2:
|
|||
) -> "CallSpec2":
|
||||
params = self.params.copy()
|
||||
indices = self.indices.copy()
|
||||
arg2scope = self._arg2scope.copy()
|
||||
arg2scope = dict(self._arg2scope)
|
||||
for arg, val in zip(argnames, valset):
|
||||
if arg in params:
|
||||
raise ValueError(f"duplicate parametrization of {arg!r}")
|
||||
|
@ -1546,12 +1548,8 @@ class Metafunc:
|
|||
|
||||
def update_dependency_tree(self) -> None:
|
||||
definition = self.definition
|
||||
(
|
||||
fixture_closure,
|
||||
_,
|
||||
) = cast(
|
||||
nodes.Node, definition.parent
|
||||
).session._fixturemanager.getfixtureclosure(
|
||||
fm = cast(nodes.Node, definition.parent).session._fixturemanager
|
||||
fixture_closure, _ = fm.getfixtureclosure(
|
||||
definition,
|
||||
definition._fixtureinfo.initialnames,
|
||||
definition._fixtureinfo.name2fixturedefs,
|
||||
|
@ -1813,9 +1811,8 @@ class Function(PyobjMixin, nodes.Item):
|
|||
self.keywords.update(keywords)
|
||||
|
||||
if fixtureinfo is None:
|
||||
fixtureinfo = self.session._fixturemanager.getfixtureinfo(
|
||||
self, self.obj, self.cls, funcargs=True
|
||||
)
|
||||
fm = self.session._fixturemanager
|
||||
fixtureinfo = fm.getfixtureinfo(self, self.obj, self.cls)
|
||||
self._fixtureinfo: FuncFixtureInfo = fixtureinfo
|
||||
self.fixturenames = fixtureinfo.names_closure
|
||||
self._initrequest()
|
||||
|
|
|
@ -61,7 +61,7 @@ class PytestRemovedIn9Warning(PytestDeprecationWarning):
|
|||
__module__ = "pytest"
|
||||
|
||||
|
||||
class PytestReturnNotNoneWarning(PytestRemovedIn8Warning):
|
||||
class PytestReturnNotNoneWarning(PytestWarning):
|
||||
"""Warning emitted when a test function is returning value other than None."""
|
||||
|
||||
__module__ = "pytest"
|
||||
|
|
|
@ -4704,8 +4704,8 @@ def test_dont_recompute_dependency_tree_if_no_dynamic_parametrize(pytester: Pyte
|
|||
reprec.assertoutcome(passed=5)
|
||||
|
||||
|
||||
def test_deduplicate_names(pytester: Pytester) -> None:
|
||||
def test_deduplicate_names() -> None:
|
||||
items = deduplicate_names("abacd")
|
||||
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")
|
||||
|
|
|
@ -482,6 +482,24 @@ class TestDoctests:
|
|||
reprec = pytester.inline_run(p, "--doctest-modules")
|
||||
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):
|
||||
p = pytester.mkpydir("hello")
|
||||
p.joinpath("__init__.py").write_text(
|
||||
|
|
|
@ -262,3 +262,34 @@ def test_module_full_path_without_drive(pytester: Pytester) -> None:
|
|||
"* 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 pickle
|
||||
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_lock_path
|
||||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportMode
|
||||
from _pytest.pathlib import ImportPathMismatchError
|
||||
from _pytest.pathlib import insert_missing_modules
|
||||
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
||||
from _pytest.pathlib import module_name_from_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 visit
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
|
@ -585,6 +588,10 @@ class TestImportLibMode:
|
|||
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
|
||||
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(
|
||||
self, monkeypatch: MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
|
@ -615,3 +622,72 @@ class TestImportLibMode:
|
|||
assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
|
||||
assert modules["xxx"].tests is modules["xxx.tests"]
|
||||
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