Skip to content

Commit 4cf70d3

Browse files
committed
Treat NewTypes like normal subclasses
NewTypes are assumed not to inherit any members from their base classes. This results in incorrect inference results. Avoid this by changing the transformation for NewTypes to treat them like any other subclass. pylint-dev/pylint#3162 pylint-dev/pylint#2296
1 parent a9f5f97 commit 4cf70d3

File tree

2 files changed

+72
-9
lines changed

2 files changed

+72
-9
lines changed

astroid/brain/brain_typing.py

+44-9
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@
3838
from astroid.util import Uninferable
3939

4040
TYPING_NAMEDTUPLE_BASENAMES = {"NamedTuple", "typing.NamedTuple"}
41-
TYPING_TYPEVARS = {"TypeVar", "NewType"}
42-
TYPING_TYPEVARS_QUALIFIED = {"typing.TypeVar", "typing.NewType"}
4341
TYPING_TYPE_TEMPLATE = """
4442
class Meta(type):
4543
def __getitem__(self, item):
@@ -52,6 +50,11 @@ def __args__(self):
5250
class {0}(metaclass=Meta):
5351
pass
5452
"""
53+
# PEP484 suggests NewType is equivalent to this for typing purposes
54+
TYPING_NEWTYPE_TEMPLATE = """
55+
class {derived}({base}):
56+
pass
57+
"""
5558
TYPING_MEMBERS = set(getattr(typing, "__all__", []))
5659

5760
TYPING_ALIAS = frozenset(
@@ -106,23 +109,32 @@ def __class_getitem__(cls, item):
106109
"""
107110

108111

109-
def looks_like_typing_typevar_or_newtype(node):
112+
def looks_like_typing_typevar(node):
113+
func = node.func
114+
if isinstance(func, Attribute):
115+
return func.attrname == "TypeVar"
116+
if isinstance(func, Name):
117+
return func.name == "TypeVar"
118+
return False
119+
120+
121+
def looks_like_typing_newtype(node):
110122
func = node.func
111123
if isinstance(func, Attribute):
112-
return func.attrname in TYPING_TYPEVARS
124+
return func.attrname == "NewType"
113125
if isinstance(func, Name):
114-
return func.name in TYPING_TYPEVARS
126+
return func.name == "NewType"
115127
return False
116128

117129

118-
def infer_typing_typevar_or_newtype(node, context_itton=None):
130+
def infer_typing_typevar(node, context_itton=None):
119131
"""Infer a typing.TypeVar(...) or typing.NewType(...) call"""
120132
try:
121133
func = next(node.func.infer(context=context_itton))
122134
except (InferenceError, StopIteration) as exc:
123135
raise UseInferenceDefault from exc
124136

125-
if func.qname() not in TYPING_TYPEVARS_QUALIFIED:
137+
if func.qname() != "typing.TypeVar":
126138
raise UseInferenceDefault
127139
if not node.args:
128140
raise UseInferenceDefault
@@ -132,6 +144,24 @@ def infer_typing_typevar_or_newtype(node, context_itton=None):
132144
return node.infer(context=context_itton)
133145

134146

147+
def infer_typing_newtype(node, context_itton=None):
148+
"""Infer a typing.TypeVar(...) or typing.NewType(...) call"""
149+
try:
150+
func = next(node.func.infer(context=context_itton))
151+
except (InferenceError, StopIteration) as exc:
152+
raise UseInferenceDefault from exc
153+
154+
if func.qname() != "typing.NewType":
155+
raise UseInferenceDefault
156+
if not node.args:
157+
raise UseInferenceDefault
158+
159+
derived = node.args[0].as_string().strip("'")
160+
base = node.args[1].as_string().strip("'")
161+
node = extract_node(TYPING_NEWTYPE_TEMPLATE.format(derived=derived, base=base))
162+
return node.infer(context=context_itton)
163+
164+
135165
def _looks_like_typing_subscript(node):
136166
"""Try to figure out if a Subscript node *might* be a typing-related subscript"""
137167
if isinstance(node, Name):
@@ -409,8 +439,13 @@ def infer_typing_cast(
409439

410440
AstroidManager().register_transform(
411441
Call,
412-
inference_tip(infer_typing_typevar_or_newtype),
413-
looks_like_typing_typevar_or_newtype,
442+
inference_tip(infer_typing_typevar),
443+
looks_like_typing_typevar,
444+
)
445+
AstroidManager().register_transform(
446+
Call,
447+
inference_tip(infer_typing_newtype),
448+
looks_like_typing_newtype,
414449
)
415450
AstroidManager().register_transform(
416451
Subscript, inference_tip(infer_typing_attr), _looks_like_typing_subscript

tests/unittest_brain.py

+28
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,34 @@ def test_typing_types(self) -> None:
16591659
inferred = next(node.infer())
16601660
self.assertIsInstance(inferred, nodes.ClassDef, node.as_string())
16611661

1662+
def test_typing_newtype_attrs(self) -> None:
1663+
ast_nodes = builder.extract_node(
1664+
"""
1665+
from typing import List, NewType
1666+
import typing
1667+
NewType("Foo", str) #@
1668+
NewType("Bar", "int") #@
1669+
"""
1670+
)
1671+
assert isinstance(ast_nodes, list)
1672+
1673+
# Base type given by reference
1674+
foo_node = ast_nodes[0]
1675+
foo_inferred = next(foo_node.infer())
1676+
self.assertIsInstance(foo_inferred, astroid.ClassDef)
1677+
1678+
# Check base type method is inferred
1679+
foo_base_class_method = foo_inferred.getattr("endswith")[0]
1680+
self.assertIsInstance(foo_base_class_method, astroid.FunctionDef)
1681+
1682+
# Base type given by string
1683+
bar_node = ast_nodes[1]
1684+
bar_inferred = next(bar_node.infer())
1685+
self.assertIsInstance(bar_inferred, astroid.ClassDef)
1686+
1687+
bar_base_class_method = bar_inferred.getattr("bit_count")[0]
1688+
self.assertIsInstance(bar_base_class_method, astroid.FunctionDef)
1689+
16621690
def test_namedtuple_nested_class(self):
16631691
result = builder.extract_node(
16641692
"""

0 commit comments

Comments
 (0)