summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCeridwen <ceridwenv@gmail.com>2015-11-06 10:48:59 -0500
committerCeridwen <ceridwenv@gmail.com>2015-11-06 10:48:59 -0500
commit336760405946c1ee0c8f1dc1e194b69ae73ed857 (patch)
treec5beb7e32d903a7bae271fa645bcdcc0b072a9fa
parent2a0ae68ad53ca60b7a31124ea2bef5c219d4e03b (diff)
downloadastroid-336760405946c1ee0c8f1dc1e194b69ae73ed857.tar.gz
Add structured exceptions to decorators and remaining functions.
* Use explicit StopIteration to pass information from generators to raise_if_nothing_inferred and path_wrapper, rather than return or implicit termination by reaching the end of the code block. * Remove remove_nodes in favor of handling the cases in local_attr, istance_attr, and getattr, to avoid the need for complicated information passing when needing to raise an exception.
-rw-r--r--astroid/decorators.py63
-rw-r--r--astroid/exceptions.py12
-rw-r--r--astroid/inference.py28
-rw-r--r--astroid/protocols.py27
-rw-r--r--astroid/scoped_nodes.py64
5 files changed, 112 insertions, 82 deletions
diff --git a/astroid/decorators.py b/astroid/decorators.py
index 815b540..e60eb49 100644
--- a/astroid/decorators.py
+++ b/astroid/decorators.py
@@ -93,15 +93,26 @@ def path_wrapper(func):
return
yielded = set()
- for res in _func(node, context, **kwargs):
- # unproxy only true instance, not const, tuple, dict...
- if res.__class__.__name__ == 'Instance':
- ares = res._proxied
+ generator = _func(node, context, **kwargs)
+ try:
+ while True:
+ res = next(generator)
+ # unproxy only true instance, not const, tuple, dict...
+ if res.__class__.__name__ == 'Instance':
+ ares = res._proxied
+ else:
+ ares = res
+ if ares not in yielded:
+ yield res
+ yielded.add(ares)
+ except StopIteration as error:
+ # Explicit StopIteration to return error information, see
+ # comment in raise_if_nothing_inferred.
+ if len(error.args) > 0:
+ util.reraise(StopIteration(error.args[0]))
else:
- ares = res
- if ares not in yielded:
- yield res
- yielded.add(ares)
+ util.reraise(StopIteration())
+
return wrapped
@@ -117,18 +128,32 @@ def yes_if_nothing_inferred(func, instance, args, kwargs):
@wrapt.decorator
def raise_if_nothing_inferred(func, instance, args, kwargs):
- '''All generators wrapped with raise_if_nothing_inferred *must* raise
- exceptions.DefaultStop if they can terminate without output, to
- propagate error information.
+ '''All generators wrapped with raise_if_nothing_inferred *must*
+ explicitly raise StopIteration with information to create an
+ appropriate structured InferenceError.
+
'''
+ # TODO: Explicitly raising StopIteration in a generator will cause
+ # a RuntimeError in Python >=3.7, as per
+ # http://legacy.python.org/dev/peps/pep-0479/ . Before 3.7 is
+ # released, this code will need to use one of four possible
+ # solutions: a decorator that restores the current behavior as
+ # described in
+ # http://legacy.python.org/dev/peps/pep-0479/#sub-proposal-decorator-to-explicitly-request-current-behaviour
+ # , dynamic imports or exec to generate different code for
+ # different versions, drop support for all Python versions <3.3,
+ # or refactoring to change how these decorators work. In any
+ # event, after dropping support for Python <3.3 this code should
+ # be refactored to use `yield from`.
inferred = False
- fields = {}
try:
- for node in func(*args, **kwargs):
+ generator = func(*args, **kwargs)
+ while True:
+ yield next(generator)
inferred = True
- yield node
- except exceptions.DefaultStop as e:
- fields = vars(e)
- del fields['message']
- if not inferred:
- raise exceptions.InferenceError(**fields)
+ except StopIteration as error:
+ if not inferred:
+ if len(error.args) > 0:
+ util.reraise(exceptions.InferenceError(**error.args[0]))
+ else:
+ util.reraise(exceptions.InferenceError())
diff --git a/astroid/exceptions.py b/astroid/exceptions.py
index 9f49753..ede21d9 100644
--- a/astroid/exceptions.py
+++ b/astroid/exceptions.py
@@ -69,18 +69,6 @@ class NoDefault(AstroidError):
super(NoDefault, self).__init__(message, **kws)
-class DefaultStop(AstroidError):
- '''This is a special error that's only meant to be raised in
- generators wrapped with raise_if_nothing_inferred and
- yes_if_nothing_inferred. It does nothing other than carry a set
- of attributes to be used in raising in InferenceError.
-
- '''
-
- # def __init__(self, message='{func!r} has no default for {name!r}.', **kws):
- # super(NoDefault, self).__init__(message, **kws)
-
-
class ResolveError(AstroidError):
"""Base class of astroid resolution/inference error.
diff --git a/astroid/inference.py b/astroid/inference.py
index b5e45df..fda4546 100644
--- a/astroid/inference.py
+++ b/astroid/inference.py
@@ -118,7 +118,9 @@ def infer_call(self, context=None):
except exceptions.InferenceError:
## XXX log error ?
continue
- raise exceptions.DefaultStop(node=self, context=context)
+ # Explicit StopIteration to return error information, see comment
+ # in raise_if_nothing_inferred.
+ raise StopIteration(dict(node=self, context=context))
nodes.Call._infer = infer_call
@@ -179,7 +181,9 @@ def infer_attribute(self, context=None):
except AttributeError:
# XXX method / function
context.boundnode = None
- raise exceptions.DefaultStop(node=self, context=context)
+ # Explicit StopIteration to return error information, see comment
+ # in raise_if_nothing_inferred.
+ raise StopIteration(dict(node=self, context=context))
nodes.Attribute._infer = decorators.path_wrapper(infer_attribute)
nodes.AssignAttr.infer_lhs = infer_attribute # # won't work with a path wrapper
@@ -282,7 +286,9 @@ def infer_subscript(self, context=None):
for inferred in assigned.infer(context):
yield inferred
- raise exceptions.DefaultStop(node=self, context=context)
+ # Explicit StopIteration to return error information, see comment
+ # in raise_if_nothing_inferred.
+ raise StopIteration(dict(node=self, context=context))
nodes.Subscript._infer = decorators.path_wrapper(infer_subscript)
nodes.Subscript.infer_lhs = infer_subscript
@@ -338,7 +344,9 @@ def _infer_boolop(self, context=None):
else:
yield value
- raise exceptions.DefaultStop(node=self, context=context)
+ # Explicit StopIteration to return error information, see comment
+ # in raise_if_nothing_inferred.
+ raise StopIteration(dict(node=self, context=context))
nodes.BoolOp._infer = _infer_boolop
@@ -411,7 +419,9 @@ def infer_unaryop(self, context=None):
for inferred in _filter_operation_errors(self, _infer_unaryop, context,
util.BadUnaryOperationMessage):
yield inferred
- raise exceptions.DefaultStop(node=self, context=context)
+ # Explicit StopIteration to return error information, see comment
+ # in raise_if_nothing_inferred.
+ raise StopIteration(dict(node=self, context=context))
nodes.UnaryOp._infer_unaryop = _infer_unaryop
nodes.UnaryOp._infer = infer_unaryop
@@ -728,11 +738,15 @@ def instance_getitem(self, index, context=None):
method = next(self.igetattr('__getitem__', context=context))
if not isinstance(method, bases.BoundMethod):
- raise exceptions.InferenceError(node=self, context=context)
+ raise exceptions.InferenceError(
+ 'Could not find __getitem__ for {node!r}.',
+ node=self, context=context)
try:
return next(method.infer_call_result(self, new_context))
except StopIteration:
- util.reraise(exceptions.InferenceError(node=self, context=context))
+ util.reraise(exceptions.InferenceError(
+ message='Inference for {node!r}[{index!s}] failed.',
+ node=self, index=index.value, context=context))
bases.Instance.getitem = instance_getitem
diff --git a/astroid/protocols.py b/astroid/protocols.py
index 2e01124..5809e4a 100644
--- a/astroid/protocols.py
+++ b/astroid/protocols.py
@@ -249,9 +249,10 @@ def for_assigned_stmts(self, node, context=None, asspath=None):
for inferred in _resolve_looppart(self.iter.infer(context),
asspath, context):
yield inferred
- raise exceptions.DefaultStop(node=self, unknown=node,
- assign_path=asspath, context=context)
-
+ # Explicit StopIteration to return error information, see comment
+ # in raise_if_nothing_inferred.
+ raise StopIteration(dict(node=self, unknown=node,
+ assign_path=asspath, context=context))
nodes.For.assigned_stmts = for_assigned_stmts
nodes.Comprehension.assigned_stmts = for_assigned_stmts
@@ -337,8 +338,10 @@ def assign_assigned_stmts(self, node, context=None, asspath=None):
return
for inferred in _resolve_asspart(self.value.infer(context), asspath, context):
yield inferred
- raise exceptions.DefaultStop(node=self, unknown=node,
- assign_path=asspath, context=context)
+ # Explicit StopIteration to return error information, see comment
+ # in raise_if_nothing_inferred.
+ raise StopIteration(dict(node=self, unknown=node,
+ assign_path=asspath, context=context))
nodes.Assign.assigned_stmts = assign_assigned_stmts
nodes.AugAssign.assigned_stmts = assign_assigned_stmts
@@ -379,9 +382,11 @@ def excepthandler_assigned_stmts(self, node, context=None, asspath=None):
if isinstance(assigned, nodes.ClassDef):
assigned = bases.Instance(assigned)
yield assigned
- raise exceptions.DefaultStop(node=self, unknown=node,
- assign_path=asspath, context=context)
-
+ # Explicit StopIteration to return error information, see comment
+ # in raise_if_nothing_inferred.
+ raise StopIteration(dict(node=self, unknown=node,
+ assign_path=asspath, context=context))
+
nodes.ExceptHandler.assigned_stmts = excepthandler_assigned_stmts
@@ -479,8 +484,10 @@ def with_assigned_stmts(self, node, context=None, asspath=None):
'in {node!r}.', node=self, targets=node,
assign_path=asspath, context=context))
yield obj
- raise exceptions.DefaultStop(node=self, unknown=node,
- assign_path=asspath, context=context)
+ # Explicit StopIteration to return error information, see comment
+ # in raise_if_nothing_inferred.
+ raise StopIteration(dict(node=self, unknown=node,
+ assign_path=asspath, context=context))
nodes.With.assigned_stmts = with_assigned_stmts
diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py
index 3b1e18a..4f5e39d 100644
--- a/astroid/scoped_nodes.py
+++ b/astroid/scoped_nodes.py
@@ -86,20 +86,6 @@ def _verify_duplicates_mro(sequences, cls, context):
mros=sequences, cls=cls, context=context)
-def remove_nodes(cls):
- @wrapt.decorator
- def decorator(func, instance, args, kwargs):
- nodes = [n for n in func(*args, **kwargs) if not isinstance(n, cls)]
- if not nodes:
- # TODO: no way to access the context when raising this error.
- raise exceptions.AttributeInferenceError(
- 'No nodes left after removing all {remove_type!r} from '
- 'nodes inferred for {node!r}.',
- node=instance, remove_type=cls)
- return nodes
- return decorator
-
-
def function_to_method(n, klass):
if isinstance(n, FunctionDef):
if n.type == 'classmethod':
@@ -359,23 +345,28 @@ class Module(LocalsDictNodeNG):
def display_type(self):
return 'Module'
- @remove_nodes(node_classes.DelName)
def getattr(self, name, context=None, ignore_locals=False):
+ result = []
if name in self.special_attributes:
if name == '__file__':
- return [node_classes.const_factory(self.file)] + self.locals.get(name, [])
- if name == '__path__' and self.package:
- return [node_classes.List()] + self.locals.get(name, [])
- return std_special_attributes(self, name)
- if not ignore_locals and name in self.locals:
- return self.locals[name]
- if self.package:
+ result = ([node_classes.const_factory(self.file)] +
+ self.locals.get(name, []))
+ elif name == '__path__' and self.package:
+ result = [node_classes.List()] + self.locals.get(name, [])
+ else:
+ result = std_special_attributes(self, name)
+ elif not ignore_locals and name in self.locals:
+ result = self.locals[name]
+ elif self.package:
try:
- return [self.import_module(name, relative_only=True)]
+ result = [self.import_module(name, relative_only=True)]
except (exceptions.AstroidBuildingException, SyntaxError):
util.reraise(exceptions.AttributeInferenceError(target=self,
attribute=name,
context=context))
+ result = [n for n in result if not isinstance(n, node_classes.DelName)]
+ if result:
+ return result
raise exceptions.AttributeInferenceError(target=self, attribute=name,
context=context)
@@ -1311,7 +1302,6 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG,
def has_base(self, node):
return node in self.bases
- @remove_nodes(node_classes.DelAttr)
def local_attr(self, name, context=None):
"""return the list of assign node associated to name in this class
locals or in its parents
@@ -1320,15 +1310,20 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG,
if no attribute with this name has been find in this class or
its parent classes
"""
- try:
- return self.locals[name]
- except KeyError:
- for class_node in self.local_attr_ancestors(name, context):
- return class_node.locals[name]
+ result = []
+ if name in self.locals:
+ result = self.locals[name]
+ else:
+ try:
+ result = next(self.local_attr_ancestors(name, context)).locals[name]
+ except StopIteration:
+ pass
+ result = [n for n in result if not isinstance(n, node_classes.DelAttr)]
+ if result:
+ return result
raise exceptions.AttributeInferenceError(target=self, attribute=name,
context=context)
- @remove_nodes(node_classes.DelAttr)
def instance_attr(self, name, context=None):
"""return the astroid nodes associated to name in this class instance
attributes dictionary and in its parents
@@ -1343,10 +1338,11 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG,
# get all values from parents
for class_node in self.instance_attr_ancestors(name, context):
values += class_node.instance_attrs[name]
- if not values:
- raise exceptions.AttributeInferenceError(target=self, attribute=name,
- context=context)
- return values
+ values = [n for n in values if not isinstance(n, node_classes.DelAttr)]
+ if values:
+ return values
+ raise exceptions.AttributeInferenceError(target=self, attribute=name,
+ context=context)
def instanciate_class(self):
"""return Instance of ClassDef node, else return self"""