Skip to content

Commit

Permalink
Return BoundMethods instead of the result of those methods
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielNoord committed Jul 6, 2022
1 parent b4e4a78 commit 36d2d45
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 46 deletions.
72 changes: 38 additions & 34 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 nodes, 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,21 +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"]

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)

@property
def attr___init__(self) -> nodes.Const:
return nodes.Const(None)
def attr___init__(self) -> bases.BoundMethod:
"""Calling cls.__init__() normally returns None."""
# The *args and **kwargs are necessary not too 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"]

# 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)


class ModuleModel(ObjectModel):
Expand Down Expand Up @@ -304,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 @@ -458,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 @@ -483,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
from astroid import helpers

return helpers.object_type(self._instance)
Expand All @@ -495,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 @@ -508,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 @@ -781,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 @@ -798,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 @@ -827,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 @@ -852,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
26 changes: 14 additions & 12 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 @@ -306,12 +306,13 @@ def test_module_model(self) -> None:
self.assertIsInstance(dict_, astroid.Dict)

init_ = next(ast_nodes[9].infer())
assert isinstance(init_, nodes.Const)
assert init_.value is None
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_, nodes.Module)
assert new_.name == "xml"
assert isinstance(new_, bases.BoundMethod)

# The following nodes are just here for theoretical completeness,
# and they either return Uninferable or raise InferenceError.
Expand Down Expand Up @@ -484,12 +485,13 @@ def func(a=1, b=2):
self.assertIs(next(ast_node.infer()), astroid.Uninferable)

init_ = next(ast_nodes[9].infer())
assert isinstance(init_, nodes.Const)
assert init_.value is None
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_, nodes.FunctionDef)
assert new_.name == "func"
assert isinstance(new_, bases.BoundMethod)

# The following nodes are just here for theoretical completeness,
# and they either return Uninferable or raise InferenceError.
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 36d2d45

Please sign in to comment.