Skip to content

Commit 15619bf

Browse files
migrate node fs paths to pathlib primary and prepare to drop smart ctors
crosscheck/fix needed for: * fall back to py.path for relative Module names * give collection unification cache keys better names * add warnings for Node implied args WIP NEEDSUNITTESTS * fix Package.from_parent, UNTESTED Co-authored-by: Ran Benita <[email protected]>
1 parent c9e9a59 commit 15619bf

File tree

9 files changed

+190
-113
lines changed

9 files changed

+190
-113
lines changed

src/_pytest/deprecated.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@
6464

6565
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
6666

67+
NODE_IMPLIED_ARG = UnformattedWarning(
68+
PytestDeprecationWarning,
69+
"implying{type.__name__}.{arg} from parent.{arg} has been deprecated\n"
70+
"please use the Node.from_parent to have them be implied by the superclass named ctors",
71+
)
72+
6773

6874
# You want to make some `__init__` or function "private".
6975
#

src/_pytest/doctest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,9 @@ def __init__(
254254
parent: "Union[DoctestTextfile, DoctestModule]",
255255
runner: Optional["doctest.DocTestRunner"] = None,
256256
dtest: Optional["doctest.DocTest"] = None,
257+
**kw: Any,
257258
) -> None:
258-
super().__init__(name, parent)
259+
super().__init__(name=name, parent=parent, **kw)
259260
self.runner = runner
260261
self.dtest = dtest
261262
self.obj = None

src/_pytest/fixtures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
662662
"\n\nRequested here:\n{}:{}".format(
663663
funcitem.nodeid,
664664
fixturedef.argname,
665-
getlocation(fixturedef.func, funcitem.config.rootdir),
665+
getlocation(fixturedef.func, str(funcitem.config.rootpath)),
666666
source_path_str,
667667
source_lineno,
668668
)

src/_pytest/main.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,12 @@ class Session(nodes.FSCollector):
464464

465465
def __init__(self, config: Config) -> None:
466466
super().__init__(
467-
config.rootdir, parent=None, config=config, session=self, nodeid=""
467+
fs_path=config.rootpath,
468+
name="",
469+
parent=None,
470+
config=config,
471+
session=self,
472+
nodeid="",
468473
)
469474
self.testsfailed = 0
470475
self.testscollected = 0
@@ -688,7 +693,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
688693
if col:
689694
if isinstance(col[0], Package):
690695
pkg_roots[str(parent)] = col[0]
691-
node_cache1[Path(col[0].fspath)] = [col[0]]
696+
node_cache1[col[0].fs_path] = [col[0]]
692697

693698
# If it's a directory argument, recurse and look for any Subpackages.
694699
# Let the Package collector deal with subnodes, don't collect here.
@@ -717,7 +722,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
717722
continue
718723

719724
for x in self._collectfile(path):
720-
key2 = (type(x), Path(x.fspath))
725+
key2 = (type(x), x.fs_path)
721726
if key2 in node_cache2:
722727
yield node_cache2[key2]
723728
else:
@@ -749,12 +754,12 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
749754
continue
750755
if not isinstance(node, nodes.Collector):
751756
continue
752-
key = (type(node), node.nodeid)
753-
if key in matchnodes_cache:
754-
rep = matchnodes_cache[key]
757+
key_matchnodes = (type(node), node.nodeid)
758+
if key_matchnodes in matchnodes_cache:
759+
rep = matchnodes_cache[key_matchnodes]
755760
else:
756761
rep = collect_one_node(node)
757-
matchnodes_cache[key] = rep
762+
matchnodes_cache[key_matchnodes] = rep
758763
if rep.passed:
759764
submatchnodes = []
760765
for r in rep.result:

src/_pytest/nodes.py

Lines changed: 131 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from _pytest.config import Config
2727
from _pytest.config import ConftestImportFailure
2828
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
29+
from _pytest.deprecated import NODE_IMPLIED_ARG
2930
from _pytest.mark.structures import Mark
3031
from _pytest.mark.structures import MarkDecorator
3132
from _pytest.mark.structures import NodeKeywords
@@ -110,45 +111,68 @@ class Node(metaclass=NodeMeta):
110111
"parent",
111112
"config",
112113
"session",
113-
"fspath",
114+
"fs_path",
114115
"_nodeid",
115116
"_store",
116117
"__dict__",
117118
)
118119

