-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
migrate node fs paths to pathlib primary and reorg ctors/from_parent #8037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
424f826
ebc5d75
b7b4162
9749629
e9936de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Deprecate implying node ctor arguments. | ||
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1 @@ | ||||||
Introduce ``Node.fs_path`` as pathlib replacement for ``Node.fspath`` | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Also I would add another paragraph: This is part of our continuous effort of eventually phasing out pytest's dependency to `py <https://py.readthedocs.io/en/latest/>`__. |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -64,6 +64,12 @@ | |||||||||
|
||||||||||
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") | ||||||||||
|
||||||||||
NODE_IMPLIED_ARG = UnformattedWarning( | ||||||||||
PytestDeprecationWarning, | ||||||||||
"implying {type.__name__}.{arg} from parent.{arg} has been deprecated\n" | ||||||||||
"please use the Node.from_parent to have them be implied by the superclass named ctors", | ||||||||||
Comment on lines
+69
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
) | ||||||||||
|
||||||||||
|
||||||||||
# You want to make some `__init__` or function "private". | ||||||||||
# | ||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -26,6 +26,7 @@ | |||||
from _pytest.config import Config | ||||||
from _pytest.config import ConftestImportFailure | ||||||
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH | ||||||
from _pytest.deprecated import NODE_IMPLIED_ARG | ||||||
from _pytest.mark.structures import Mark | ||||||
from _pytest.mark.structures import MarkDecorator | ||||||
from _pytest.mark.structures import NodeKeywords | ||||||
|
@@ -110,45 +111,67 @@ class Node(metaclass=NodeMeta): | |||||
"parent", | ||||||
"config", | ||||||
"session", | ||||||
"fspath", | ||||||
"fs_path", | ||||||
"_nodeid", | ||||||
"_store", | ||||||
"__dict__", | ||||||
) | ||||||
|
||||||
name: str | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is mypy not able to figure these out on its own? |
||||||
config: Config | ||||||
session: "Session" | ||||||
fs_path: Path | ||||||
_nodeid: str | ||||||
|
||||||
def __init__( | ||||||
self, | ||||||
*, | ||||||
name: str, | ||||||
parent: "Optional[Node]" = None, | ||||||
parent: Optional["Node"], | ||||||
RonnyPfannschmidt marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
config: Optional[Config] = None, | ||||||
session: "Optional[Session]" = None, | ||||||
fspath: Optional[py.path.local] = None, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By dropping |
||||||
session: Optional["Session"] = None, | ||||||
fs_path: Optional[Path] = None, | ||||||
nodeid: Optional[str] = None, | ||||||
) -> None: | ||||||
#: A unique name within the scope of the parent node. | ||||||
self.name = name | ||||||
|
||||||
if nodeid is not None: | ||||||
assert "::()" not in nodeid | ||||||
else: | ||||||
assert parent is not None | ||||||
nodeid = parent.nodeid | ||||||
if name != "()": | ||||||
nodeid = f"{nodeid}::{name}" | ||||||
warnings.warn(NODE_IMPLIED_ARG.format(type=type(self), arg="nodeid")) | ||||||
self._nodeid = nodeid | ||||||
#: The parent collector node. | ||||||
self.parent = parent | ||||||
|
||||||
#: The pytest config object. | ||||||
if config: | ||||||
self.config: Config = config | ||||||
else: | ||||||
if not parent: | ||||||
raise TypeError("config or parent must be provided") | ||||||
if config is None: | ||||||
assert parent is not None | ||||||
self.config = parent.config | ||||||
warnings.warn(NODE_IMPLIED_ARG.format(type=type(self), arg="config")) | ||||||
else: | ||||||
self.config = config | ||||||
|
||||||
#: The pytest session this node is part of. | ||||||
if session: | ||||||
self.session = session | ||||||
else: | ||||||
if not parent: | ||||||
raise TypeError("session or parent must be provided") | ||||||
if session is None: | ||||||
assert parent is not None | ||||||
self.session = parent.session | ||||||
|
||||||
warnings.warn(NODE_IMPLIED_ARG.format(type=type(self), arg="session")) | ||||||
else: | ||||||
self.session = session | ||||||
|
||||||
#: Filesystem path where this node was collected from (can be None). | ||||||
self.fspath = fspath or getattr(parent, "fspath", None) | ||||||
if fs_path is None: | ||||||
assert parent is not None | ||||||
self.fs_path = parent.fs_path | ||||||
warnings.warn(NODE_IMPLIED_ARG.format(type=type(self), arg="fs_path")) | ||||||
else: | ||||||
self.fs_path = fs_path | ||||||
|
||||||
# The explicit annotation is to avoid publicly exposing NodeKeywords. | ||||||
#: Keywords/markers collected from all scopes. | ||||||
|
@@ -160,22 +183,29 @@ def __init__( | |||||
#: Allow adding of extra keywords to use for matching. | ||||||
self.extra_keyword_matches: Set[str] = set() | ||||||
|
||||||
if nodeid is not None: | ||||||
assert "::()" not in nodeid | ||||||
self._nodeid = nodeid | ||||||
else: | ||||||
if not self.parent: | ||||||
raise TypeError("nodeid or parent must be provided") | ||||||
self._nodeid = self.parent.nodeid | ||||||
if self.name != "()": | ||||||
self._nodeid += "::" + self.name | ||||||
|
||||||
# A place where plugins can store information on the node for their | ||||||
# own use. Currently only intended for internal plugins. | ||||||
self._store = Store() | ||||||
|
||||||
@property | ||||||
def fspath(self) -> py.path.local: | ||||||
"""Filesystem path where this node was collected from (can be None). | ||||||
|
||||||
Since pytest 6.2, prefer to use `fs_path` instead which returns a `pathlib.Path` | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
instead of a `py.path.local`. | ||||||
""" | ||||||
return py.path.local(self.fs_path) | ||||||
|
||||||
@classmethod | ||||||
def from_parent(cls, parent: "Node", **kw): | ||||||
def from_parent( | ||||||
cls, | ||||||
parent: "Node", | ||||||
*, | ||||||
name: str, | ||||||
fs_path: Optional[Path] = None, | ||||||
nodeid: Optional[str] = None, | ||||||
**kw: Any, | ||||||
): | ||||||
"""Public constructor for Nodes. | ||||||
|
||||||
This indirection got introduced in order to enable removing | ||||||
|
@@ -190,7 +220,26 @@ def from_parent(cls, parent: "Node", **kw): | |||||
raise TypeError("config is not a valid argument for from_parent") | ||||||
if "session" in kw: | ||||||
raise TypeError("session is not a valid argument for from_parent") | ||||||
return cls._create(parent=parent, **kw) | ||||||
if nodeid is not None: | ||||||
assert "::()" not in nodeid | ||||||
else: | ||||||
nodeid = parent.nodeid | ||||||
if name != "()": | ||||||
nodeid = f"{nodeid}::{name}" | ||||||
|
||||||
config = parent.config | ||||||
session = parent.session | ||||||
if fs_path is None: | ||||||
fs_path = parent.fs_path | ||||||
return cls._create( | ||||||
parent=parent, | ||||||
config=config, | ||||||
session=session, | ||||||
nodeid=nodeid, | ||||||
name=name, | ||||||
fs_path=fs_path, | ||||||
**kw, | ||||||
) | ||||||
|
||||||
@property | ||||||
def ihook(self): | ||||||
|
@@ -495,38 +544,60 @@ def _check_initialpaths_for_relpath( | |||||
|
||||||
|
||||||
class FSCollector(Collector): | ||||||
def __init__( | ||||||
self, | ||||||
fspath: py.path.local, | ||||||
parent=None, | ||||||
config: Optional[Config] = None, | ||||||
session: Optional["Session"] = None, | ||||||
def __init__(self, **kw): | ||||||
|
||||||
fs_path: Optional[Path] = kw.pop("fs_path", None) | ||||||
fspath: Optional[py.path.local] = kw.pop("fspath", None) | ||||||
|
||||||
if fspath is not None: | ||||||
assert fs_path is None | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
fs_path = Path(fspath) | ||||||
kw["fs_path"] = fs_path | ||||||
super().__init__(**kw) | ||||||
|
||||||
@classmethod | ||||||
def from_parent( | ||||||
cls, | ||||||
parent: Node, | ||||||
*, | ||||||
fspath: Optional[py.path.local] = None, | ||||||
fs_path: Optional[Path] = None, | ||||||
nodeid: Optional[str] = None, | ||||||
) -> None: | ||||||
name = fspath.basename | ||||||
if parent is not None: | ||||||
name: Optional[str] = None, | ||||||
RonnyPfannschmidt marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
**kw, | ||||||
): | ||||||
"""The public constructor.""" | ||||||
if fspath is not None: | ||||||
assert ( | ||||||
fs_path is None | ||||||
), "fs_path and fspath cannot both be passed at the same time" | ||||||
known_path = Path(fspath) | ||||||
else: | ||||||
assert fs_path is not None, "fs_path or fspath must be given" | ||||||
known_path = fs_path | ||||||
fspath = py.path.local(known_path) | ||||||
|
||||||
if name is None: | ||||||
name = known_path.name | ||||||
rel = fspath.relto(parent.fspath) | ||||||
if rel: | ||||||
name = rel | ||||||
name = name.replace(os.sep, SEP) | ||||||
self.fspath = fspath | ||||||
|
||||||
session = session or parent.session | ||||||
name = str(rel) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will end up with a name containing Also >>> local("src/pytest").relto(local("."))
'src\\pytest' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. relto is from py.path.local - relative pathlib paths are relative There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh I see, thanks, but does the comment about >>> local("src/pytest").relto(local("."))
'src\\pytest' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh OK so this needs fixing then. 👍 |
||||||
|
||||||
if nodeid is None: | ||||||
nodeid = self.fspath.relto(session.config.rootdir) | ||||||
assert parent is not None | ||||||
session = parent.session | ||||||
relp = session._bestrelpathcache[known_path] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice |
||||||
if not relp.startswith(".." + os.sep): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have a comment here explaining the purpose of this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gentle ping |
||||||
nodeid = str(relp) | ||||||
|
||||||
if not nodeid: | ||||||
nodeid = _check_initialpaths_for_relpath(session, fspath) | ||||||
if nodeid and os.sep != SEP: | ||||||
nodeid = nodeid.replace(os.sep, SEP) | ||||||
|
||||||
super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath) | ||||||
|
||||||
@classmethod | ||||||
def from_parent(cls, parent, *, fspath, **kw): | ||||||
"""The public constructor.""" | ||||||
return super().from_parent(parent=parent, fspath=fspath, **kw) | ||||||
return super().from_parent( | ||||||
parent=parent, name=name, fs_path=known_path, nodeid=nodeid, **kw | ||||||
) | ||||||
|
||||||
def gethookproxy(self, fspath: "os.PathLike[str]"): | ||||||
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) | ||||||
|
@@ -555,12 +626,20 @@ class Item(Node): | |||||
def __init__( | ||||||
self, | ||||||
name, | ||||||
parent=None, | ||||||
config: Optional[Config] = None, | ||||||
session: Optional["Session"] = None, | ||||||
nodeid: Optional[str] = None, | ||||||
parent: Node, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this one i missed while restoring ctor based implication |
||||||
config: Config, | ||||||
session: "Session", | ||||||
nodeid: str, | ||||||
fs_path: Path, | ||||||
) -> None: | ||||||
super().__init__(name, parent, config, session, nodeid=nodeid) | ||||||
super().__init__( | ||||||
name=name, | ||||||
parent=parent, | ||||||
config=config, | ||||||
session=session, | ||||||
nodeid=nodeid, | ||||||
fs_path=fs_path, | ||||||
) | ||||||
self._report_sections: List[Tuple[str, str, str]] = [] | ||||||
|
||||||
#: A list of tuples (name, value) that holds user defined properties | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if I read this sentence as a user I would not be able to grok what it's about, so some more details would be good here, like what exactly is deprecated, and how to fix.
Also we usually add a corresponding note in the
docs/deprecations.rst
file (can just be a copy/paste of the changelog entry mostly).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. 👍