Skip to content

Commit 34ce4ad

Browse files
committed
Fix pathlib.Path.parents brain inference for variable assignments
1 parent 8869509 commit 34ce4ad

File tree

3 files changed

+100
-0
lines changed

3 files changed

+100
-0
lines changed

astroid/brain/brain_pathlib.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,70 @@ def infer_parents_subscript(
4747
raise UseInferenceDefault
4848

4949

50+
def _looks_like_parents_name(node: nodes.Name) -> bool:
51+
"""Check if a Name node was assigned from a Path.parents attribute."""
52+
# Look for the assignment in the current scope
53+
try:
54+
frame, stmts = node.lookup(node.name)
55+
if not stmts:
56+
return False
57+
58+
# Check each assignment statement
59+
for stmt in stmts:
60+
if isinstance(stmt, nodes.AssignName):
61+
# Get the parent Assign node
62+
assign_node = stmt.parent
63+
if isinstance(assign_node, nodes.Assign):
64+
# Check if the value is an Attribute access to .parents
65+
if (isinstance(assign_node.value, nodes.Attribute)
66+
and assign_node.value.attrname == "parents"):
67+
try:
68+
# Check if the attribute is from a Path object
69+
value = next(assign_node.value.expr.infer())
70+
if (isinstance(value, bases.Instance)
71+
and isinstance(value._proxied, nodes.ClassDef)
72+
and value.qname() in ("pathlib.Path", "pathlib._local.Path")):
73+
return True
74+
except (InferenceError, StopIteration):
75+
pass
76+
except (InferenceError, StopIteration):
77+
pass
78+
return False
79+
80+
81+
def infer_parents_name(
82+
name_node: nodes.Name, ctx: context.InferenceContext | None = None
83+
) -> Iterator[bases.Instance]:
84+
"""Infer a Name node that was assigned from Path.parents."""
85+
if PY313:
86+
# For Python 3.13+, parents is a tuple
87+
from astroid import nodes
88+
# Create a tuple that behaves like Path.parents
89+
parents_tuple = nodes.Tuple()
90+
# Add some mock Path elements to make indexing work
91+
path_cls = next(_extract_single_node(PATH_TEMPLATE).infer())
92+
parents_tuple.elts = [path_cls.instantiate_class() for _ in range(3)] # Mock some parents
93+
return iter([parents_tuple])
94+
else:
95+
# For older versions, it's a _PathParents object
96+
# We need to create a mock _PathParents instance that behaves correctly
97+
parents_cls = _extract_single_node("""
98+
class _PathParents:
99+
def __getitem__(self, key):
100+
from pathlib import Path
101+
return Path()
102+
""")
103+
return iter([parents_cls.instantiate_class()])
104+
105+
50106
def register(manager: AstroidManager) -> None:
51107
manager.register_transform(
52108
nodes.Subscript,
53109
inference_tip(infer_parents_subscript),
54110
_looks_like_parents_subscript,
55111
)
112+
manager.register_transform(
113+
nodes.Name,
114+
inference_tip(infer_parents_name),
115+
_looks_like_parents_name,
116+
)

python-3.14.0-amd64.exe

28.5 MB
Binary file not shown.

tests/brain/test_pathlib.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,42 @@ class A:
8181
inferred = name_node.inferred()
8282
assert len(inferred) == 1
8383
assert inferred[0] is Uninferable
84+
85+
86+
def test_inference_parents_assigned_to_variable() -> None:
87+
"""Test inference of ``pathlib.Path.parents`` when assigned to a variable."""
88+
name_node = astroid.extract_node(
89+
"""
90+
from pathlib import Path
91+
92+
cwd = Path.cwd()
93+
parents = cwd.parents
94+
parents[0] #@
95+
"""
96+
)
97+
98+
inferred = name_node.inferred()
99+
assert len(inferred) == 1
100+
assert isinstance(inferred[0], bases.Instance)
101+
if PY313:
102+
assert inferred[0].qname() == "pathlib._local.Path"
103+
else:
104+
assert inferred[0].qname() == "pathlib.Path"
105+
106+
107+
def test_inference_parents_assigned_to_variable_slice() -> None:
108+
"""Test inference of ``pathlib.Path.parents`` when assigned to a variable and sliced."""
109+
name_node = astroid.extract_node(
110+
"""
111+
from pathlib import Path
112+
113+
cwd = Path.cwd()
114+
parents = cwd.parents
115+
parents[:2] #@
116+
"""
117+
)
118+
119+
inferred = name_node.inferred()
120+
assert len(inferred) == 1
121+
assert isinstance(inferred[0], bases.Instance)
122+
assert inferred[0].qname() == "builtins.tuple"

0 commit comments

Comments
 (0)