From 853b0a4c299d909b7e543a4307f16e3746d642cd Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Sat, 23 Oct 2021 21:33:12 +0300
Subject: [PATCH 1/7] nodes: fix bug in Node() fspath compat

Since path <-> fspath are converted to each other, we need to check both
before looking at the parent, in case fspath is set but path is not.
---
 src/_pytest/nodes.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py
index 3b70e111789..37b0b3d2b70 100644
--- a/src/_pytest/nodes.py
+++ b/src/_pytest/nodes.py
@@ -210,9 +210,9 @@ def __init__(
             self.session = parent.session
 
         #: Filesystem path where this node was collected from (can be None).
-        self.path = _imply_path_only(
-            path or getattr(parent, "path", None), fspath=fspath
-        )
+        if path is None and fspath is None:
+            path = getattr(parent, "path", None)
+        self.path = _imply_path_only(path, fspath=fspath)
 
         # The explicit annotation is to avoid publicly exposing NodeKeywords.
         #: Keywords/markers collected from all scopes.

From 6be3f31dba3f4832577e46c8dbaa0a7048980a83 Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Sat, 23 Oct 2021 21:18:51 +0300
Subject: [PATCH 2/7] nodes: remove redundent _imply_path call

The Node ctor will take care of the check.
---
 src/_pytest/nodes.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py
index 37b0b3d2b70..0a5acbad918 100644
--- a/src/_pytest/nodes.py
+++ b/src/_pytest/nodes.py
@@ -634,7 +634,6 @@ def from_parent(
         **kw,
     ):
         """The public constructor."""
-        path, fspath = _imply_path(path, fspath=fspath)
         return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
 
     def gethookproxy(self, fspath: "os.PathLike[str]"):

From afc7442e22e629038b84990200d066058f6be4c6 Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Sat, 23 Oct 2021 21:40:57 +0300
Subject: [PATCH 3/7] nodes: inline `_imply_path`

Only one usage left, and we certainly don't expect more!

Rename `_imply_path_only` to `_imply_path`, that's a less confusing name
now.
---
 src/_pytest/config/compat.py | 13 +++++++++++--
 src/_pytest/nodes.py         | 22 +++-------------------
 2 files changed, 14 insertions(+), 21 deletions(-)

diff --git a/src/_pytest/config/compat.py b/src/_pytest/config/compat.py
index c93f3738d4f..731641ddebf 100644
--- a/src/_pytest/config/compat.py
+++ b/src/_pytest/config/compat.py
@@ -4,8 +4,9 @@
 from typing import Optional
 
 from ..compat import LEGACY_PATH
+from ..compat import legacy_path
 from ..deprecated import HOOK_LEGACY_PATH_ARG
-from _pytest.nodes import _imply_path
+from _pytest.nodes import _check_path
 
 # hookname: (Path, LEGACY_PATH)
 imply_paths_hooks = {
@@ -52,7 +53,15 @@ def fixed_hook(**kw):
                         ),
                         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[fspath_var] = fspath_value
                 return hook(**kw)
diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py
index 0a5acbad918..33bf1062b02 100644
--- a/src/_pytest/nodes.py
+++ b/src/_pytest/nodes.py
@@ -101,23 +101,7 @@ def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
         )
 
 
-def _imply_path(
-    path: Optional[Path], fspath: Optional[LEGACY_PATH]
-) -> Tuple[Path, LEGACY_PATH]:
-    if path is not None:
-        if fspath is not None:
-            _check_path(path, fspath)
-        else:
-            fspath = legacy_path(path)
-        return path, fspath
-    else:
-        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:
+def _imply_path(path: Optional[Path], fspath: Optional[LEGACY_PATH]) -> Path:
     if path is not None:
         if fspath is not None:
             _check_path(path, fspath)
