Skip to content

Commit

Permalink
Add __init__ to the ObjectModel and return BoundMethods (#…
Browse files Browse the repository at this point in the history
…1687)

Co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com>
  • Loading branch information
DanielNoord and jacobtylerwalls authored Jul 6, 2022
1 parent ef41556 commit 9224558
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 40 deletions.
3 changes: 3 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ Release date: TBA

Closes #104, Closes #1611

* ``__new__`` and ``__init__`` have been added to the ``ObjectModel`` and are now
inferred as ``BoundMethods``.

* Old style string formatting (using ``%`` operators) is now correctly inferred.

Closes #151
Expand Down
77 changes: 41 additions & 36 deletions astroid/interpreter/objectmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,20 @@
import pprint
import types
from functools import lru_cache
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

import astroid
from astroid import util
from astroid import bases, nodes, util
from astroid.context import InferenceContext, copy_context
from astroid.exceptions import AttributeInferenceError, InferenceError, NoDefault
from astroid.manager import AstroidManager
from astroid.nodes import node_classes

objects = util.lazy_import("objects")
builder = util.lazy_import("builder")

if TYPE_CHECKING:
from astroid import builder
from astroid.objects import Property

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


def _get_bound_node(model: ObjectModel) -> Any:
# TODO: Use isinstance instead of try ... except after _instance has typing
try:
return model._instance._proxied
except AttributeError:
return model._instance


class ObjectModel:
def __init__(self):
self._instance = None
Expand Down Expand Up @@ -119,17 +129,31 @@ def lookup(self, name):
raise AttributeInferenceError(target=self._instance, attribute=name)

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

Instance is either an instance or a class definition of the instance to be
created.
"""
# TODO: Use isinstance instead of try ... except after _instance has typing
try:
return self._instance._proxied.instantiate_class()
except AttributeError:
return self._instance.instantiate_class()
return bases.BoundMethod(proxy=node, bound=_get_bound_node(self))

@property
def attr___init__(self) -> bases.BoundMethod:
"""Calling cls.__init__() normally returns None."""
# The *args and **kwargs are necessary not to trigger warnings about missing
# or extra parameters for '__init__' methods we don't infer correctly.
# This BoundMethod is the fallback value for those.
node: nodes.FunctionDef = builder.extract_node(
"""def __init__(self, *args, **kwargs): return None"""
)
# We set the parent as being the ClassDef of 'object' as that
# is where this method originally comes from
node.parent: nodes.ClassDef = AstroidManager().builtins_module["object"]

return bases.BoundMethod(proxy=node, bound=_get_bound_node(self))


class ModuleModel(ObjectModel):
Expand Down Expand Up @@ -300,9 +324,6 @@ def attr___module__(self):

@property
def attr___get__(self):
# pylint: disable=import-outside-toplevel; circular import
from astroid import bases

func = self._instance

class DescriptorBoundMethod(bases.BoundMethod):
Expand Down Expand Up @@ -409,7 +430,6 @@ def attr___ne__(self):
attr___delattr___ = attr___ne__
attr___getattribute__ = attr___ne__
attr___hash__ = attr___ne__
attr___init__ = attr___ne__
attr___dir__ = attr___ne__
attr___call__ = attr___ne__
attr___class__ = attr___ne__
Expand Down Expand Up @@ -455,9 +475,6 @@ def attr_mro(self):
if not self._instance.newstyle:
raise AttributeInferenceError(target=self._instance, attribute="mro")

# pylint: disable=import-outside-toplevel; circular import
from astroid import bases

other_self = self

# Cls.mro is a method and we need to return one in order to have a proper inference.
Expand Down Expand Up @@ -492,10 +509,6 @@ def attr___subclasses__(self):
This looks only in the current module for retrieving the subclasses,
thus it might miss a couple of them.
"""
# pylint: disable=import-outside-toplevel; circular import
from astroid import bases
from astroid.nodes import scoped_nodes

