diff options
-rw-r--r-- | ChangeLog | 11 | ||||
-rw-r--r-- | astroid/bases.py | 1 | ||||
-rw-r--r-- | astroid/interpreter/objectmodel.py | 27 | ||||
-rw-r--r-- | astroid/objects.py | 11 | ||||
-rw-r--r-- | astroid/protocols.py | 5 | ||||
-rw-r--r-- | astroid/tests/unittest_object_model.py | 42 |
6 files changed, 95 insertions, 2 deletions
@@ -30,6 +30,17 @@ Change log for the astroid package (used to be astng) objectmodel.py, which describes, in a more comprehensive way, the data model of each object. + * Exceptions have their own object model + + Some of exceptions's attributes, such as .args and .message, + can't be inferred correctly since they are descriptors that get + transformed into the proper objects at runtime. This can cause issues + with the static analysis, since they are inferred as different than + what's expected. Now when we're creating instances of exceptions, + we're inferring a special object that knows how to transform those + runtime attributes into the proper objects via a custom object model. + Closes issue #81 + * Fix a crash which occurred when a method had a same name as a builtin object, decorated at the same time by that builtin object ( a property for instance) diff --git a/astroid/bases.py b/astroid/bases.py index 126dc44b..43247f6c 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -271,7 +271,6 @@ class Instance(BaseInstance): pass - class UnboundMethod(Proxy): """a special node representing a method not bound to an instance""" diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py index cd79b961..2e8fedb7 100644 --- a/astroid/interpreter/objectmodel.py +++ b/astroid/interpreter/objectmodel.py @@ -41,6 +41,7 @@ mechanism. import itertools import pprint import os +import types import six @@ -537,3 +538,29 @@ class InstanceModel(ObjectModel): @property def py__dict__(self): return _dunder_dict(self._instance, self._instance.instance_attrs) + + +class ExceptionInstanceModel(InstanceModel): + + @property + def pyargs(self): + message = node_classes.Const('') + args = node_classes.Tuple(parent=self._instance) + args.postinit((message, )) + return args + + if six.PY3: + # It's available only on Python 3. + + @property + def py__traceback__(self): + builtins = astroid.MANAGER.builtins() + traceback_type = builtins[types.TracebackType.__name__] + return traceback_type.instantiate_class() + + if six.PY2: + # It's available only on Python 2. + + @property + def pymessage(self): + return node_classes.Const('') diff --git a/astroid/objects.py b/astroid/objects.py index 6597f3e8..83074664 100644 --- a/astroid/objects.py +++ b/astroid/objects.py @@ -169,3 +169,14 @@ class Super(node_classes.NodeNG): def getattr(self, name, context=None): return list(self.igetattr(name, context=context)) + + +class ExceptionInstance(bases.Instance): + """Class for instances of exceptions + + It has special treatment for some of the exceptions's attributes, + which are transformed at runtime into certain concrete objects, such as + the case of .args. + """ + + special_attributes = util.lazy_descriptor(lambda: objectmodel.ExceptionInstanceModel()) diff --git a/astroid/protocols.py b/astroid/protocols.py index 9efbad30..f7f90e4f 100644 --- a/astroid/protocols.py +++ b/astroid/protocols.py @@ -21,6 +21,8 @@ from astroid import helpers from astroid import nodes from astroid import util +raw_building = util.lazy_import('raw_building') +objects = util.lazy_import('objects') def _reflected_name(name): return "__r" + name[2:] @@ -384,7 +386,8 @@ def _resolve_asspart(parts, asspath, context): def excepthandler_assigned_stmts(self, node=None, context=None, asspath=None): for assigned in node_classes.unpack_infer(self.type): if isinstance(assigned, nodes.ClassDef): - assigned = bases.Instance(assigned) + assigned = objects.ExceptionInstance(assigned) + yield assigned # Explicit StopIteration to return error information, see comment # in raise_if_nothing_inferred. diff --git a/astroid/tests/unittest_object_model.py b/astroid/tests/unittest_object_model.py index 5eff4299..ec3f4e92 100644 --- a/astroid/tests/unittest_object_model.py +++ b/astroid/tests/unittest_object_model.py @@ -459,5 +459,47 @@ class GeneratorModelTest(unittest.TestCase): self.assertIsInstance(send, astroid.BoundMethod) +class ExceptionModelTest(unittest.TestCase): + + @unittest.skipIf(six.PY2, "needs Python 3") + def test_model_py3(self): + ast_nodes = test_utils.extract_node(''' + try: + x[42] + except ValueError as err: + err.args #@ + err.__traceback__ #@ + + err.message #@ + ''') + args = next(ast_nodes[0].infer()) + self.assertIsInstance(args, astroid.Tuple) + tb = next(ast_nodes[1].infer()) + self.assertIsInstance(tb, astroid.Instance) + self.assertEqual(tb.name, 'traceback') + + with self.assertRaises(exceptions.InferenceError): + next(ast_nodes[2].infer()) + + @unittest.skipUnless(six.PY2, "needs Python 2") + def test_model_py3(self): + ast_nodes = test_utils.extract_node(''' + try: + x[42] + except ValueError as err: + err.args #@ + err.message #@ + + err.__traceback__ #@ + ''') + args = next(ast_nodes[0].infer()) + self.assertIsInstance(args, astroid.Tuple) + message = next(ast_nodes[1].infer()) + self.assertIsInstance(message, astroid.Const) + + with self.assertRaises(exceptions.InferenceError): + next(ast_nodes[2].infer()) + + if __name__ == '__main__': unittest.main() |