Skip to content

Commit a6fdf0b

Browse files
authored
Add __new__ and __call__ toObjectModel and ClassModel (#1606)
1 parent dd80a68 commit a6fdf0b

File tree

5 files changed

+62
-17
lines changed

5 files changed

+62
-17
lines changed

ChangeLog

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ Release date: TBA
6767
* Rename ``ModuleSpec`` -> ``module_type`` constructor parameter to match attribute
6868
name and improve typing. Use ``type`` instead.
6969

70+
* ``ObjectModel`` and ``ClassModel`` now know about their ``__new__`` and ``__call__`` attributes.
71+
7072
* Fixed pylint ``not-callable`` false positive with nested-tuple assignment in a for-loop.
7173

7274
Refs PyCQA/pylint#5113

astroid/bases.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
import collections
1111
import collections.abc
1212
from collections.abc import Sequence
13-
from typing import TYPE_CHECKING, Any
13+
from typing import Any
1414

15-
from astroid import decorators
15+
from astroid import decorators, nodes
1616
from astroid.const import PY310_PLUS
1717
from astroid.context import (
1818
CallContext,
@@ -33,9 +33,6 @@
3333
helpers = lazy_import("helpers")
3434
manager = lazy_import("manager")
3535

36-
if TYPE_CHECKING:
37-
from astroid import nodes
38-
3936

4037
# TODO: check if needs special treatment
4138
BOOL_SPECIAL_METHOD = "__bool__"
@@ -271,10 +268,20 @@ def _wrap_attr(self, attrs, context=None):
271268
else:
272269
yield attr
273270

274-
def infer_call_result(self, caller, context=None):
271+
def infer_call_result(
272+
self, caller: nodes.Call | Proxy, context: InferenceContext | None = None
273+
):
275274
"""infer what a class instance is returning when called"""
276275
context = bind_context_to_node(context, self)
277276
inferred = False
277+
278+
# If the call is an attribute on the instance, we infer the attribute itself
279+
if isinstance(caller, nodes.Call) and isinstance(caller.func, nodes.Attribute):
280+
for res in self.igetattr(caller.func.attrname, context):
281+
inferred = True
282+
yield res
283+
284+
# Otherwise we infer the call to the __call__ dunder normally
278285
for node in self._proxied.igetattr("__call__", context):
279286
if node is Uninferable or not node.callable():
280287
continue
@@ -418,9 +425,6 @@ def _infer_builtin_new(
418425
) -> collections.abc.Generator[
419426
nodes.Const | Instance | type[Uninferable], None, None
420427
]:
421-
# pylint: disable-next=import-outside-toplevel; circular import
422-
from astroid import nodes
423-
424428
if not caller.args:
425429
return
426430
# Attempt to create a constant

astroid/interpreter/objectmodel.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,19 @@ def lookup(self, name):
118118
return getattr(self, IMPL_PREFIX + name)
119119
raise AttributeInferenceError(target=self._instance, attribute=name)
120120

121+
@property
122+
def attr___new__(self):
123+
"""Calling cls.__new__(cls) on an object returns an instance of that object.
124+
125+
Instance is either an instance or a class definition of the instance to be
126+
created.
127+
"""
128+
# TODO: Use isinstance instead of try ... except after _instance has typing
129+
try:
130+
return self._instance._proxied.instantiate_class()
131+
except AttributeError:
132+
return self._instance.instantiate_class()
133+
121134

122135
class ModuleModel(ObjectModel):
123136
def _builtins(self):
@@ -389,7 +402,6 @@ def attr___ne__(self):
389402
attr___repr__ = attr___ne__
390403
attr___reduce__ = attr___ne__
391404
attr___reduce_ex__ = attr___ne__
392-
attr___new__ = attr___ne__
393405
attr___lt__ = attr___ne__
394406
attr___eq__ = attr___ne__
395407
attr___gt__ = attr___ne__
@@ -512,6 +524,11 @@ def infer_call_result(self, caller, context=None):
512524
def attr___dict__(self):
513525
return node_classes.Dict(parent=self._instance)
514526

527+
@property
528+
def attr___call__(self):
529+
"""Calling a class A() returns an instance of A."""
530+
return self._instance.instantiate_class()
531+
515532

516533
class SuperModel(ObjectModel):
517534
@property

astroid/nodes/scoped_nodes/scoped_nodes.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2298,7 +2298,12 @@ def infer_call_result(self, caller, context=None):
22982298
try:
22992299
metaclass = self.metaclass(context=context)
23002300
if metaclass is not None:
2301-
dunder_call = next(metaclass.igetattr("__call__", context))
2301+
# Only get __call__ if it's defined locally for the metaclass.
2302+
# Otherwise we will find ObjectModel.__call__ which will
2303+
# return an instance of the metaclass. Instantiating the class is
2304+
# handled later.
2305+
if "__call__" in metaclass.locals:
2306+
dunder_call = next(metaclass.igetattr("__call__", context))
23022307
except (AttributeInferenceError, StopIteration):
23032308
pass
23042309

@@ -2550,7 +2555,11 @@ def getattr(self, name, context=None, class_context=True):
25502555
if not name:
25512556
raise AttributeInferenceError(target=self, attribute=name, context=context)
25522557

2553-
values = self.locals.get(name, [])
2558+
# don't modify the list in self.locals!
2559+
values = list(self.locals.get(name, []))
2560+
for classnode in self.ancestors(recurs=True, context=context):
2561+
values += classnode.locals.get(name, [])
2562+
25542563
if name in self.special_attributes and class_context and not values:
25552564
result = [self.special_attributes.lookup(name)]
25562565
if name == "__bases__":
@@ -2559,11 +2568,6 @@ def getattr(self, name, context=None, class_context=True):
25592568
result += values
25602569
return result
25612570

2562-
# don't modify the list in self.locals!
2563-
values = list(values)
2564-
for classnode in self.ancestors(recurs=True, context=context):
2565-
values += classnode.locals.get(name, [])
2566-
25672571
if class_context:
25682572
values += self._metaclass_lookup_attribute(name, context)
25692573

tests/unittest_objects.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ def test_frozenset(self) -> None:
3232
self.assertEqual(inferred.qname(), "builtins.frozenset")
3333
self.assertIsInstance(proxied, nodes.ClassDef)
3434

35+
def test_lookup_regression_slots(self) -> None:
36+
"""Regression test for attr__new__ of ObjectModel.
37+
38+
ObjectModel._instance is not always an bases.Instance, so we can't
39+
rely on the ._proxied attribute of an Instance.
40+
"""
41+
42+
node = builder.extract_node(
43+
"""
44+
class ClassHavingUnknownAncestors(Unknown):
45+
__slots__ = ["yo"]
46+
47+
def test(self):
48+
self.not_yo = 42
49+
"""
50+
)
51+
assert node.getattr("__new__")
52+
3553

3654
class SuperTests(unittest.TestCase):
3755
def test_inferring_super_outside_methods(self) -> None:

0 commit comments

Comments
 (0)