Skip to content

Commit 1419ac5

Browse files
authored
Bug pylint 4326 (#1183)
* Adds unittest dealing with class subscript * Adds support of type hints inside numpy's brains * Adds unit test to check astroid does not crash if numpy is not available
1 parent f9c1b31 commit 1419ac5

File tree

6 files changed

+159
-7
lines changed

6 files changed

+159
-7
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ What's New in astroid 2.8.1?
1212
============================
1313
Release date: TBA
1414

15+
* Adds support of type hints inside numpy's brains.
16+
17+
Closes PyCQA/pylint#4326
18+
1519
* Enable inference of dataclass import from pydantic.dataclasses.
1620
This allows the dataclasses brain to recognize pydantic dataclasses.
1721

astroid/brain/brain_numpy_core_numerictypes.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# TODO(hippo91) : correct the methods signature.
1010

1111
"""Astroid hooks for numpy.core.numerictypes module."""
12+
from astroid.brain.brain_numpy_utils import numpy_supports_type_hints
1213
from astroid.brain.helpers import register_module_extender
1314
from astroid.builder import parse
1415
from astroid.manager import AstroidManager
@@ -19,9 +20,7 @@ def numpy_core_numerictypes_transform():
1920
# According to numpy doc the generic object should expose
2021
# the same API than ndarray. This has been done here partially
2122
# through the astype method.
22-
return parse(
23-
"""
24-
# different types defined in numerictypes.py
23+
generic_src = """
2524
class generic(object):
2625
def __init__(self, value):
2726
self.T = np.ndarray([0, 0])
@@ -106,8 +105,16 @@ def trace(self): return uninferable
106105
def transpose(self): return uninferable
107106
def var(self): return uninferable
108107
def view(self): return uninferable
109-
110-
108+
"""
109+
if numpy_supports_type_hints():
110+
generic_src += """
111+
@classmethod
112+
def __class_getitem__(cls, value):
113+
return cls
114+
"""
115+
return parse(
116+
generic_src
117+
+ """
111118
class dtype(object):
112119
def __init__(self, obj, align=False, copy=False):
113120
self.alignment = None

astroid/brain/brain_numpy_ndarray.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010

1111
"""Astroid hooks for numpy ndarray class."""
12+
from astroid.brain.brain_numpy_utils import numpy_supports_type_hints
1213
from astroid.builder import extract_node
1314
from astroid.inference_tip import inference_tip
1415
from astroid.manager import AstroidManager
@@ -143,6 +144,12 @@ def transpose(self, *axes): return np.ndarray([0, 0])
143144
def var(self, axis=None, dtype=None, out=None, ddof=0, keepdims=False): return np.ndarray([0, 0])
144145
def view(self, dtype=None, type=None): return np.ndarray([0, 0])
145146
"""
147+
if numpy_supports_type_hints():
148+
ndarray += """
149+
@classmethod
150+
def __class_getitem__(cls, value):
151+
return cls
152+
"""
146153
node = extract_node(ndarray)
147154
return node.infer(context=context)
148155

astroid/brain/brain_numpy_utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,35 @@
88

99

1010
"""Different utilities for the numpy brains"""
11+
from typing import Tuple
12+
1113
from astroid.builder import extract_node
1214
from astroid.nodes.node_classes import Attribute, Import, Name, NodeNG
1315

16+
# Class subscript is available in numpy starting with version 1.20.0
17+
NUMPY_VERSION_TYPE_HINTS_SUPPORT = ("1", "20", "0")
18+
19+
20+
def numpy_supports_type_hints() -> bool:
21+
"""
22+
Returns True if numpy supports type hints
23+
"""
24+
np_ver = _get_numpy_version()
25+
return np_ver and np_ver > NUMPY_VERSION_TYPE_HINTS_SUPPORT
26+
27+
28+
def _get_numpy_version() -> Tuple[str, str, str]:
29+
"""
30+
Return the numpy version number if numpy can be imported. Otherwise returns
31+
('0', '0', '0')
32+
"""
33+
try:
34+
import numpy # pylint: disable=import-outside-toplevel
35+
36+
return tuple(numpy.version.version.split("."))
37+
except ImportError:
38+
return ("0", "0", "0")
39+
1440

1541
def infer_numpy_member(src, node, context=None):
1642
node = extract_node(src)

tests/unittest_brain_numpy_core_numerictypes.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
except ImportError:
1717
HAS_NUMPY = False
1818

19-
from astroid import builder, nodes
19+
from astroid import Uninferable, builder, nodes
20+
from astroid.brain.brain_numpy_utils import (
21+
NUMPY_VERSION_TYPE_HINTS_SUPPORT,
22+
_get_numpy_version,
23+
numpy_supports_type_hints,
24+
)
2025

2126

2227
@unittest.skipUnless(HAS_NUMPY, "This test requires the numpy library.")
@@ -341,6 +346,88 @@ def test_datetime_astype_return(self):
341346
),
342347
)
343348

349+
@unittest.skipUnless(
350+
HAS_NUMPY and numpy_supports_type_hints(),
351+
f"This test requires the numpy library with a version above {NUMPY_VERSION_TYPE_HINTS_SUPPORT}",
352+
)
353+
def test_generic_types_are_subscriptables(self):
354+
"""
355+
Test that all types deriving from generic are subscriptables
356+
"""
357+
for type_ in (
358+
"bool_",
359+
"bytes_",
360+
"character",
361+
"complex128",
362+
"complex192",
363+
"complex64",
364+
"complexfloating",
365+
"datetime64",
366+
"flexible",
367+
"float16",
368+
"float32",
369+
"float64",
370+
"float96",
371+
"floating",
372+
"generic",
373+
"inexact",
374+
"int16",
375+
"int32",
376+
"int32",
377+
"int64",
378+
"int8",
379+
"integer",
380+
"number",
381+
"signedinteger",
382+
"str_",
383+
"timedelta64",
384+
"uint16",
385+
"uint32",
386+
"uint32",
387+
"uint64",
388+
"uint8",
389+
"unsignedinteger",
390+
"void",
391+
):
392+
with self.subTest(type_=type_):
393+
src = f"""
394+
import numpy as np
395+
np.{type_}[int]
396+
"""
397+
node = builder.extract_node(src)
398+
cls_node = node.inferred()[0]
399+
self.assertIsInstance(cls_node, nodes.ClassDef)
400+
self.assertEqual(cls_node.name, type_)
401+
402+
403+
@unittest.skipIf(
404+
HAS_NUMPY, "Those tests check that astroid does not crash if numpy is not available"
405+
)
406+
class NumpyBrainUtilsTest(unittest.TestCase):
407+
"""
408+
This class is dedicated to test that astroid does not crash
409+
if numpy module is not available
410+
"""
411+
412+
def test_get_numpy_version_do_not_crash(self):
413+
"""
414+
Test that the function _get_numpy_version doesn't crash even if numpy is not installed
415+
"""
416+
self.assertEqual(_get_numpy_version(), ("0", "0", "0"))
417+
418+
def test_numpy_object_uninferable(self):
419+
"""
420+
Test that in case numpy is not available, then a numpy object is uninferable
421+
but the inference doesn't lead to a crash
422+
"""
423+
src = """
424+
import numpy as np
425+
np.number[int]
426+
"""
427+
node = builder.extract_node(src)
428+
cls_node = node.inferred()[0]
429+
self.assertIs(cls_node, Uninferable)
430+
344431

345432
if __name__ == "__main__":
346433
unittest.main()

tests/unittest_brain_numpy_ndarray.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
except ImportError:
1717
HAS_NUMPY = False
1818

19-
from astroid import builder
19+
from astroid import builder, nodes
20+
from astroid.brain.brain_numpy_utils import (
21+
NUMPY_VERSION_TYPE_HINTS_SUPPORT,
22+
numpy_supports_type_hints,
23+
)
2024

2125

2226
@unittest.skipUnless(HAS_NUMPY, "This test requires the numpy library.")
@@ -161,6 +165,23 @@ def test_numpy_ndarray_attribute_inferred_as_ndarray(self):
161165
msg=f"Illicit type for {attr_:s} ({inferred_values[-1].pytype()})",
162166
)
163167

168+
@unittest.skipUnless(
169+
HAS_NUMPY and numpy_supports_type_hints(),
170+
f"This test requires the numpy library with a version above {NUMPY_VERSION_TYPE_HINTS_SUPPORT}",
171+
)
172+
def test_numpy_ndarray_class_support_type_indexing(self):
173+
"""
174+
Test that numpy ndarray class can be subscripted (type hints)
175+
"""
176+
src = """
177+
import numpy as np
178+
np.ndarray[int]
179+
"""
180+
node = builder.extract_node(src)
181+
cls_node = node.inferred()[0]
182+
self.assertIsInstance(cls_node, nodes.ClassDef)
183+
self.assertEqual(cls_node.name, "ndarray")
184+
164185

165186
if __name__ == "__main__":
166187
unittest.main()

0 commit comments

Comments
 (0)