if not self._instance.newstyle:
raise AttributeInferenceError(
target=self._instance, attribute="__subclasses__"
Expand All @@ -505,7 +518,7 @@ def attr___subclasses__(self):
root = self._instance.root()
classes = [
cls
for cls in root.nodes_of_class(scoped_nodes.ClassDef)
for cls in root.nodes_of_class(nodes.ClassDef)
if cls != self._instance and cls.is_subtype_of(qname, context=self.context)
]

Expand Down Expand Up @@ -778,12 +791,8 @@ def attr_values(self):
class PropertyModel(ObjectModel):
"""Model for a builtin property"""

# pylint: disable=import-outside-toplevel
def _init_function(self, name):
from astroid.nodes.node_classes import Arguments
from astroid.nodes.scoped_nodes import FunctionDef

args = Arguments()
args = nodes.Arguments()
args.postinit(
args=[],
defaults=[],
Expand All @@ -795,18 +804,16 @@ def _init_function(self, name):
kwonlyargs_annotations=[],
)

function = FunctionDef(name=name, parent=self._instance)
function = nodes.FunctionDef(name=name, parent=self._instance)

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

@property
def attr_fget(self):
from astroid.nodes.scoped_nodes import FunctionDef

func = self._instance

class PropertyFuncAccessor(FunctionDef):
class PropertyFuncAccessor(nodes.FunctionDef):
def infer_call_result(self, caller=None, context=None):
nonlocal func
if caller and len(caller.args) != 1:
Expand All @@ -824,8 +831,6 @@ def infer_call_result(self, caller=None, context=None):

@property
def attr_fset(self):
from astroid.nodes.scoped_nodes import FunctionDef

func = self._instance

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

class PropertyFuncAccessor(FunctionDef):
class PropertyFuncAccessor(nodes.FunctionDef):
def infer_call_result(self, caller=None, context=None):
nonlocal func_setter
if caller and len(caller.args) != 2:
Expand Down
84 changes: 80 additions & 4 deletions tests/unittest_object_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest

import astroid
from astroid import builder, nodes, objects, test_utils, util
from astroid import bases, builder, nodes, objects, test_utils, util
from astroid.const import PY311_PLUS
from astroid.exceptions import InferenceError

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

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

cls = next(ast_nodes[7].infer())
self.assertIsInstance(cls, astroid.ClassDef)
Expand Down Expand Up @@ -253,6 +253,27 @@ def test_module_model(self) -> None:
xml.__cached__ #@
xml.__package__ #@
xml.__dict__ #@
xml.__init__ #@
xml.__new__ #@
xml.__subclasshook__ #@
xml.__str__ #@
xml.__sizeof__ #@
xml.__repr__ #@
xml.__reduce__ #@
xml.__setattr__ #@
xml.__reduce_ex__ #@
xml.__lt__ #@
xml.__eq__ #@
xml.__gt__ #@
xml.__format__ #@
xml.__delattr___ #@
xml.__getattribute__ #@
xml.__hash__ #@
xml.__dir__ #@
xml.__call__ #@
xml.__closure__ #@
"""
)
assert isinstance(ast_nodes, list)
Expand Down Expand Up @@ -284,6 +305,21 @@ def test_module_model(self) -> None:
dict_ = next(ast_nodes[8].infer())
self.assertIsInstance(dict_, astroid.Dict)

init_ = next(ast_nodes[9].infer())
assert isinstance(init_, bases.BoundMethod)
init_result = next(init_.infer_call_result(nodes.Call()))
assert isinstance(init_result, nodes.Const)
assert init_result.value is None

new_ = next(ast_nodes[10].infer())
assert isinstance(new_, bases.BoundMethod)

# The following nodes are just here for theoretical completeness,
# and they either return Uninferable or raise InferenceError.
for ast_node in ast_nodes[11:28]:
with pytest.raises(InferenceError):
next(ast_node.infer())


class FunctionModelTest(unittest.TestCase):
def test_partial_descriptor_support(self) -> None:
Expand Down Expand Up @@ -394,6 +430,27 @@ def func(a=1, b=2):
func.__globals__ #@
func.__code__ #@
func.__closure__ #@
func.__init__ #@
func.__new__ #@
func.__subclasshook__ #@
func.__str__ #@
func.__sizeof__ #@
func.__repr__ #@
func.__reduce__ #@
func.__reduce_ex__ #@
func.__lt__ #@
func.__eq__ #@
func.__gt__ #@
func.__format__ #@
func.__delattr___ #@
func.__getattribute__ #@
func.__hash__ #@
func.__dir__ #@
func.__class__ #@
func.__setattr__ #@
''',
module_name="fake_module",
)
Expand Down Expand Up @@ -427,6 +484,25 @@ def func(a=1, b=2):
for ast_node in ast_nodes[7:9]:
self.assertIs(next(ast_node.infer()), astroid.Uninferable)

init_ = next(ast_nodes[9].infer())
assert isinstance(init_, bases.BoundMethod)
init_result = next(init_.infer_call_result(nodes.Call()))
assert isinstance(init_result, nodes.Const)
assert init_result.value is None

new_ = next(ast_nodes[10].infer())
assert isinstance(new_, bases.BoundMethod)

# The following nodes are just here for theoretical completeness,
# and they either return Uninferable or raise InferenceError.
for ast_node in ast_nodes[11:26]:
inferred = next(ast_node.infer())
assert inferred is util.Uninferable

for ast_node in ast_nodes[26:27]:
with pytest.raises(InferenceError):
inferred = next(ast_node.infer())

def test_empty_return_annotation(self) -> None:
ast_node = builder.extract_node(
"""
Expand Down
20 changes: 20 additions & 0 deletions tests/unittest_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,26 @@ def __new__(metacls, classname, bases, classdict, **kwds):
isinstance(i, (nodes.NodeNG, type(util.Uninferable))) for i in inferred
)

def test_super_init_call(self) -> None:
"""Test that __init__ is still callable."""
init_node: nodes.Attribute = builder.extract_node(
"""
class SuperUsingClass:
@staticmethod
def test():
super(object, 1).__new__ #@
super(object, 1).__init__ #@
class A:
pass
A().__new__ #@
A().__init__ #@
"""
)
assert isinstance(next(init_node[0].infer()), bases.BoundMethod)
assert isinstance(next(init_node[1].infer()), bases.BoundMethod)
assert isinstance(next(init_node[2].infer()), bases.BoundMethod)
assert isinstance(next(init_node[3].infer()), bases.BoundMethod)


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

0 comments on commit 9224558

Please sign in to comment.