Skip to content

Commit

Permalink
Remove deprecated py.path (fspath) node constructor arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
bluetech committed Jan 3, 2024
1 parent a98f02d commit 6c89f92
Show file tree
Hide file tree
Showing 12 changed files with 66 additions and 153 deletions.
1 change: 0 additions & 1 deletion doc/en/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@
("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"),
Expand Down
79 changes: 40 additions & 39 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <warnings>`.


.. _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 <uncooperative-constructors-deprecated>` 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 <legacy-path-hooks-deprecated>` (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
Expand Down Expand Up @@ -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 <uncooperative-constructors-deprecated>` 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 <legacy-path-hooks-deprecated>` (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``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
15 changes: 0 additions & 15 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,10 @@
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:
Expand Down
13 changes: 0 additions & 13 deletions src/_pytest/config/compat.py

This file was deleted.

9 changes: 0 additions & 9 deletions src/_pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
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

Expand All @@ -35,14 +34,6 @@
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"
Expand Down
18 changes: 16 additions & 2 deletions src/_pytest/legacypath.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,9 +13,8 @@

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
Expand All @@ -36,6 +36,20 @@
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:
"""
Expand Down
1 change: 0 additions & 1 deletion src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,6 @@ def __init__(self, config: Config) -> None:
super().__init__(
name="",
path=config.rootpath,
fspath=None,
parent=None,
config=config,
session=self,
Expand Down
45 changes: 5 additions & 40 deletions src/_pytest/nodes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import abc
import os
import pathlib
import warnings
from functools import cached_property
from inspect import signature
Expand Down Expand Up @@ -28,11 +27,8 @@
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
Expand Down Expand Up @@ -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")


Expand Down Expand Up @@ -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 <pytest.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__ = (
Expand All @@ -200,7 +167,6 @@ def __init__(
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:
Expand All @@ -226,10 +192,11 @@ def __init__(
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.
Expand Down Expand Up @@ -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,
Expand All @@ -611,8 +577,8 @@ def __init__(
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:
Expand Down Expand Up @@ -652,12 +618,11 @@ def from_parent(
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):
Expand Down
3 changes: 0 additions & 3 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
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
Expand Down Expand Up @@ -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,
Expand All @@ -684,7 +682,6 @@ def __init__(
# super().__init__(self, fspath, parent=parent)
session = parent.session
super().__init__(
fspath=fspath,
path=path,
parent=parent,
config=config,
Expand Down
22 changes: 0 additions & 22 deletions testing/deprecated_test.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -87,25 +84,6 @@ def __init__(self, foo: int, *, _ispytest: bool = False) -> 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(
Expand Down
Loading

0 comments on commit 6c89f92

Please sign in to comment.