Skip to content

Commit 9224558

Browse files
Add __init__ to the ObjectModel and return BoundMethods (#1687)
Co-authored-by: Jacob Walls <[email protected]>
1 parent ef41556 commit 9224558

File tree

4 files changed

+144
-40
lines changed

4 files changed

+144
-40
lines changed

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ Release date: TBA
3838

3939
Closes #104, Closes #1611
4040

41+
* ``__new__`` and ``__init__`` have been added to the ``ObjectModel`` and are now
42+
inferred as ``BoundMethods``.
43+
4144
* Old style string formatting (using ``%`` operators) is now correctly inferred.
4245

4346
Closes #151

astroid/interpreter/objectmodel.py

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,20 @@
2828
import pprint
2929
import types
3030
from functools import lru_cache
31-
from typing import TYPE_CHECKING
31+
from typing import TYPE_CHECKING, Any
3232

3333
import astroid
34-
from astroid import util
34+
from astroid import bases, nodes, util
3535
from astroid.context import InferenceContext, copy_context
3636
from astroid.exceptions import AttributeInferenceError, InferenceError, NoDefault
3737
from astroid.manager import AstroidManager
3838
from astroid.nodes import node_classes
3939

4040
objects = util.lazy_import("objects")
41+
builder = util.lazy_import("builder")
4142

4243
if TYPE_CHECKING:
44+
from astroid import builder
4345
from astroid.objects import Property
4446

4547
IMPL_PREFIX = "attr_"
@@ -63,6 +65,14 @@ def _dunder_dict(instance, attributes):
6365
return obj
6466

6567

68+
def _get_bound_node(model: ObjectModel) -> Any:
69+
# TODO: Use isinstance instead of try ... except after _instance has typing
70+
try:
71+
return model._instance._proxied
72+
except AttributeError:
73+
return model._instance
74+
75+
6676
class ObjectModel:
6777
def __init__(self):
6878
self._instance = None
@@ -119,17 +129,31 @@ def lookup(self, name):
119129
raise AttributeInferenceError(target=self._instance, attribute=name)
120130

121131
@property
122-
def attr___new__(self):
123-
"""Calling cls.__new__(cls) on an object returns an instance of that object.
132+
def attr___new__(self) -> bases.BoundMethod:
133+
"""Calling cls.__new__(type) on an object returns an instance of 'type'."""
134+
node: nodes.FunctionDef = builder.extract_node(
135+
"""def __new__(self, cls): return cls()"""
136+
)
137+
# We set the parent as being the ClassDef of 'object' as that
138+
# triggers correct inference as a call to __new__ in bases.py
139+
node.parent: nodes.ClassDef = AstroidManager().builtins_module["object"]
124140

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()
141+
return bases.BoundMethod(proxy=node, bound=_get_bound_node(self))
142+
143+
@property
144+
def attr___init__(self) -> bases.BoundMethod:
145+
"""Calling cls.__init__() normally returns None."""
146+
# The *args and **kwargs are necessary not to trigger warnings about missing
147+
# or extra parameters for '__init__' methods we don't infer correctly.
148+
# This BoundMethod is the fallback value for those.
149+
node: nodes.FunctionDef = builder.extract_node(
150+
"""def __init__(self, *args, **kwargs): return None"""
151+
)
152+
# We set the parent as being the ClassDef of 'object' as that
153+
# is where this method originally comes from
154+
node.parent: nodes.ClassDef = AstroidManager().builtins_module["object"]
155+
156+
return bases.BoundMethod(proxy=node, bound=_get_bound_node(self))
133157

134158

135159
class ModuleModel(ObjectModel):
@@ -300,9 +324,6 @@ def attr___module__(self):
300324

301325
@property
302326
def attr___get__(self):
303-
# pylint: disable=import-outside-toplevel; circular import
304-
from astroid import bases
305-
306327
func = self._instance
307328

308329
class DescriptorBoundMethod(bases.BoundMethod):
@@ -409,7 +430,6 @@ def attr___ne__(self):
409430
attr___delattr___ = attr___ne__
410431
attr___getattribute__ = attr___ne__
411432
attr___hash__ = attr___ne__
412-
attr___init__ = attr___ne__
413433
attr___dir__ = attr___ne__
414434
attr___call__ = attr___ne__
415435
attr___class__ = attr___ne__
@@ -455,9 +475,6 @@ def attr_mro(self):
455475
if not self._instance.newstyle:
456476
raise AttributeInferenceError(target=self._instance, attribute="mro")
457477

458-
# pylint: disable=import-outside-toplevel; circular import
459-
from astroid import bases
460-
461478
other_self = self
462479

463480
# Cls.mro is a method and we need to return one in order to have a proper inference.
@@ -492,10 +509,6 @@ def attr___subclasses__(self):
492509
This looks only in the current module for retrieving the subclasses,
493510
thus it might miss a couple of them.
494511
"""
495-
# pylint: disable=import-outside-toplevel; circular import
496-
from astroid import bases
497-
from astroid.nodes import scoped_nodes
498-
499512
if not self._instance.newstyle:
500513
raise AttributeInferenceError(
501514
target=self._instance, attribute="__subclasses__"
@@ -505,7 +518,7 @@ def attr___subclasses__(self):
505518
root = self._instance.root()
506519
classes = [
507520
cls
508-
for cls in root.nodes_of_class(scoped_nodes.ClassDef)
521+
for cls in root.nodes_of_class(nodes.ClassDef)
509522
if cls != self._instance and cls.is_subtype_of(qname, context=self.context)
510523
]
511524

@@ -778,12 +791,8 @@ def attr_values(self):
778791
class PropertyModel(ObjectModel):
779792
"""Model for a builtin property"""
780793

781-
# pylint: disable=import-outside-toplevel
782794
def _init_function(self, name):
783-
from astroid.nodes.node_classes import Arguments
784-
from astroid.nodes.scoped_nodes import FunctionDef
785-
786-
args = Arguments()
795+
args = nodes.Arguments()
787796
args.postinit(
788797
args=[],
789798
defaults=[],
@@ -795,18 +804,16 @@ def _init_function(self, name):
795804
kwonlyargs_annotations=[],
796805
)
797806

798-
function = FunctionDef(name=name, parent=self._instance)
807+
function = nodes.FunctionDef(name=name, parent=self._instance)
799808

800809
function.postinit(args=args, body=[])
801810
return function
802811

803812
@property
804813
def attr_fget(self):
805-
from astroid.nodes.scoped_nodes import FunctionDef
806-
807814
func = self._instance
808815

809-
class PropertyFuncAccessor(FunctionDef):
816+
class PropertyFuncAccessor(nodes.FunctionDef):
810817
def infer_call_result(self, caller=None, context=None):
811818
nonlocal func
812819
if caller and len(caller.args) != 1:
@@ -824,8 +831,6 @@ def infer_call_result(self, caller=None, context=None):
824831

825832
@property
826833
def attr_fset(self):
827-
from astroid.nodes.scoped_nodes import FunctionDef
828-
829834
func = self._instance
830835

831836
def find_setter(func: Property) -> astroid.FunctionDef | None:
@@ -849,7 +854,7 @@ def find_setter(func: Property) -> astroid.FunctionDef | None:
849854
f"Unable to find the setter of property {func.function.name}"
850855
)
851856

852-
class PropertyFuncAccessor(FunctionDef):
857+
class PropertyFuncAccessor(nodes.FunctionDef):
853858
def infer_call_result(self, caller=None, context=None):
854859
nonlocal func_setter
855860
if caller and len(caller.args) != 2:

tests/unittest_object_model.py

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99

1010
import astroid
11-
from astroid import builder, nodes, objects, test_utils, util
11+
from astroid import bases, builder, nodes, objects, test_utils, util
1212
from astroid.const import PY311_PLUS
1313
from astroid.exceptions import InferenceError
1414

@@ -203,9 +203,9 @@ class C(A): pass
203203
called_mro = next(ast_nodes[5].infer())
204204
self.assertEqual(called_mro.elts, mro.elts)
205205

206-
bases = next(ast_nodes[6].infer())
207-
self.assertIsInstance(bases, astroid.Tuple)
208-
self.assertEqual([cls.name for cls in bases.elts], ["object"])
206+
base_nodes = next(ast_nodes[6].infer())
207+
self.assertIsInstance(base_nodes, astroid.Tuple)
208+
self.assertEqual([cls.name for cls in base_nodes.elts], ["object"])
209209

210210
cls = next(ast_nodes[7].infer())
211211
self.assertIsInstance(cls, astroid.ClassDef)
@@ -253,6 +253,27 @@ def test_module_model(self) -> None:
253253
xml.__cached__ #@
254254
xml.__package__ #@
255255
xml.__dict__ #@
256+
xml.__init__ #@
257+
xml.__new__ #@
258+
259+
xml.__subclasshook__ #@
260+
xml.__str__ #@
261+
xml.__sizeof__ #@
262+
xml.__repr__ #@
263+
xml.__reduce__ #@
264+
265+
xml.__setattr__ #@
266+
xml.__reduce_ex__ #@
267+
xml.__lt__ #@
268+
xml.__eq__ #@
269+
xml.__gt__ #@
270+
xml.__format__ #@
271+
xml.__delattr___ #@
272+
xml.__getattribute__ #@
273+
xml.__hash__ #@
274+
xml.__dir__ #@
275+
xml.__call__ #@
276+
xml.__closure__ #@
256277
"""
257278
)
258279
assert isinstance(ast_nodes, list)
@@ -284,6 +305,21 @@ def test_module_model(self) -> None:
284305
dict_ = next(ast_nodes[8].infer())
285306
self.assertIsInstance(dict_, astroid.Dict)
286307

308+
init_ = next(ast_nodes[9].infer())
309+
assert isinstance(init_, bases.BoundMethod)
310+
init_result = next(init_.infer_call_result(nodes.Call()))
311+
assert isinstance(init_result, nodes.Const)
312+
assert init_result.value is None
313+
314+
new_ = next(ast_nodes[10].infer())
315+
assert isinstance(new_, bases.BoundMethod)
316+
317+
# The following nodes are just here for theoretical completeness,
318+
# and they either return Uninferable or raise InferenceError.
319+
for ast_node in ast_nodes[11:28]:
320+
with pytest.raises(InferenceError):
321+
next(ast_node.infer())
322+
287323

288324
class FunctionModelTest(unittest.TestCase):
289325
def test_partial_descriptor_support(self) -> None:
@@ -394,6 +430,27 @@ def func(a=1, b=2):
394430
func.__globals__ #@
395431
func.__code__ #@
396432
func.__closure__ #@
433+
func.__init__ #@
434+
func.__new__ #@
435+
436+
func.__subclasshook__ #@
437+
func.__str__ #@
438+
func.__sizeof__ #@
439+
func.__repr__ #@
440+
func.__reduce__ #@
441+
442+
func.__reduce_ex__ #@
443+
func.__lt__ #@
444+
func.__eq__ #@
445+
func.__gt__ #@
446+
func.__format__ #@
447+
func.__delattr___ #@
448+
func.__getattribute__ #@
449+
func.__hash__ #@
450+
func.__dir__ #@
451+
func.__class__ #@
452+
453+
func.__setattr__ #@
397454
''',
398455
module_name="fake_module",
399456
)
@@ -427,6 +484,25 @@ def func(a=1, b=2):
427484
for ast_node in ast_nodes[7:9]:
428485
self.assertIs(next(ast_node.infer()), astroid.Uninferable)
429486

487+
init_ = next(ast_nodes[9].infer())
488+
assert isinstance(init_, bases.BoundMethod)
489+
init_result = next(init_.infer_call_result(nodes.Call()))
490+
assert isinstance(init_result, nodes.Const)
491+
assert init_result.value is None
492+
493+
new_ = next(ast_nodes[10].infer())
494+
assert isinstance(new_, bases.BoundMethod)
495+
496+
# The following nodes are just here for theoretical completeness,
497+
# and they either return Uninferable or raise InferenceError.
498+
for ast_node in ast_nodes[11:26]:
499+
inferred = next(ast_node.infer())
500+
assert inferred is util.Uninferable
501+
502+
for ast_node in ast_nodes[26:27]:
503+
with pytest.raises(InferenceError):
504+
inferred = next(ast_node.infer())
505+
430506
def test_empty_return_annotation(self) -> None:
431507
ast_node = builder.extract_node(
432508
"""

tests/unittest_objects.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,26 @@ def __new__(metacls, classname, bases, classdict, **kwds):
568568
isinstance(i, (nodes.NodeNG, type(util.Uninferable))) for i in inferred
569569
)
570570

571+
def test_super_init_call(self) -> None:
572+
"""Test that __init__ is still callable."""
573+
init_node: nodes.Attribute = builder.extract_node(
574+
"""
575+
class SuperUsingClass:
576+
@staticmethod
577+
def test():
578+
super(object, 1).__new__ #@
579+
super(object, 1).__init__ #@
580+
class A:
581+
pass
582+
A().__new__ #@
583+
A().__init__ #@
584+
"""
585+
)
586+
assert isinstance(next(init_node[0].infer()), bases.BoundMethod)
587+
assert isinstance(next(init_node[1].infer()), bases.BoundMethod)
588+
assert isinstance(next(init_node[2].infer()), bases.BoundMethod)
589+
assert isinstance(next(init_node[3].infer()), bases.BoundMethod)
590+
571591

572592
if __name__ == "__main__":
573593
unittest.main()

0 commit comments

Comments
 (0)