120+
name: str
121+
parent: Optional["Node"]
122+
config: Config
123+
session: "Session"
124+
fs_path: Path
125+
_nodeid: str
126+
119127
def __init__(
120128
self,
129+
*,
121130
name: str,
122-
parent: "Optional[Node]" = None,
131+
parent: Optional["Node"],
123132
config: Optional[Config] = None,
124-
session: "Optional[Session]" = None,
125-
fspath: Optional[py.path.local] = None,
133+
session: Optional["Session"] = None,
134+
fs_path: Optional[Path] = None,
126135
nodeid: Optional[str] = None,
127136
) -> None:
128137
#: A unique name within the scope of the parent node.
129138
self.name = name
130139

140+
if nodeid is not None:
141+
assert "::()" not in nodeid
142+
else:
143+
assert parent is not None
144+
nodeid = parent.nodeid
145+
if name != "()":
146+
nodeid = f"{nodeid}::{name}"
147+
warnings.warn(NODE_IMPLIED_ARG.format(type=type(self), arg="nodeid"))
148+
self._nodeid = nodeid
131149
#: The parent collector node.
132150
self.parent = parent
133151

134152
#: The pytest config object.
135-
if config:
136-
self.config: Config = config
137-
else:
138-
if not parent:
139-
raise TypeError("config or parent must be provided")
153+
if config is None:
154+
assert parent is not None
140155
self.config = parent.config
156+
warnings.warn(NODE_IMPLIED_ARG.format(type=type(self), arg="config"))
157+
else:
158+
self.config = config
141159

142160
#: The pytest session this node is part of.
143-
if session:
144-
self.session = session
145-
else:
146-
if not parent:
147-
raise TypeError("session or parent must be provided")
161+
if session is None:
162+
assert parent is not None
148163
self.session = parent.session
149164

165+
warnings.warn(NODE_IMPLIED_ARG.format(type=type(self), arg="session"))
166+
else:
167+
self.session = session
168+
150169
#: Filesystem path where this node was collected from (can be None).
151-
self.fspath = fspath or getattr(parent, "fspath", None)
170+
if fs_path is None:
171+
assert parent is not None
172+
self.fs_path = parent.fs_path
173+
warnings.warn(NODE_IMPLIED_ARG.format(type=type(self), arg="fs_path"))
174+
else:
175+
self.fs_path = fs_path
152176

