From 1ff11afaaedcc8d1f916ba1af47639e73ad3d578 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 15 May 2021 23:24:50 +0200 Subject: [PATCH] 983 - Infer return value of None for correctly inferred functions with no returns Squashed commit of the following: commit 7ec6a68cdc3077b5cb634a7e1c1c793248f4eb01 Author: Andrew Haigh Date: Mon May 3 09:09:17 2021 +1000 Update changelog commit b50a7dde7a2a83ae23b4d81896f3eed7e8aca2fc Author: Andrew Haigh Date: Mon May 3 09:02:29 2021 +1000 Update check of implicit return to ignore abstract methods Ref #485. To avoid additional breakage, we optionally extend is_abstract to consider functions whose body is any raise statement (not just raise NotImplementedError) commit ff719c81f904b8b68be6a9f0530ba3cd70ef9b37 Author: Andrew Haigh Date: Sat May 1 12:19:52 2021 +1000 Add check of __getitem__ signature to instance_getitem commit 427d422e150dfdb442bd0d34b2421b3db3ab3a14 Author: Andrew Haigh Date: Sat May 1 11:45:27 2021 +1000 Fix test definition of igetattr recursion and context_manager_inference Ref #663. This test did not actually check for regression of the issue fixed in 55076ca0 (i.e. it also passed on c87bea17 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 8ec2b471862b50b8f0197d52658cd0b36045a7a1 Author: Andrew Haigh Date: Sat Apr 24 09:16:16 2021 +1000 Update FunctionDef.infer_call_result to infer None with no Returns Ref #485. If the function was inferred (unlike many compiler-builtins) and it contains no Return nodes, then the implicit return value is None. --- ChangeLog | 5 +++ astroid/bases.py | 6 +++ astroid/scoped_nodes.py | 13 +++++-- tests/unittest_inference.py | 28 ++++---------- tests/unittest_scoped_nodes.py | 71 ++++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 24 deletions(-) diff --git a/ChangeLog b/ChangeLog index 9d7b945fde..1be9949748 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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? ============================ diff --git a/astroid/bases.py b/astroid/bases.py index 02da1a8876..97b10366bf 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -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)) diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py index 27237e8296..8a1152204d 100644 --- a/astroid/scoped_nodes.py +++ b/astroid/scoped_nodes.py @@ -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 ' 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 @@ -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) @@ -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( diff --git a/tests/unittest_inference.py b/tests/unittest_inference.py index e41ebf01ed..832d64fee9 100644 --- a/tests/unittest_inference.py +++ b/tests/unittest_inference.py @@ -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'] #@ """ @@ -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(): diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py index 7fe537b2fb..cedcbccf63 100644 --- a/tests/unittest_scoped_nodes.py +++ b/tests/unittest_scoped_nodes.py @@ -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 = """