Skip to content

Commit 9b114ad

Browse files
deepyamanDanielNoordcdce8p
authored
Add pathlib brain to handle .parents inference (#1442)
Co-authored-by: Daniël van Noord <[email protected]> Co-authored-by: Marc Mueller <[email protected]>
1 parent c4b59dd commit 9b114ad

File tree

3 files changed

+143
-0
lines changed

3 files changed

+143
-0
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ Release date: TBA
5454

5555
Closes #1403
5656

57+
* Add ``pathlib`` brain to handle ``pathlib.PurePath.parents`` inference.
58+
59+
Closes PyCQA/pylint#5783
60+
5761
* Fix test for Python ``3.11``. In some instances ``err.__traceback__`` will
5862
be uninferable now.
5963

astroid/brain/brain_pathlib.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
2+
# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
3+
# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
4+
5+
from __future__ import annotations
6+
7+
from collections.abc import Iterator
8+
9+
from astroid import bases, context, inference_tip, nodes
10+
from astroid.builder import _extract_single_node
11+
from astroid.exceptions import InferenceError, UseInferenceDefault
12+
from astroid.manager import AstroidManager
13+
14+
PATH_TEMPLATE = """
15+
from pathlib import Path
16+
Path
17+
"""
18+
19+
20+
def _looks_like_parents_subscript(node: nodes.Subscript) -> bool:
21+
if not (
22+
isinstance(node.value, nodes.Name)
23+
or isinstance(node.value, nodes.Attribute)
24+
and node.value.attrname == "parents"
25+
):
26+
return False
27+
28+
try:
29+
value = next(node.value.infer())
30+
except (InferenceError, StopIteration):
31+
return False
32+
return (
33+
isinstance(value, bases.Instance)
34+
and isinstance(value._proxied, nodes.ClassDef)
35+
and value.qname() == "pathlib._PathParents"
36+
)
37+
38+
39+
def infer_parents_subscript(
40+
subscript_node: nodes.Subscript, ctx: context.InferenceContext | None = None
41+
) -> Iterator[bases.Instance]:
42+
if isinstance(subscript_node.slice, nodes.Const):
43+
path_cls = next(_extract_single_node(PATH_TEMPLATE).infer())
44+
return iter([path_cls.instantiate_class()])
45+
46+
raise UseInferenceDefault
47+
48+
49+
AstroidManager().register_transform(
50+
nodes.Subscript,
51+
inference_tip(infer_parents_subscript),
52+
_looks_like_parents_subscript,
53+
)

tests/unittest_brain_pathlib.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
2+
# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
3+
# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
4+
5+
6+
import astroid
7+
from astroid import bases
8+
from astroid.const import PY310_PLUS
9+
from astroid.util import Uninferable
10+
11+
12+
def test_inference_parents() -> None:
13+
"""Test inference of ``pathlib.Path.parents``."""
14+
name_node = astroid.extract_node(
15+
"""
16+
from pathlib import Path
17+
18+
current_path = Path().resolve()
19+
path_parents = current_path.parents
20+
path_parents
21+
"""
22+
)
23+
inferred = name_node.inferred()
24+
assert len(inferred) == 1
25+
assert isinstance(inferred[0], bases.Instance)
26+
assert inferred[0].qname() == "pathlib._PathParents"
27+
28+
29+
def test_inference_parents_subscript_index() -> None:
30+
"""Test inference of ``pathlib.Path.parents``, accessed by index."""
31+
parents, path = astroid.extract_node(
32+
"""
33+
from pathlib import Path
34+
35+
current_path = Path().resolve()
36+
path_parents = current_path.parents
37+
path_parents #@
38+
path_parents[2] #@
39+
"""
40+
)
41+
inferred = parents.inferred()
42+
assert len(inferred) == 1
43+
assert isinstance(inferred[0], bases.Instance)
44+
assert inferred[0].qname() == "pathlib._PathParents"
45+
46+
inferred = path.inferred()
47+
assert len(inferred) == 1
48+
assert isinstance(inferred[0], bases.Instance)
49+
assert inferred[0].qname() == "pathlib.Path"
50+
51+
52+
def test_inference_parents_subscript_slice() -> None:
53+
"""Test inference of ``pathlib.Path.parents``, accessed by slice."""
54+
name_node = astroid.extract_node(
55+
"""
56+
from pathlib import Path
57+
58+
current_path = Path().resolve()
59+
parent_path = current_path.parents[:2]
60+
parent_path
61+
"""
62+
)
63+
inferred = name_node.inferred()
64+
assert len(inferred) == 1
65+
if PY310_PLUS:
66+
assert isinstance(inferred[0], bases.Instance)
67+
assert inferred[0].qname() == "builtins.tuple"
68+
else:
69+
assert inferred[0] is Uninferable
70+
71+
72+
def test_inference_parents_subscript_not_path() -> None:
73+
"""Test inference of other ``.parents`` subscripts is unaffected."""
74+
name_node = astroid.extract_node(
75+
"""
76+
class A:
77+
parents = 42
78+
79+
c = A()
80+
error = c.parents[:2]
81+
error
82+
"""
83+
)
84+
inferred = name_node.inferred()
85+
assert len(inferred) == 1
86+
assert inferred[0] is Uninferable

0 commit comments

Comments
 (0)