153177
# The explicit annotation is to avoid publicly exposing NodeKeywords.
154178
#: Keywords/markers collected from all scopes.
@@ -160,22 +184,29 @@ def __init__(
160184
#: Allow adding of extra keywords to use for matching.
161185
self.extra_keyword_matches: Set[str] = set()
162186

163-
if nodeid is not None:
164-
assert "::()" not in nodeid
165-
self._nodeid = nodeid
166-
else:
167-
if not self.parent:
168-
raise TypeError("nodeid or parent must be provided")
169-
self._nodeid = self.parent.nodeid
170-
if self.name != "()":
171-
self._nodeid += "::" + self.name
172-
173187
# A place where plugins can store information on the node for their
174188
# own use. Currently only intended for internal plugins.
175189
self._store = Store()
176190

191+
@property
192+
def fspath(self) -> py.path.local:
193+
"""Filesystem path where this node was collected from (can be None).
194+
195+
Since pytest 6.2, prefer to use `fs_path` instead which returns a `pathlib.Path`
196+
instead of a `py.path.local`.
197+
"""
198+
return py.path.local(self.fs_path)
199+
177200
@classmethod
178-
def from_parent(cls, parent: "Node", **kw):
201+
def from_parent(
202+
cls,
203+
parent: "Node",
204+
*,
205+
name: str,
206+
fs_path: Optional[Path] = None,
207+
nodeid: Optional[str] = None,
208+
**kw: Any,
209+
):
179210
"""Public constructor for Nodes.
180211
181212
This indirection got introduced in order to enable removing
@@ -190,7 +221,26 @@ def from_parent(cls, parent: "Node", **kw):
190221
raise TypeError("config is not a valid argument for from_parent")
191222
if "session" in kw:
192223
raise TypeError("session is not a valid argument for from_parent")
193-
return cls._create(parent=parent, **kw)
224+
if nodeid is not None:
225+
assert "::()" not in nodeid
226+
else:
227+
nodeid = parent.nodeid
228+
if name != "()":
229+
nodeid = f"{nodeid}::{name}"
230+
231+
config = parent.config
232+
session = parent.session
233+
if fs_path is None:
234+
fs_path = parent.fs_path
235+
return cls._create(
236+
parent=parent,
237+
config=config,
238+
session=session,
239+
nodeid=nodeid,
240+
name=name,
241+
fs_path=fs_path,
242+
**kw,
243+
)
194244

195245
@property
196246
def ihook(self):
@@ -495,38 +545,58 @@ def _check_initialpaths_for_relpath(
495545

496546

497547
class FSCollector(Collector):
498-
def __init__(
499-
self,
500-
fspath: py.path.local,
501-
parent=None,
502-
config: Optional[Config] = None,
503-
session: Optional["Session"] = None,
548+
def __init__(self, **kw):
549+
550+
fs_path: Optional[Path] = kw.pop("fs_path", None)
551+
fspath: Optional[py.path.local] = kw.pop("fspath", None)
552+
553+
if fspath is not None:
554+
assert fs_path is None
555+
fs_path = Path(fspath)
556+
kw["fs_path"] = fs_path
557+
super().__init__(**kw)
558+
559+
@classmethod
560+
def from_parent(
561+
cls,
562+
parent: Node,
563+
*,
564+
fspath: Optional[py.path.local] = None,
565+
fs_path: Optional[Path] = None,
504566
nodeid: Optional[str] = None,
505-
) -> None:
506-
name = fspath.basename
507-
if parent is not None:
567+
name: Optional[str] = None,
568+
**kw,
569+
):
570+
"""The public constructor."""
571+
if fspath is not None:
572+
assert fs_path is None
573+
known_path = Path(fspath)
574+
else:
575+
assert fs_path is not None
576+
known_path = fs_path
577+
fspath = py.path.local(known_path)
578+
579+
if name is None:
580+
name = known_path.name
508581
rel = fspath.relto(parent.fspath)
509582
if rel:
510-
name = rel
511-
name = name.replace(os.sep, SEP)
512-
self.fspath = fspath
513-
514-
session = session or parent.session
583+
name = str(rel)
515584

516585
if nodeid is None:
517-
nodeid = self.fspath.relto(session.config.rootdir)
586+
assert parent is not None
587+
session = parent.session
588+
relp = session._bestrelpathcache[known_path]
589+
if not relp.startswith(".." + os.sep):
590+
nodeid = str(relp)
518591

519592
if not nodeid:
520593
nodeid = _check_initialpaths_for_relpath(session, fspath)
521594
if nodeid and os.sep != SEP:
522595
nodeid = nodeid.replace(os.sep, SEP)
523596

524-
super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath)
525-
526-
@classmethod
527-
def from_parent(cls, parent, *, fspath, **kw):
528-
"""The public constructor."""
529-
return super().from_parent(parent=parent, fspath=fspath, **kw)
597+
return super().from_parent(
598+
parent=parent, name=name, fs_path=known_path, nodeid=nodeid, **kw
599+
)
530600

531601
def gethookproxy(self, fspath: "os.PathLike[str]"):
532602
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
@@ -555,12 +625,20 @@ class Item(Node):
555625
def __init__(
556626
self,
557627
name,
558-
parent=None,
559-
config: Optional[Config] = None,
560-
session: Optional["Session"] = None,
561-
nodeid: Optional[str] = None,
628+
parent: Node,
629+
config: Config,
630+
session: "Session",
631+
nodeid: str,
632+
fs_path: Path,
562633
) -> None:
563-
super().__init__(name, parent, config, session, nodeid=nodeid)
634+
super().__init__(
635+
name=name,
636+
parent=parent,
637+
config=config,
638+
session=session,
639+
nodeid=nodeid,
640+
fs_path=fs_path,
641+
)
564642
self._report_sections: List[Tuple[str, str, str]] = []
565643

566644
#: A list of tuples (name, value) that holds user defined properties

src/_pytest/pytester.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,7 @@ def copy_example(self, name: Optional[str] = None) -> Path:
900900
example_dir = self._request.config.getini("pytester_example_dir")
901901
if example_dir is None:
902902
raise ValueError("pytester_example_dir is unset, can't copy examples")
903-
example_dir = Path(str(self._request.config.rootdir)) / example_dir
903+
example_dir = self._request.config.rootpath / example_dir
904904

905905
for extra_element in self._request.node.iter_markers("pytester_example_path"):
906906
assert extra_element.args

0 commit comments

Comments
 (0)