From 6c89f9261c6f5bde93bd116ef56b7ac96fc0ef21 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 1 Jan 2024 16:45:17 +0200 Subject: [PATCH] Remove deprecated py.path (`fspath`) node constructor arguments --- doc/en/conf.py | 1 - doc/en/deprecations.rst | 79 ++++++++++++++++++------------------ src/_pytest/compat.py | 15 ------- src/_pytest/config/compat.py | 13 ------ src/_pytest/deprecated.py | 9 ---- src/_pytest/legacypath.py | 18 +++++++- src/_pytest/main.py | 1 - src/_pytest/nodes.py | 45 +++----------------- src/_pytest/python.py | 3 -- testing/deprecated_test.py | 22 ---------- testing/test_legacypath.py | 4 +- testing/test_nodes.py | 9 ++-- 12 files changed, 66 insertions(+), 153 deletions(-) delete mode 100644 src/_pytest/config/compat.py diff --git a/doc/en/conf.py b/doc/en/conf.py index d3a98015a..2bc18be58 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -199,7 +199,6 @@ nitpick_ignore = [ ("py:class", "_tracing.TagTracerSub"), ("py:class", "warnings.WarningMessage"), # Undocumented type aliases - ("py:class", "LEGACY_PATH"), ("py:class", "_PluggyPlugin"), # TypeVars ("py:class", "_pytest._code.code.E"), diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index c623f09ca..bcc195c60 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -19,45 +19,6 @@ Below is a complete list of all pytest features which are considered deprecated. :class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. -.. _node-ctor-fspath-deprecation: - -``fspath`` argument for Node constructors replaced with ``pathlib.Path`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 7.0 - -In order to support the transition from ``py.path.local`` to :mod:`pathlib`, -the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like -:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()` -is now deprecated. - -Plugins which construct nodes should pass the ``path`` argument, of type -:class:`pathlib.Path`, instead of the ``fspath`` argument. - -Plugins which implement custom items and collectors are encouraged to replace -``fspath`` parameters (``py.path.local``) with ``path`` parameters -(``pathlib.Path``), and drop any other usage of the ``py`` library if possible. - -If possible, plugins with custom items should use :ref:`cooperative -constructors ` to avoid hardcoding -arguments they only pass on to the superclass. - -.. note:: - The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the - new attribute being ``path``) is **the opposite** of the situation for - hooks, :ref:`outlined below ` (the old - argument being ``path``). - - This is an unfortunate artifact due to historical reasons, which should be - resolved in future versions as we slowly get rid of the :pypi:`py` - dependency (see :issue:`9283` for a longer discussion). - -Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo` -which still is expected to return a ``py.path.local`` object, nodes still have -both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes, -no matter what argument was used in the constructor. We expect to deprecate the -``fspath`` attribute in a future release. - .. _legacy-path-hooks-deprecated: Configuring hook specs/impls using markers @@ -251,6 +212,46 @@ an appropriate period of deprecation has passed. Some breaking changes which could not be deprecated are also listed. +.. _node-ctor-fspath-deprecation: + +``fspath`` argument for Node constructors replaced with ``pathlib.Path`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.0 + +In order to support the transition from ``py.path.local`` to :mod:`pathlib`, +the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like +:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()` +is now deprecated. + +Plugins which construct nodes should pass the ``path`` argument, of type +:class:`pathlib.Path`, instead of the ``fspath`` argument. + +Plugins which implement custom items and collectors are encouraged to replace +``fspath`` parameters (``py.path.local``) with ``path`` parameters +(``pathlib.Path``), and drop any other usage of the ``py`` library if possible. + +If possible, plugins with custom items should use :ref:`cooperative +constructors ` to avoid hardcoding +arguments they only pass on to the superclass. + +.. note:: + The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the + new attribute being ``path``) is **the opposite** of the situation for + hooks, :ref:`outlined below ` (the old + argument being ``path``). + + This is an unfortunate artifact due to historical reasons, which should be + resolved in future versions as we slowly get rid of the :pypi:`py` + dependency (see :issue:`9283` for a longer discussion). + +Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo` +which still is expected to return a ``py.path.local`` object, nodes still have +both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes, +no matter what argument was used in the constructor. We expect to deprecate the +``fspath`` attribute in a future release. + + ``py.path.local`` arguments for hooks replaced with ``pathlib.Path`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 73d77f978..1e9c38ca8 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -16,25 +16,10 @@ from typing import Final from typing import NoReturn from typing import TypeVar -import py - _T = TypeVar("_T") _S = TypeVar("_S") -#: constant to prepare valuing pylib path replacements/lazy proxies later on -# intended for removal in pytest 8.0 or 9.0 - -# fmt: off -# intentional space to create a fake difference for the verification -LEGACY_PATH = py.path. local -# fmt: on - - -def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH: - """Internal wrapper to prepare lazy proxies for legacy_path instances""" - return LEGACY_PATH(path) - # fmt: off # Singleton type for NOTSET, as described in: diff --git a/src/_pytest/config/compat.py b/src/_pytest/config/compat.py deleted file mode 100644 index 9c61b4dac..000000000 --- a/src/_pytest/config/compat.py +++ /dev/null @@ -1,13 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from ..compat import LEGACY_PATH - - -def _check_path(path: Path, fspath: LEGACY_PATH) -> None: - if Path(fspath) != path: - raise ValueError( - f"Path({fspath!r}) != {path!r}\n" - "if both path and fspath are given they need to be equal" - ) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 5421f2320..1bc2cf57e 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -11,7 +11,6 @@ in case of warnings which need to format their messages. from warnings import warn from _pytest.warning_types import PytestDeprecationWarning -from _pytest.warning_types import PytestRemovedIn8Warning from _pytest.warning_types import PytestRemovedIn9Warning from _pytest.warning_types import UnformattedWarning @@ -35,14 +34,6 @@ YIELD_FIXTURE = PytestDeprecationWarning( PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") -NODE_CTOR_FSPATH_ARG = UnformattedWarning( - PytestRemovedIn8Warning, - "The (fspath: py.path.local) argument to {node_type_name} is deprecated. " - "Please use the (path: pathlib.Path) argument instead.\n" - "See https://docs.pytest.org/en/latest/deprecations.html" - "#fspath-argument-for-node-constructors-replaced-with-pathlib-path", -) - HOOK_LEGACY_MARKING = UnformattedWarning( PytestDeprecationWarning, "The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n" diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 4876a083a..b2dd87436 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -1,5 +1,6 @@ """Add backward compatibility support for the legacy py path type.""" import dataclasses +import os import shlex import subprocess from pathlib import Path @@ -12,9 +13,8 @@ from typing import Union from iniconfig import SectionWrapper +import py from _pytest.cacheprovider import Cache -from _pytest.compat import LEGACY_PATH -from _pytest.compat import legacy_path from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config import PytestPluginManager @@ -36,6 +36,20 @@ if TYPE_CHECKING: import pexpect +#: constant to prepare valuing pylib path replacements/lazy proxies later on +# intended for removal in pytest 8.0 or 9.0 + +# fmt: off +# intentional space to create a fake difference for the verification +LEGACY_PATH = py.path. local +# fmt: on + + +def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH: + """Internal wrapper to prepare lazy proxies for legacy_path instances""" + return LEGACY_PATH(path) + + @final class Testdir: """ diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 83952c60c..9fb96840e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -541,7 +541,6 @@ class Session(nodes.Collector): super().__init__( name="", path=config.rootpath, - fspath=None, parent=None, config=config, session=self, diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index eefe690de..4cf6768e6 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,6 +1,5 @@ import abc import os -import pathlib import warnings from functools import cached_property from inspect import signature @@ -28,11 +27,8 @@ from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest._code.code import Traceback -from _pytest.compat import LEGACY_PATH from _pytest.config import Config from _pytest.config import ConftestImportFailure -from _pytest.config.compat import _check_path -from _pytest.deprecated import NODE_CTOR_FSPATH_ARG from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords @@ -98,27 +94,6 @@ def iterparentnodeids(nodeid: str) -> Iterator[str]: yield nodeid -def _imply_path( - node_type: Type["Node"], - path: Optional[Path], - fspath: Optional[LEGACY_PATH], -) -> Path: - if fspath is not None: - warnings.warn( - NODE_CTOR_FSPATH_ARG.format( - node_type_name=node_type.__name__, - ), - stacklevel=6, - ) - if path is not None: - if fspath is not None: - _check_path(path, fspath) - return path - else: - assert fspath is not None - return Path(fspath) - - _NodeType = TypeVar("_NodeType", bound="Node") @@ -173,14 +148,6 @@ class Node(abc.ABC, metaclass=NodeMeta): ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the leaf nodes. """ - - # Implemented in the legacypath plugin. - #: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage - #: for methods not migrated to ``pathlib.Path`` yet, such as - #: :meth:`Item.reportinfo `. Will be deprecated in - #: a future release, prefer using :attr:`path` instead. - fspath: LEGACY_PATH - # Use __slots__ to make attribute access faster. # Note that __dict__ is still available. __slots__ = ( @@ -200,7 +167,6 @@ class Node(abc.ABC, metaclass=NodeMeta): parent: "Optional[Node]" = None, config: Optional[Config] = None, session: "Optional[Session]" = None, - fspath: Optional[LEGACY_PATH] = None, path: Optional[Path] = None, nodeid: Optional[str] = None, ) -> None: @@ -226,10 +192,11 @@ class Node(abc.ABC, metaclass=NodeMeta): raise TypeError("session or parent must be provided") self.session = parent.session - if path is None and fspath is None: + if path is None: path = getattr(parent, "path", None) + assert path is not None #: Filesystem path where this node was collected from (can be None). - self.path: pathlib.Path = _imply_path(type(self), path, fspath=fspath) + self.path = path # The explicit annotation is to avoid publicly exposing NodeKeywords. #: Keywords/markers collected from all scopes. @@ -595,7 +562,6 @@ class FSCollector(Collector, abc.ABC): def __init__( self, - fspath: Optional[LEGACY_PATH] = None, path_or_parent: Optional[Union[Path, Node]] = None, path: Optional[Path] = None, name: Optional[str] = None, @@ -611,8 +577,8 @@ class FSCollector(Collector, abc.ABC): elif isinstance(path_or_parent, Path): assert path is None path = path_or_parent + assert path is not None - path = _imply_path(type(self), path, fspath=fspath) if name is None: name = path.name if parent is not None and parent.path != path: @@ -652,12 +618,11 @@ class FSCollector(Collector, abc.ABC): cls, parent, *, - fspath: Optional[LEGACY_PATH] = None, path: Optional[Path] = None, **kw, ): """The public constructor.""" - return super().from_parent(parent=parent, fspath=fspath, path=path, **kw) + return super().from_parent(parent=parent, path=path, **kw) class File(FSCollector, abc.ABC): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 969bb6765..184399080 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -47,7 +47,6 @@ from _pytest.compat import getimfunc from _pytest.compat import getlocation from _pytest.compat import is_async_function from _pytest.compat import is_generator -from _pytest.compat import LEGACY_PATH from _pytest.compat import NOTSET from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass @@ -672,7 +671,6 @@ class Package(nodes.Directory): def __init__( self, - fspath: Optional[LEGACY_PATH], parent: nodes.Collector, # NOTE: following args are unused: config=None, @@ -684,7 +682,6 @@ class Package(nodes.Directory): # super().__init__(self, fspath, parent=parent) session = parent.session super().__init__( - fspath=fspath, path=path, parent=parent, config=config, diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 5f7f4a2a6..ebff49ce6 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,8 +1,5 @@ -import re - import pytest from _pytest import deprecated -from _pytest.compat import legacy_path from _pytest.pytester import Pytester from pytest import PytestDeprecationWarning @@ -87,25 +84,6 @@ def test_private_is_deprecated() -> None: PrivateInit(10, _ispytest=True) -def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None: - mod = pytester.getmodulecol("") - - class MyFile(pytest.File): - def collect(self): - raise NotImplementedError() - - with pytest.warns( - pytest.PytestDeprecationWarning, - match=re.escape( - "The (fspath: py.path.local) argument to MyFile is deprecated." - ), - ): - MyFile.from_parent( - parent=mod.parent, - fspath=legacy_path("bla"), - ) - - def test_fixture_disallow_on_marked_functions(): """Test that applying @pytest.fixture to a marked function warns (#3364).""" with pytest.warns( diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index b4fd1bf2c..700499f24 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -1,8 +1,8 @@ from pathlib import Path import pytest -from _pytest.compat import LEGACY_PATH from _pytest.fixtures import TopRequest +from _pytest.legacypath import LEGACY_PATH from _pytest.legacypath import TempdirFactory from _pytest.legacypath import Testdir @@ -15,7 +15,7 @@ def test_item_fspath(pytester: pytest.Pytester) -> None: items2, hookrec = pytester.inline_genitems(item.nodeid) (item2,) = items2 assert item2.name == item.name - assert item2.fspath == item.fspath + assert item2.fspath == item.fspath # type: ignore[attr-defined] assert item2.path == item.path diff --git a/testing/test_nodes.py b/testing/test_nodes.py index 84c377cf9..880e2a44f 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -7,7 +7,6 @@ from typing import Type import pytest from _pytest import nodes -from _pytest.compat import legacy_path from _pytest.outcomes import OutcomeException from _pytest.pytester import Pytester from _pytest.warning_types import PytestWarning @@ -69,9 +68,9 @@ def test_subclassing_both_item_and_collector_deprecated( warnings.simplefilter("error") class SoWrong(nodes.Item, nodes.File): - def __init__(self, fspath, parent): + def __init__(self, path, parent): """Legacy ctor with legacy call # don't wana see""" - super().__init__(fspath, parent) + super().__init__(parent, path) def collect(self): raise NotImplementedError() @@ -80,9 +79,7 @@ def test_subclassing_both_item_and_collector_deprecated( raise NotImplementedError() with pytest.warns(PytestWarning) as rec: - SoWrong.from_parent( - request.session, fspath=legacy_path(tmp_path / "broken.txt") - ) + SoWrong.from_parent(request.session, path=tmp_path / "broken.txt", wrong=10) messages = [str(x.message) for x in rec] assert any( re.search(".*SoWrong.* not using a cooperative constructor.*", x)