@@ -212,7 +196,7 @@ def __init__(
         #: Filesystem path where this node was collected from (can be None).
         if path is None and fspath is None:
             path = getattr(parent, "path", None)
-        self.path = _imply_path_only(path, fspath=fspath)
+        self.path = _imply_path(path, fspath=fspath)
 
         # The explicit annotation is to avoid publicly exposing NodeKeywords.
         #: Keywords/markers collected from all scopes.
@@ -589,7 +573,7 @@ def __init__(
                 assert path is None
                 path = path_or_parent
 
-        path = _imply_path_only(path, fspath=fspath)
+        path = _imply_path(path, fspath=fspath)
         if name is None:
             name = path.name
             if parent is not None and parent.path != path:

From 755ce9bc808a5a6eb63d601a27aab6064e1faeaa Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Sat, 23 Oct 2021 22:29:20 +0300
Subject: [PATCH 4/7] hookspec: improve legacy path deprecation docs

---
 changelog/7259.deprecation.rst |  1 +
 doc/en/deprecations.rst        | 16 +++++++++------
 src/_pytest/hookspec.py        | 36 ++++++++++++++++++++--------------
 3 files changed, 32 insertions(+), 21 deletions(-)
 create mode 100644 changelog/7259.deprecation.rst

diff --git a/changelog/7259.deprecation.rst b/changelog/7259.deprecation.rst
new file mode 100644
index 00000000000..e450b359991
--- /dev/null
+++ b/changelog/7259.deprecation.rst
@@ -0,0 +1 @@
+``py.path.local`` arguments for hooks have been deprecated. See :ref:`the deprecation note <legacy-path-hooks-deprecated>` for full details.
diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst
index 775bc1958a3..ddddcfb0e6b 100644
--- a/doc/en/deprecations.rst
+++ b/doc/en/deprecations.rst
@@ -19,16 +19,20 @@ Below is a complete list of all pytest features which are considered deprecated.
 :class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
 
 
+.. _legacy-path-hooks-deprecated:
+
 ``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
+
+In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
 
-*  :func:`pytest_ignore_collect(fspath: pathlib.Path) <_pytest.hookspec.pytest_ignore_collect>`
-*  :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_report_header(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_header>`
-*  :func:`pytest_report_collectionfinish(startpath: pathlib.Path) <_pytest.hookspec.pytest_report_collectionfinish>`
+*  :func:`pytest_ignore_collect(fspath: pathlib.Path) <_pytest.hookspec.pytest_ignore_collect>` instead of ``path``
+*  :func:`pytest_collect_file(fspath: pathlib.Path) <_pytest.hookspec.pytest_collect_file>` instead of ``path``
+*  :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.
 
diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py
index 29a713b007d..e3cfc07186e 100644
--- a/src/_pytest/hookspec.py
+++ b/src/_pytest/hookspec.py
@@ -272,12 +272,13 @@ def pytest_ignore_collect(
     Stops at first non-None result, see :ref:`firstresult`.
 
     :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.
 
-    .. versionchanged:: 6.3.0
+    .. versionchanged:: 7.0.0
         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.
 
     :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`
-        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`.
 
     :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`
         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 Path startpath: The starting dir.
-    :param LEGACY_PATH startdir: The starting dir.
+    :param LEGACY_PATH startdir: The starting dir (deprecated).
 
     .. note::
 
@@ -689,9 +693,10 @@ def pytest_report_header(
         files situated at the tests root directory due to how pytest
         :ref:`discovers plugins during startup <pluginorder>`.
 
-    .. versionchanged:: 6.3.0
+    .. versionchanged:: 7.0.0
         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
 
     :param pytest.Config config: The pytest config object.
-    :param Path startpath: The starting path.
-    :param LEGACY_PATH startdir: The starting dir.
+    :param Path startpath: 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.
 
     .. note::
@@ -720,9 +725,10 @@ def pytest_report_collectionfinish(
         If you want to have your line(s) displayed first, use
         :ref:`trylast=True <plugin-hookorder>`.
 
-    .. versionchanged:: 6.3.0
+    .. versionchanged:: 7.0.0
         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.
     """
 
 

From 60ca83746ba107473bbaee870fc8cdd98766961e Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Sat, 23 Oct 2021 22:34:17 +0300
Subject: [PATCH 5/7] docs: change references to 6.3 -> 7.0

The plans have changed, next version will be 7.0 not 6.3.
---
 doc/en/deprecations.rst          | 4 ++--
 src/_pytest/cacheprovider.py     | 2 +-
 src/_pytest/config/argparsing.py | 2 +-
 src/_pytest/main.py              | 2 +-
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst
index ddddcfb0e6b..cfe7f489f7e 100644
--- a/doc/en/deprecations.rst
+++ b/doc/en/deprecations.rst
@@ -63,7 +63,7 @@ Implement the :func:`pytest_load_initial_conftests <_pytest.hookspec.pytest_load
 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,
 however some plugins providing linting/code analysis have been using this as a hack.
@@ -90,7 +90,7 @@ scheduled for removal in pytest 7 (deprecated since pytest 2.4.0):
 Raising ``unittest.SkipTest`` during collection
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. deprecated:: 6.3
+.. deprecated:: 7.0
 
 Raising :class:`unittest.SkipTest` to skip collection of tests during the
 pytest collection phase is deprecated. Use :func:`pytest.skip` instead.
diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py
index 78cec709337..78edf9ac5c6 100755
--- a/src/_pytest/cacheprovider.py
+++ b/src/_pytest/cacheprovider.py
@@ -128,7 +128,7 @@ def mkdir(self, name: str) -> Path:
         it to manage files to e.g. store/retrieve database dumps across test
         sessions.
 
-        .. versionadded:: 6.3
+        .. versionadded:: 7.0
 
         :param name:
             Must be a string not containing a ``/`` separator.
diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py
index b3aff258a78..b0bb3f168ff 100644
--- a/src/_pytest/config/argparsing.py
+++ b/src/_pytest/config/argparsing.py
@@ -185,7 +185,7 @@ def addini(
                 * ``paths``: a list of :class:`pathlib.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.
 
             Defaults to ``string`` if ``None`` or not passed.
diff --git a/src/_pytest/main.py b/src/_pytest/main.py
index cfdc091e1d6..c482224094a 100644
--- a/src/_pytest/main.py
+++ b/src/_pytest/main.py
@@ -500,7 +500,7 @@ def __repr__(self) -> str:
     def startpath(self) -> Path:
         """The path from which pytest was invoked.
 
-        .. versionadded:: 6.3.0
+        .. versionadded:: 7.0.0
         """
         return self.config.invocation_params.dir
 

From 99363ad7ffefcb006183070ada497c9d143ed25e Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Sat, 23 Oct 2021 23:17:35 +0300
Subject: [PATCH 6/7] recwarn: fix was -> were in DID NOT WARN message

---
 doc/en/how-to/capture-warnings.rst | 2 +-
 src/_pytest/recwarn.py             | 6 +++---
 testing/test_recwarn.py            | 8 ++++----
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/doc/en/how-to/capture-warnings.rst b/doc/en/how-to/capture-warnings.rst
index ae76b5bce2f..1a3d1873c41 100644
--- a/doc/en/how-to/capture-warnings.rst
+++ b/doc/en/how-to/capture-warnings.rst
@@ -268,7 +268,7 @@ argument ``match`` to assert that the exception matches a text or regex::
     ...     warnings.warn("this is not here", UserWarning)
     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:
 
diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py
index 950d853f51d..175b571a80c 100644
--- a/src/_pytest/recwarn.py
+++ b/src/_pytest/recwarn.py
@@ -136,7 +136,7 @@ def warns(
         ...     warnings.warn("this is not here", UserWarning)
         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
@@ -274,7 +274,7 @@ def __exit__(
                 if not any(issubclass(r.category, self.expected_warning) for r in self):
                     __tracebackhide__ = True
                     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(
                             self.expected_warning, [each.message for each in self]
                         )
@@ -287,7 +287,7 @@ def __exit__(
                     else:
                         fail(
                             "DID NOT WARN. No warnings of type {} matching"
-                            " ('{}') was emitted. The list of emitted warnings"
+                            " ('{}') were emitted. The list of emitted warnings"
                             " is: {}.".format(
                                 self.expected_warning,
                                 self.match_expr,
diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py
index b82fd9a865b..d3f218f1660 100644
--- a/testing/test_recwarn.py
+++ b/testing/test_recwarn.py
@@ -263,7 +263,7 @@ def test_as_contextmanager(self) -> None:
             with pytest.warns(RuntimeWarning):
                 warnings.warn("user", UserWarning)
         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',?\)\]."
         )
 
@@ -271,7 +271,7 @@ def test_as_contextmanager(self) -> None:
             with pytest.warns(UserWarning):
                 warnings.warn("runtime", RuntimeWarning)
         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',?\)\]."
         )
 
@@ -279,7 +279,7 @@ def test_as_contextmanager(self) -> None:
             with pytest.warns(UserWarning):
                 pass
         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: \[\]."
         )
 
@@ -290,7 +290,7 @@ def test_as_contextmanager(self) -> None:
                 warnings.warn("import", ImportWarning)
 
         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}."
         )
         excinfo.match(

From 7706fd68401c7730ad06103577c10cc4cf031221 Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Sat, 23 Oct 2021 22:05:56 +0300
Subject: [PATCH 7/7] nodes: deprecate fspath arguments to node constructors

This is unfortunately a dependency on `py.path` which cannot be moved to
an external plugins or eased in any way, so has to be deprecated in
order for pytest to be able to eventually remove the dependency on `py`.
---
 changelog/7259.deprecation.rst               |  2 ++
 doc/en/deprecations.rst                      | 19 +++++++++++++++++++
 src/_pytest/deprecated.py                    |  8 ++++++++
 src/_pytest/nodes.py                         | 18 +++++++++++++++---
 testing/deprecated_test.py                   | 13 +++++++++++++
 testing/plugins_integration/requirements.txt |  2 +-
 6 files changed, 58 insertions(+), 4 deletions(-)

diff --git a/changelog/7259.deprecation.rst b/changelog/7259.deprecation.rst
index e450b359991..c0307740d55 100644
--- a/changelog/7259.deprecation.rst
+++ b/changelog/7259.deprecation.rst
@@ -1 +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.
diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst
index cfe7f489f7e..b82dd8521e8 100644
--- a/doc/en/deprecations.rst
+++ b/doc/en/deprecations.rst
@@ -18,6 +18,25 @@ Deprecated Features
 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>`.
 
+.. _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:
 
diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py
index 0c5db6d5335..452128d07b9 100644
--- a/src/_pytest/deprecated.py
+++ b/src/_pytest/deprecated.py
@@ -101,6 +101,14 @@
     "#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(
     "Passing None to catch any warning has been deprecated, pass no arguments instead:\n"
     " Replace pytest.warns(None) by simply pytest.warns()."
diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py
index 33bf1062b02..05cf01fc6c4 100644
--- a/src/_pytest/nodes.py
+++ b/src/_pytest/nodes.py
@@ -28,6 +28,7 @@
 from _pytest.config import Config
 from _pytest.config import ConftestImportFailure
 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 MarkDecorator
 from _pytest.mark.structures import NodeKeywords
@@ -101,7 +102,18 @@ def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
         )
 
 
-def _imply_path(path: Optional[Path], fspath: Optional[LEGACY_PATH]) -> Path:
+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=3,
+        )
     if path is not None:
         if fspath is not None:
             _check_path(path, fspath)
@@ -196,7 +208,7 @@ def __init__(
         #: Filesystem path where this node was collected from (can be None).
         if path is None and fspath is None:
             path = getattr(parent, "path", None)
-        self.path = _imply_path(path, fspath=fspath)
+        self.path = _imply_path(type(self), path, fspath=fspath)
 
         # The explicit annotation is to avoid publicly exposing NodeKeywords.
         #: Keywords/markers collected from all scopes.
@@ -573,7 +585,7 @@ def __init__(
                 assert path is None
                 path = path_or_parent
 
-        path = _imply_path(path, fspath=fspath)
+        path = _imply_path(type(self), path, fspath=fspath)
         if name is None:
             name = path.name
             if parent is not None and parent.path != path:
diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py
index bf796a3396f..0f5e483ce38 100644
--- a/testing/deprecated_test.py
+++ b/testing/deprecated_test.py
@@ -215,3 +215,16 @@ def pytest_cmdline_preparse(config, args):
             "*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"),
+        )
diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt
index 8ef321fd3f8..89e8b0890c5 100644
--- a/testing/plugins_integration/requirements.txt
+++ b/testing/plugins_integration/requirements.txt
@@ -4,7 +4,7 @@ pytest-asyncio==0.16.0
 pytest-bdd==4.1.0
 pytest-cov==3.0.0
 pytest-django==4.4.0
-pytest-flakes==4.0.3
+pytest-flakes==4.0.4
 pytest-html==3.1.1
 pytest-mock==3.6.1
 pytest-rerunfailures==10.2