Skip to content

Commit 181642f

Browse files
cdce8phippo91
andauthored
Solves "Duplicates found in MROs" false positives. (#905, #916)
* Adds inference support for all typing types that are defined through _alias function * Instead of creating a new class (by the mean of TYPING_TYPE_TEMPLATE) infer the origin class : i.e MutableSet = _alias(collections.MutableSet ...) origin is the class in collections module. Needs to add __getitem method on its metaclass so that is support indexing (MutableSet[T]). * Enable _alias mocking and testing only if python version is at least 3.7 Co-authored-by: hippo91 <[email protected]>
1 parent 8c2a13f commit 181642f

File tree

3 files changed

+223
-0
lines changed

3 files changed

+223
-0
lines changed

ChangeLog

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ Release Date: 2021-02-28
1515

1616
* Improve typing.TypedDict inference
1717

18+
* Fix the `Duplicates found in MROs` false positive.
19+
20+
Closes #905
21+
Closes PyCQA/pylint#2717
22+
Closes PyCQA/pylint#3247
23+
Closes PyCQA/pylint#4093
24+
Closes PyCQA/pylint#4131
25+
Closes PyCQA/pylint#4145
26+
1827

1928
What's New in astroid 2.5?
2029
============================

astroid/brain/brain_typing.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@
88
"""Astroid hooks for typing.py support."""
99
import sys
1010
import typing
11+
from functools import lru_cache
1112

1213
from astroid import (
1314
MANAGER,
1415
UseInferenceDefault,
1516
extract_node,
1617
inference_tip,
18+
node_classes,
1719
nodes,
1820
context,
1921
InferenceError,
2022
)
23+
import astroid
2124

25+
PY37 = sys.version_info[:2] >= (3, 7)
2226
PY39 = sys.version_info[:2] >= (3, 9)
2327

2428
TYPING_NAMEDTUPLE_BASENAMES = {"NamedTuple", "typing.NamedTuple"}
@@ -112,6 +116,98 @@ def infer_typedDict( # pylint: disable=invalid-name
112116
node.root().locals["TypedDict"] = [class_def]
113117

114118

119+
GET_ITEM_TEMPLATE = """
120+
@classmethod
121+
def __getitem__(cls, value):
122+
return cls
123+
"""
124+
125+
ABC_METACLASS_TEMPLATE = """
126+
from abc import ABCMeta
127+
ABCMeta
128+
"""
129+
130+
131+
@lru_cache()
132+
def create_typing_metaclass():
133+
#  Needs to mock the __getitem__ class method so that
134+
#  MutableSet[T] is acceptable
135+
func_to_add = extract_node(GET_ITEM_TEMPLATE)
136+
137+
abc_meta = next(extract_node(ABC_METACLASS_TEMPLATE).infer())
138+
typing_meta = nodes.ClassDef(
139+
name="ABCMeta_typing",
140+
lineno=abc_meta.lineno,
141+
col_offset=abc_meta.col_offset,
142+
parent=abc_meta.parent,
143+
)
144+
typing_meta.postinit(
145+
bases=[extract_node(ABC_METACLASS_TEMPLATE)], body=[], decorators=None
146+
)
147+
typing_meta.locals["__getitem__"] = [func_to_add]
148+
return typing_meta
149+
150+
151+
def _looks_like_typing_alias(node: nodes.Call) -> bool:
152+
"""
153+
Returns True if the node corresponds to a call to _alias function.
154+
For example :
155+
156+
MutableSet = _alias(collections.abc.MutableSet, T)
157+
158+
:param node: call node
159+
"""
160+
return (
161+
isinstance(node, nodes.Call)
162+
and isinstance(node.func, nodes.Name)
163+
and node.func.name == "_alias"
164+
and isinstance(node.args[0], nodes.Attribute)
165+
)
166+
167+
168+
def infer_typing_alias(
169+
node: nodes.Call, ctx: context.InferenceContext = None
170+
) -> typing.Optional[node_classes.NodeNG]:
171+
"""
172+
Infers the call to _alias function
173+
174+
:param node: call node
175+
:param context: inference context
176+
"""
177+
if not isinstance(node, nodes.Call):
178+
return None
179+
res = next(node.args[0].infer(context=ctx))
180+
181+
if res != astroid.Uninferable and isinstance(res, nodes.ClassDef):
182+
class_def = nodes.ClassDef(
183+
name=f"{res.name}_typing",
184+
lineno=0,
185+
col_offset=0,
186+
parent=res.parent,
187+
)
188+
class_def.postinit(
189+
bases=[res],
190+
body=res.body,
191+
decorators=res.decorators,
192+
metaclass=create_typing_metaclass(),
193+
)
194+
return class_def
195+
196+
if len(node.args) == 2 and isinstance(node.args[0], nodes.Attribute):
197+
class_def = nodes.ClassDef(
198+
name=node.args[0].attrname,
199+
lineno=0,
200+
col_offset=0,
201+
parent=node.parent,
202+
)
203+
class_def.postinit(
204+
bases=[], body=[], decorators=None, metaclass=create_typing_metaclass()
205+
)
206+
return class_def
207+
208+
return None
209+
210+
115211
MANAGER.register_transform(
116212
nodes.Call,
117213
inference_tip(infer_typing_typevar_or_newtype),
@@ -125,3 +221,6 @@ def infer_typedDict( # pylint: disable=invalid-name
125221
MANAGER.register_transform(
126222
nodes.FunctionDef, infer_typedDict, _looks_like_typedDict
127223
)
224+
225+
if PY37:
226+
MANAGER.register_transform(nodes.Call, infer_typing_alias, _looks_like_typing_alias)

tests/unittest_brain.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@
9999
import astroid.test_utils as test_utils
100100

101101

102+
def assertEqualMro(klass, expected_mro):
103+
"""Check mro names."""
104+
assert [member.name for member in klass.mro()] == expected_mro
105+
106+
102107
class HashlibTest(unittest.TestCase):
103108
def _assert_hashlib_class(self, class_obj):
104109
self.assertIn("update", class_obj)
@@ -1206,6 +1211,116 @@ class CustomTD(TypedDict):
12061211
assert len(typing_module.locals["TypedDict"]) == 1
12071212
assert inferred_base == typing_module.locals["TypedDict"][0]
12081213

1214+
@test_utils.require_version("3.8")
1215+
def test_typing_alias_type(self):
1216+
"""
1217+
Test that the type aliased thanks to typing._alias function are
1218+
correctly inferred.
1219+
"""
1220+
1221+
def check_metaclass(node: nodes.ClassDef):
1222+
meta = node.metaclass()
1223+
assert isinstance(meta, nodes.ClassDef)
1224+
assert meta.name == "ABCMeta_typing"
1225+
assert "ABCMeta" == meta.basenames[0]
1226+
assert meta.locals.get("__getitem__") is not None
1227+
1228+
abc_meta = next(meta.bases[0].infer())
1229+
assert isinstance(abc_meta, nodes.ClassDef)
1230+
assert abc_meta.name == "ABCMeta"
1231+
assert abc_meta.locals.get("__getitem__") is None
1232+
1233+
node = builder.extract_node(
1234+
"""
1235+
from typing import TypeVar, MutableSet
1236+
1237+
T = TypeVar("T")
1238+
MutableSet[T]
1239+
1240+
class Derived1(MutableSet[T]):
1241+
pass
1242+
"""
1243+
)
1244+
inferred = next(node.infer())
1245+
check_metaclass(inferred)
1246+
assertEqualMro(
1247+
inferred,
1248+
[
1249+
"Derived1",
1250+
"MutableSet_typing",
1251+
"MutableSet",
1252+
"Set",
1253+
"Collection",
1254+
"Sized",
1255+
"Iterable",
1256+
"Container",
1257+
"object",
1258+
],
1259+
)
1260+
1261+
node = builder.extract_node(
1262+
"""
1263+
import typing
1264+
class Derived2(typing.OrderedDict[int, str]):
1265+
pass
1266+
"""
1267+
)
1268+
inferred = next(node.infer())
1269+
check_metaclass(inferred)
1270+
assertEqualMro(
1271+
inferred,
1272+
[
1273+
"Derived2",
1274+
"OrderedDict_typing",
1275+
"OrderedDict",
1276+
"dict",
1277+
"object",
1278+
],
1279+
)
1280+
1281+
node = builder.extract_node(
1282+
"""
1283+
import typing
1284+
class Derived3(typing.Pattern[str]):
1285+
pass
1286+
"""
1287+
)
1288+
inferred = next(node.infer())
1289+
check_metaclass(inferred)
1290+
assertEqualMro(
1291+
inferred,
1292+
[
1293+
"Derived3",
1294+
"Pattern",
1295+
"object",
1296+
],
1297+
)
1298+
1299+
@test_utils.require_version("3.8")
1300+
def test_typing_alias_side_effects(self):
1301+
"""Test that typing._alias changes doesn't have unwanted consequences."""
1302+
node = builder.extract_node(
1303+
"""
1304+
import typing
1305+
import collections.abc
1306+
1307+
class Derived(collections.abc.Iterator[int]):
1308+
pass
1309+
"""
1310+
)
1311+
inferred = next(node.infer())
1312+
assert inferred.metaclass() is None # Should this be ABCMeta?
1313+
assertEqualMro(
1314+
inferred,
1315+
[
1316+
"Derived",
1317+
# Should this be more?
1318+
# "Iterator_typing"?
1319+
# "Iterator",
1320+
# "object",
1321+
],
1322+
)
1323+
12091324

12101325
class ReBrainTest(unittest.TestCase):
12111326
def test_regex_flags(self):

0 commit comments

Comments
 (0)