Merge branch 'main' into Improvement-remove-prune_dependency_tree

This commit is contained in:
Sadra Barikbin 2023-09-09 00:52:21 +03:30
commit c72507cad9
29 changed files with 352 additions and 147 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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/>`__.

View File

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

View File

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

View File

@ -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

View File

@ -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.

View File

@ -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)
=========================

View File

@ -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:

View File

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

View File

@ -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>`.

View File

@ -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.

View File

@ -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.
"""

View File

@ -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.

View File

@ -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:

View File

@ -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))

View File

@ -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

View File

@ -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"""

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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"

View File

@ -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")

View File

@ -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(

View File

@ -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 *")

View File

@ -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