Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add __init__ to the ObjectModel and return BoundMethods #1687

Merged
merged 5 commits into from
Jul 6, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 40 additions & 33 deletions astroid/interpreter/objectmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,17 @@
from typing import TYPE_CHECKING

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 Down Expand Up @@ -119,17 +121,41 @@ 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"]

# TODO: Use isinstance instead of try ... except after _instance has typing
try:
bound = self._instance._proxied
except AttributeError:
bound = self._instance
return bases.BoundMethod(proxy=node, bound=bound)

@property
def attr___init__(self) -> bases.BoundMethod:
"""Calling cls.__init__() normally returns None."""
# The *args and **kwargs are necessary not too trigger warnings about missing
DanielNoord marked this conversation as resolved.
Show resolved Hide resolved
# 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"]

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()
bound = self._instance._proxied
except AttributeError:
return self._instance.instantiate_class()
bound = self._instance
return bases.BoundMethod(proxy=node, bound=bound)
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved


class ModuleModel(ObjectModel):
Expand Down Expand Up @@ -300,9 +326,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 +432,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 +477,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 All @@ -480,7 +499,7 @@ def attr___bases__(self):

@property
def attr___class__(self):
# pylint: disable=import-outside-toplevel; circular import
# pylint: disable=import-outside-toplevel; circular importdd
DanielNoord marked this conversation as resolved.
Show resolved Hide resolved
from astroid import helpers

return helpers.object_type(self._instance)
Expand All @@ -492,10 +511,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 +520,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 +793,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 +806,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 +833,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 +856,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()