Skip to content

Commit

Permalink
983 - Infer return value of None for correctly inferred functions wit…
Browse files Browse the repository at this point in the history
…h no returns

Squashed commit of the following:

commit 7ec6a68
Author: Andrew Haigh <hello@nelf.in>
Date:   Mon May 3 09:09:17 2021 +1000

    Update changelog

commit b50a7dd
Author: Andrew Haigh <hello@nelf.in>
Date:   Mon May 3 09:02:29 2021 +1000

    Update check of implicit return to ignore abstract methods

    Ref pylint-dev#485. To avoid additional breakage, we optionally extend is_abstract
    to consider functions whose body is any raise statement (not just raise
    NotImplementedError)

commit ff719c8
Author: Andrew Haigh <hello@nelf.in>
Date:   Sat May 1 12:19:52 2021 +1000

    Add check of __getitem__ signature to instance_getitem

commit 427d422
Author: Andrew Haigh <hello@nelf.in>
Date:   Sat May 1 11:45:27 2021 +1000

    Fix test definition of igetattr recursion and context_manager_inference

    Ref pylint-dev#663. This test did not actually check for regression of the issue
    fixed in 55076ca (i.e. it also passed on c87bea1 before the fix was
    applied). Additionally, it over-specified the behaviour it was
    attempting to check: whether the value returned from the context manager
    was Uninferable was not directly relevant to the test, so when this
    value changed due to unrelated fixes in inference, this test failed.

commit 8ec2b47
Author: Andrew Haigh <hello@nelf.in>
Date:   Sat Apr 24 09:16:16 2021 +1000

    Update FunctionDef.infer_call_result to infer None with no Returns

    Ref pylint-dev#485. If the function was inferred (unlike many compiler-builtins)
    and it contains no Return nodes, then the implicit return value is None.
  • Loading branch information
cdce8p committed May 15, 2021
1 parent 8181dc9 commit 1ff11af
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 24 deletions.
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ Release Date: TBA
Closes PyCQA/pylint#3535
Closes PyCQA/pylint#4358

* Allow inferring a return value of None for non-abstract empty functions and
functions with no return statements (implicitly returning None)

Closes #485


What's New in astroid 2.5.6?
============================
Expand Down
6 changes: 6 additions & 0 deletions astroid/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,12 @@ def getitem(self, index, context=None):
raise exceptions.InferenceError(
"Could not find __getitem__ for {node!r}.", node=self, context=context
)
if len(method.args.arguments) != 2: # (self, index)
raise exceptions.AstroidTypeError(
"__getitem__ for {node!r} does not have correct signature",
node=self,
context=context,
)
return next(method.infer_call_result(self, new_context))


Expand Down
13 changes: 10 additions & 3 deletions astroid/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1661,11 +1661,12 @@ def is_bound(self):
"""
return self.type == "classmethod"

def is_abstract(self, pass_is_abstract=True):
def is_abstract(self, pass_is_abstract=True, raises_any=False):
"""Check if the method is abstract.
A method is considered abstract if any of the following is true:
* The only statement is 'raise NotImplementedError'
* The only statement is 'raise <SomeException>' and raises_any is True
* The only statement is 'pass' and pass_is_abstract is True
* The method is annotated with abc.astractproperty/abc.abstractmethod
Expand All @@ -1686,6 +1687,8 @@ def is_abstract(self, pass_is_abstract=True):

for child_node in self.body:
if isinstance(child_node, node_classes.Raise):
if raises_any:
return True
if child_node.raises_not_implemented():
return True
return pass_is_abstract and isinstance(child_node, node_classes.Pass)
Expand Down Expand Up @@ -1744,8 +1747,12 @@ def infer_call_result(self, caller=None, context=None):

first_return = next(returns, None)
if not first_return:
if self.body and isinstance(self.body[-1], node_classes.Assert):
yield node_classes.Const(None)
if self.body:
if self.is_abstract(pass_is_abstract=False, raises_any=True):
# TODO: should pass always/never be abstract?
yield util.Uninferable
else:
yield node_classes.Const(None)
return

raise exceptions.InferenceError(
Expand Down
28 changes: 7 additions & 21 deletions tests/unittest_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,14 +706,6 @@ class InvalidGetitem2(object):
NoGetitem()[4] #@
InvalidGetitem()[5] #@
InvalidGetitem2()[10] #@
"""
)
for node in ast_nodes[:3]:
self.assertRaises(InferenceError, next, node.infer())
for node in ast_nodes[3:]:
self.assertEqual(next(node.infer()), util.Uninferable)
ast_nodes = extract_node(
"""
[1, 2, 3][None] #@
'lala'['bala'] #@
"""
Expand Down Expand Up @@ -5349,26 +5341,20 @@ class Cls:
def test_prevent_recursion_error_in_igetattr_and_context_manager_inference():
code = """
class DummyContext(object):
def method(self, msg): # pylint: disable=C0103
pass
def __enter__(self):
pass
return self
def __exit__(self, ex_type, ex_value, ex_tb):
return True
class CallMeMaybe(object):
def __call__(self):
while False:
with DummyContext() as con:
f_method = con.method
break
if False:
with DummyContext() as con:
pass
with DummyContext() as con:
con #@
f_method = con.method
with DummyContext() as con:
con.__enter__ #@
"""
node = extract_node(code)
assert next(node.infer()) is util.Uninferable
next(node.infer()) # should not raise StopIteration/RuntimeError


def test_infer_context_manager_with_unknown_args():
Expand Down
71 changes: 71 additions & 0 deletions tests/unittest_scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,77 @@ def func():
self.assertIsInstance(func_vals[0], nodes.Const)
self.assertIsNone(func_vals[0].value)

def test_no_returns_is_implicitly_none(self):
code = """
def f():
pass
value = f()
value
"""
node = builder.extract_node(code)
inferred = next(node.infer())
assert isinstance(inferred, nodes.Const)
assert inferred.value is None

def test_only_raises_is_not_implicitly_none(self):
code = """
def f():
raise SystemExit()
f()
"""
node = builder.extract_node(code) # type: nodes.Call
inferred = next(node.infer())
assert inferred is util.Uninferable

def test_abstract_methods_are_not_implicitly_none(self):
code = """
from abc import ABCMeta, abstractmethod
class Abstract(metaclass=ABCMeta):
@abstractmethod
def foo(self):
pass
def bar(self):
pass
Abstract().foo() #@
Abstract().bar() #@
class Concrete(Abstract):
def foo(self):
return 123
Concrete().foo() #@
Concrete().bar() #@
"""
afoo, abar, cfoo, cbar = builder.extract_node(code)

assert next(afoo.infer()) is util.Uninferable
for node, value in [(abar, None), (cfoo, 123), (cbar, None)]:
inferred = next(node.infer())
assert isinstance(inferred, nodes.Const)
assert inferred.value == value

@pytest.mark.xfail(reason="unimplemented")
def test_overloaded_functions_are_uninferable(self):
code = """
class Node:
def infer(self, _context=None):
return self._infer() #@
def _infer(self):
pass
class Constant(Node):
def _infer(self):
return 123
c = Constant()
c.infer()
"""
node = builder.extract_node(code) # type: nodes.Return
func = node.value
# TODO: inferred_func is multiple values
inferred = next(func.infer())
assert inferred is util.Uninferable

def test_func_instance_attr(self):
"""test instance attributes for functions"""
data = """
Expand Down

0 comments on commit 1ff11af

Please sign in to comment.