Merge pull request #9232 from bluetech/deprecate-node-fspath-args

Deprecate Node constuctor fspath argument, and other small changes/fixes
This commit is contained in:
Ran Benita 2021-10-27 09:26:57 +03:00 committed by GitHub
commit b7fc0003fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 115 additions and 58 deletions

View File

@ -0,0 +1,3 @@
``py.path.local`` arguments for hooks have been deprecated. See :ref:`the deprecation note <legacy-path-hooks-deprecated>` for full details.
``py.path.local`` arguments to Node constructors have been deprecated. See :ref:`the deprecation note <node-ctor-fspath-deprecation>` for full details.

View File

@ -18,17 +18,40 @@ Deprecated Features
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`. :class:`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
``py.path.local`` ``fspath`` parameters with ``pathlib.Path`` parameters, and
drop any other usage of the ``py`` library if possible.
.. _legacy-path-hooks-deprecated:
``py.path.local`` arguments for hooks replaced with ``pathlib.Path`` ``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order to support the transition to :mod:`pathlib`, the following hooks now receive additional arguments: .. deprecated:: 7.0
* :func:`pytest_ignore_collect(fspath: pathlib.Path) <_pytest.hookspec.pytest_ignore_collect>` In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
* :func:`pytest_collect_file(fspath: pathlib.Path) <_pytest.hookspec.pytest_collect_file>`
* :func:`pytest_pycollect_makemodule(fspath: pathlib.Path) <_pytest.hookspec.pytest_pycollect_makemodule>` * :func:`pytest_ignore_collect(fspath: pathlib.Path) <_pytest.hookspec.pytest_ignore_collect>` instead of ``path``
* :func:`pytest_report_header(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_header>` * :func:`pytest_collect_file(fspath: pathlib.Path) <_pytest.hookspec.pytest_collect_file>` instead of ``path``
* :func:`pytest_report_collectionfinish(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_collectionfinish>` * :func:`pytest_pycollect_makemodule(fspath: pathlib.Path) <_pytest.hookspec.pytest_pycollect_makemodule>` instead of ``path``
* :func:`pytest_report_header(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_header>` instead of ``startdir``
* :func:`pytest_report_collectionfinish(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_collectionfinish>` instead of ``startdir``
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments. The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
@ -59,7 +82,7 @@ Implement the :func:`pytest_load_initial_conftests <_pytest.hookspec.pytest_load
Diamond inheritance between :class:`pytest.File` and :class:`pytest.Item` Diamond inheritance between :class:`pytest.File` and :class:`pytest.Item`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.3 .. deprecated:: 7.0
Inheriting from both Item and file at once has never been supported officially, Inheriting from both Item and file at once has never been supported officially,
however some plugins providing linting/code analysis have been using this as a hack. however some plugins providing linting/code analysis have been using this as a hack.
@ -86,7 +109,7 @@ scheduled for removal in pytest 7 (deprecated since pytest 2.4.0):
Raising ``unittest.SkipTest`` during collection Raising ``unittest.SkipTest`` during collection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.3 .. deprecated:: 7.0
Raising :class:`unittest.SkipTest` to skip collection of tests during the Raising :class:`unittest.SkipTest` to skip collection of tests during the
pytest collection phase is deprecated. Use :func:`pytest.skip` instead. pytest collection phase is deprecated. Use :func:`pytest.skip` instead.

View File

@ -268,7 +268,7 @@ argument ``match`` to assert that the exception matches a text or regex::
... warnings.warn("this is not here", UserWarning) ... warnings.warn("this is not here", UserWarning)
Traceback (most recent call last): Traceback (most recent call last):
... ...
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
You can also call :func:`pytest.warns` on a function or code string: You can also call :func:`pytest.warns` on a function or code string:

View File

@ -128,7 +128,7 @@ class Cache:
it to manage files to e.g. store/retrieve database dumps across test it to manage files to e.g. store/retrieve database dumps across test
sessions. sessions.
.. versionadded:: 6.3 .. versionadded:: 7.0
:param name: :param name:
Must be a string not containing a ``/`` separator. Must be a string not containing a ``/`` separator.

View File

@ -185,7 +185,7 @@ class Parser:
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
* ``pathlist``: a list of ``py.path``, separated as in a shell * ``pathlist``: a list of ``py.path``, separated as in a shell
.. versionadded:: 6.3 .. versionadded:: 7.0
The ``paths`` variable type. The ``paths`` variable type.
Defaults to ``string`` if ``None`` or not passed. Defaults to ``string`` if ``None`` or not passed.

View File

@ -4,8 +4,9 @@ from pathlib import Path
from typing import Optional from typing import Optional
from ..compat import LEGACY_PATH from ..compat import LEGACY_PATH
from ..compat import legacy_path
from ..deprecated import HOOK_LEGACY_PATH_ARG from ..deprecated import HOOK_LEGACY_PATH_ARG
from _pytest.nodes import _imply_path from _pytest.nodes import _check_path
# hookname: (Path, LEGACY_PATH) # hookname: (Path, LEGACY_PATH)
imply_paths_hooks = { imply_paths_hooks = {
@ -52,7 +53,15 @@ class PathAwareHookProxy:
), ),
stacklevel=2, stacklevel=2,
) )
path_value, fspath_value = _imply_path(path_value, fspath_value) if path_value is not None:
if fspath_value is not None:
_check_path(path_value, fspath_value)
else:
fspath_value = legacy_path(path_value)
else:
assert fspath_value is not None
path_value = Path(fspath_value)
kw[path_var] = path_value kw[path_var] = path_value
kw[fspath_var] = fspath_value kw[fspath_var] = fspath_value
return hook(**kw) return hook(**kw)

View File

@ -101,6 +101,14 @@ HOOK_LEGACY_PATH_ARG = UnformattedWarning(
"#py-path-local-arguments-for-hooks-replaced-with-pathlib-path", "#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
) )
NODE_CTOR_FSPATH_ARG = UnformattedWarning(
PytestDeprecationWarning,
"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",
)
WARNS_NONE_ARG = PytestDeprecationWarning( WARNS_NONE_ARG = PytestDeprecationWarning(
"Passing None to catch any warning has been deprecated, pass no arguments instead:\n" "Passing None to catch any warning has been deprecated, pass no arguments instead:\n"
" Replace pytest.warns(None) by simply pytest.warns()." " Replace pytest.warns(None) by simply pytest.warns()."

View File

@ -272,12 +272,13 @@ def pytest_ignore_collect(
Stops at first non-None result, see :ref:`firstresult`. Stops at first non-None result, see :ref:`firstresult`.
:param pathlib.Path fspath: The path to analyze. :param pathlib.Path fspath: The path to analyze.
:param LEGACY_PATH path: The path to analyze. :param LEGACY_PATH path: The path to analyze (deprecated).
:param pytest.Config config: The pytest config object. :param pytest.Config config: The pytest config object.
.. versionchanged:: 6.3.0 .. versionchanged:: 7.0.0
The ``fspath`` parameter was added as a :class:`pathlib.Path` The ``fspath`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
""" """
@ -289,11 +290,12 @@ def pytest_collect_file(
The new node needs to have the specified ``parent`` as a parent. The new node needs to have the specified ``parent`` as a parent.
:param pathlib.Path fspath: The path to analyze. :param pathlib.Path fspath: The path to analyze.
:param LEGACY_PATH path: The path to collect. :param LEGACY_PATH path: The path to collect (deprecated).
.. versionchanged:: 6.3.0 .. versionchanged:: 7.0.0
The ``fspath`` parameter was added as a :class:`pathlib.Path` The ``fspath`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
""" """
@ -345,11 +347,13 @@ def pytest_pycollect_makemodule(
Stops at first non-None result, see :ref:`firstresult`. Stops at first non-None result, see :ref:`firstresult`.
:param pathlib.Path fspath: The path of the module to collect. :param pathlib.Path fspath: The path of the module to collect.
:param legacy_path path: The path of the module to collect. :param LEGACY_PATH path: The path of the module to collect (deprecated).
.. versionchanged:: 6.3.0 .. versionchanged:: 7.0.0
The ``fspath`` parameter was added as a :class:`pathlib.Path` The ``fspath`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. equivalent of the ``path`` parameter.
The ``path`` parameter has been deprecated in favor of ``fspath``.
""" """
@ -674,7 +678,7 @@ def pytest_report_header(
:param pytest.Config config: The pytest config object. :param pytest.Config config: The pytest config object.
:param Path startpath: The starting dir. :param Path startpath: The starting dir.
:param LEGACY_PATH startdir: The starting dir. :param LEGACY_PATH startdir: The starting dir (deprecated).
.. note:: .. note::
@ -689,9 +693,10 @@ def pytest_report_header(
files situated at the tests root directory due to how pytest files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`. :ref:`discovers plugins during startup <pluginorder>`.
.. versionchanged:: 6.3.0 .. versionchanged:: 7.0.0
The ``startpath`` parameter was added as a :class:`pathlib.Path` The ``startpath`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter. equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
""" """
@ -709,8 +714,8 @@ def pytest_report_collectionfinish(
.. versionadded:: 3.2 .. versionadded:: 3.2
:param pytest.Config config: The pytest config object. :param pytest.Config config: The pytest config object.
:param Path startpath: The starting path. :param Path startpath: The starting dir.
:param LEGACY_PATH startdir: The starting dir. :param LEGACY_PATH startdir: The starting dir (deprecated).
:param items: List of pytest items that are going to be executed; this list should not be modified. :param items: List of pytest items that are going to be executed; this list should not be modified.
.. note:: .. note::
@ -720,9 +725,10 @@ def pytest_report_collectionfinish(
If you want to have your line(s) displayed first, use If you want to have your line(s) displayed first, use
:ref:`trylast=True <plugin-hookorder>`. :ref:`trylast=True <plugin-hookorder>`.
.. versionchanged:: 6.3.0 .. versionchanged:: 7.0.0
The ``startpath`` parameter was added as a :class:`pathlib.Path` The ``startpath`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter. equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
""" """

View File

@ -500,7 +500,7 @@ class Session(nodes.FSCollector):
def startpath(self) -> Path: def startpath(self) -> Path:
"""The path from which pytest was invoked. """The path from which pytest was invoked.
.. versionadded:: 6.3.0 .. versionadded:: 7.0.0
""" """
return self.config.invocation_params.dir return self.config.invocation_params.dir

View File

@ -28,6 +28,7 @@ from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
from _pytest.mark.structures import Mark from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords from _pytest.mark.structures import NodeKeywords
@ -102,22 +103,17 @@ def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
def _imply_path( def _imply_path(
path: Optional[Path], fspath: Optional[LEGACY_PATH] node_type: Type["Node"],
) -> Tuple[Path, LEGACY_PATH]: path: Optional[Path],
if path is not None: fspath: Optional[LEGACY_PATH],
) -> Path:
if fspath is not None: if fspath is not None:
_check_path(path, fspath) warnings.warn(
else: NODE_CTOR_FSPATH_ARG.format(
fspath = legacy_path(path) node_type_name=node_type.__name__,
return path, fspath ),
else: stacklevel=3,
assert fspath is not None )
return Path(fspath), fspath
# Optimization: use _imply_path_only over _imply_path when only need Path.
# This is to avoid `legacy_path(path)` which is surprisingly heavy.
def _imply_path_only(path: Optional[Path], fspath: Optional[LEGACY_PATH]) -> Path:
if path is not None: if path is not None:
if fspath is not None: if fspath is not None:
_check_path(path, fspath) _check_path(path, fspath)
@ -210,9 +206,9 @@ class Node(metaclass=NodeMeta):
self.session = parent.session self.session = parent.session
#: Filesystem path where this node was collected from (can be None). #: Filesystem path where this node was collected from (can be None).
self.path = _imply_path_only( if path is None and fspath is None:
path or getattr(parent, "path", None), fspath=fspath path = getattr(parent, "path", None)
) self.path = _imply_path(type(self), path, fspath=fspath)
# The explicit annotation is to avoid publicly exposing NodeKeywords. # The explicit annotation is to avoid publicly exposing NodeKeywords.
#: Keywords/markers collected from all scopes. #: Keywords/markers collected from all scopes.
@ -589,7 +585,7 @@ class FSCollector(Collector):
assert path is None assert path is None
path = path_or_parent path = path_or_parent
path = _imply_path_only(path, fspath=fspath) path = _imply_path(type(self), path, fspath=fspath)
if name is None: if name is None:
name = path.name name = path.name
if parent is not None and parent.path != path: if parent is not None and parent.path != path:
@ -634,7 +630,6 @@ class FSCollector(Collector):
**kw, **kw,
): ):
"""The public constructor.""" """The public constructor."""
path, fspath = _imply_path(path, fspath=fspath)
return super().from_parent(parent=parent, fspath=fspath, path=path, **kw) return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
def gethookproxy(self, fspath: "os.PathLike[str]"): def gethookproxy(self, fspath: "os.PathLike[str]"):

View File

@ -136,7 +136,7 @@ def warns(
... warnings.warn("this is not here", UserWarning) ... warnings.warn("this is not here", UserWarning)
Traceback (most recent call last): Traceback (most recent call last):
... ...
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
""" """
__tracebackhide__ = True __tracebackhide__ = True
@ -274,7 +274,7 @@ class WarningsChecker(WarningsRecorder):
if not any(issubclass(r.category, self.expected_warning) for r in self): if not any(issubclass(r.category, self.expected_warning) for r in self):
__tracebackhide__ = True __tracebackhide__ = True
fail( fail(
"DID NOT WARN. No warnings of type {} was emitted. " "DID NOT WARN. No warnings of type {} were emitted. "
"The list of emitted warnings is: {}.".format( "The list of emitted warnings is: {}.".format(
self.expected_warning, [each.message for each in self] self.expected_warning, [each.message for each in self]
) )
@ -287,7 +287,7 @@ class WarningsChecker(WarningsRecorder):
else: else:
fail( fail(
"DID NOT WARN. No warnings of type {} matching" "DID NOT WARN. No warnings of type {} matching"
" ('{}') was emitted. The list of emitted warnings" " ('{}') were emitted. The list of emitted warnings"
" is: {}.".format( " is: {}.".format(
self.expected_warning, self.expected_warning,
self.match_expr, self.match_expr,

View File

@ -215,3 +215,16 @@ def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None:
"*Please use pytest_load_initial_conftests hook instead.*", "*Please use pytest_load_initial_conftests hook instead.*",
] ]
) )
def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
mod = pytester.getmodulecol("")
with pytest.warns(
pytest.PytestDeprecationWarning,
match=re.escape("The (fspath: py.path.local) argument to File is deprecated."),
):
pytest.File.from_parent(
parent=mod.parent,
fspath=legacy_path("bla"),
)

View File

@ -4,7 +4,7 @@ pytest-asyncio==0.16.0
pytest-bdd==4.1.0 pytest-bdd==4.1.0
pytest-cov==3.0.0 pytest-cov==3.0.0
pytest-django==4.4.0 pytest-django==4.4.0
pytest-flakes==4.0.3 pytest-flakes==4.0.4
pytest-html==3.1.1 pytest-html==3.1.1
pytest-mock==3.6.1 pytest-mock==3.6.1
pytest-rerunfailures==10.2 pytest-rerunfailures==10.2

View File

@ -263,7 +263,7 @@ class TestWarns:
with pytest.warns(RuntimeWarning): with pytest.warns(RuntimeWarning):
warnings.warn("user", UserWarning) warnings.warn("user", UserWarning)
excinfo.match( excinfo.match(
r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) was emitted. " r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) were emitted. "
r"The list of emitted warnings is: \[UserWarning\('user',?\)\]." r"The list of emitted warnings is: \[UserWarning\('user',?\)\]."
) )
@ -271,7 +271,7 @@ class TestWarns:
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
warnings.warn("runtime", RuntimeWarning) warnings.warn("runtime", RuntimeWarning)
excinfo.match( excinfo.match(
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) was emitted. " r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted. "
r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)\]." r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)\]."
) )
@ -279,7 +279,7 @@ class TestWarns:
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
pass pass
excinfo.match( excinfo.match(
r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) was emitted. " r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted. "
r"The list of emitted warnings is: \[\]." r"The list of emitted warnings is: \[\]."
) )
@ -290,7 +290,7 @@ class TestWarns:
warnings.warn("import", ImportWarning) warnings.warn("import", ImportWarning)
message_template = ( message_template = (
"DID NOT WARN. No warnings of type {0} was emitted. " "DID NOT WARN. No warnings of type {0} were emitted. "
"The list of emitted warnings is: {1}." "The list of emitted warnings is: {1}."
) )
excinfo.match( excinfo.match(