From 1ba0f2d96fbc45ff0b6014b12db98716183e8277 Mon Sep 17 00:00:00 2001 From: Ceridwen Date: Mon, 2 Nov 2015 00:10:54 -0500 Subject: This bookmark adds structured exceptions to astroid. Major changes: * AstroidError has an __init__ that accepts arbitrary keyword-only arguments for adding information to exceptions, and a __str__ that lazily uses exception attributes to generate a message. The first positional argument to an exception is assigned to .message. The new API should be fully backwards compatible in general. * Some exceptions are combined or renamed; the old names are still available. * The OperationErrors used by pylint are now BadOperationMessages and located in util.py. * The AstroidBuildingException in _data_build stores the SyntaxError in its .error attribute rather than args[0]. * Many places where exceptions are raised have new, hopefully more useful error messages. The only major issue remaining is how to propagate information into decorators. --- astroid/arguments.py | 39 +++++-- astroid/bases.py | 31 +++--- astroid/brain/brain_builtin_inference.py | 10 +- astroid/brain/brain_gi.py | 4 +- astroid/brain/brain_six.py | 2 +- astroid/builder.py | 23 ++-- astroid/decorators.py | 16 ++- astroid/exceptions.py | 178 +++++++++++++++++++++++-------- astroid/helpers.py | 2 +- astroid/inference.py | 65 ++++++----- astroid/manager.py | 49 +++++---- astroid/mixins.py | 20 +++- astroid/node_classes.py | 17 +-- astroid/objects.py | 43 +++++--- astroid/protocols.py | 56 ++++++++-- astroid/scoped_nodes.py | 75 +++++++------ astroid/tests/unittest_brain.py | 2 +- astroid/tests/unittest_builder.py | 2 +- astroid/tests/unittest_lookup.py | 6 +- astroid/tests/unittest_nodes.py | 8 +- astroid/tests/unittest_objects.py | 20 ++-- astroid/tests/unittest_scoped_nodes.py | 45 ++++---- astroid/util.py | 35 ++++++ tox.ini | 3 +- 24 files changed, 506 insertions(+), 245 deletions(-) diff --git a/astroid/arguments.py b/astroid/arguments.py index 5670fa8..6483189 100644 --- a/astroid/arguments.py +++ b/astroid/arguments.py @@ -141,9 +141,18 @@ class CallSite(object): return values def infer_argument(self, funcnode, name, context): - """infer a function argument value according to the call context""" + """infer a function argument value according to the call context + + Arguments: + funcnode: The function being called. + name: The name of the argument whose value is being inferred. + context: TODO + """ if name in self.duplicated_keywords: - raise exceptions.InferenceError(name) + raise exceptions.InferenceError('The arguments passed to {func!r} ' + ' have duplicate keywords.', + call_site=self, func=funcnode, + arg=name, context=context) # Look into the keywords first, maybe it's already there. try: @@ -154,7 +163,11 @@ class CallSite(object): # Too many arguments given and no variable arguments. if len(self.positional_arguments) > len(funcnode.args.args): if not funcnode.args.vararg: - raise exceptions.InferenceError(name) + raise exceptions.InferenceError('Too many positional arguments ' + 'passed to {func!r} that does ' + 'not have *args.', + call_site=self, func=funcnode, + arg=name, context=context) positional = self.positional_arguments[:len(funcnode.args.args)] vararg = self.positional_arguments[len(funcnode.args.args):] @@ -204,7 +217,13 @@ class CallSite(object): # It wants all the keywords that were passed into # the call site. if self.has_invalid_keywords(): - raise exceptions.InferenceError + raise exceptions.InferenceError( + "Inference failed to find values for all keyword arguments " + "to {func!r}: {unpacked_kwargs!r} doesn't correspond to " + "{keyword_arguments!r}.", + keyword_arguments=self.keyword_arguments, + unpacked_kwargs = self._unpacked_kwargs, + call_site=self, func=funcnode, arg=name, context=context) kwarg = nodes.Dict(lineno=funcnode.args.lineno, col_offset=funcnode.args.col_offset, parent=funcnode.args) @@ -215,7 +234,13 @@ class CallSite(object): # It wants all the args that were passed into # the call site. if self.has_invalid_arguments(): - raise exceptions.InferenceError + raise exceptions.InferenceError( + "Inference failed to find values for all positional " + "arguments to {func!r}: {unpacked_args!r} doesn't " + "correspond to {positional_arguments!r}.", + positional_arguments=self.positional_arguments, + unpacked_args=self._unpacked_args, + call_site=self, func=funcnode, arg=name, context=context) args = nodes.Tuple(lineno=funcnode.args.lineno, col_offset=funcnode.args.col_offset, parent=funcnode.args) @@ -227,4 +252,6 @@ class CallSite(object): return funcnode.args.default_value(name).infer(context) except exceptions.NoDefault: pass - raise exceptions.InferenceError(name) + raise exceptions.InferenceError('No value found for argument {name} to ' + '{func!r}', call_site=self, + func=funcnode, arg=name, context=context) diff --git a/astroid/bases.py b/astroid/bases.py index 4b7f83b..15cf579 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -98,13 +98,15 @@ def _infer_stmts(stmts, context, frame=None): for inferred in stmt.infer(context=context): yield inferred inferred = True - except exceptions.UnresolvableName: + except exceptions.NameInferenceError: continue except exceptions.InferenceError: yield util.YES inferred = True if not inferred: - raise exceptions.InferenceError(str(stmt)) + raise exceptions.InferenceError( + 'Inference failed for all members of {stmts!r}.', + stmts=stmts, frame=frame, context=context) def _infer_method_result_truth(instance, method_name, context): @@ -129,7 +131,7 @@ class Instance(Proxy): def getattr(self, name, context=None, lookupclass=True): try: values = self._proxied.instance_attr(name, context) - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError as exception: if name == '__class__': return [self._proxied] if lookupclass: @@ -139,14 +141,16 @@ class Instance(Proxy): return self._proxied.local_attr(name) return self._proxied.getattr(name, context, class_context=False) - util.reraise(exceptions.NotFoundError(name)) + util.reraise(exceptions.AttributeInferenceError(target=self, + attribute=name, + context=context)) # since we've no context information, return matching class members as # well if lookupclass: try: return values + self._proxied.getattr(name, context, class_context=False) - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError: pass return values @@ -164,15 +168,15 @@ class Instance(Proxy): for stmt in _infer_stmts(self._wrap_attr(get_attr, context), context, frame=self): yield stmt - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError: try: - # fallback to class'igetattr since it has some logic to handle + # fallback to class.igetattr since it has some logic to handle # descriptors for stmt in self._wrap_attr(self._proxied.igetattr(name, context), context): yield stmt - except exceptions.NotFoundError: - util.reraise(exceptions.InferenceError(name)) + except exceptions.AttributeInferenceError as error: + util.reraise(exceptions.InferenceError(**vars(error))) def _wrap_attr(self, attrs, context=None): """wrap bound methods of attrs in a InstanceMethod proxies""" @@ -207,7 +211,8 @@ class Instance(Proxy): inferred = True yield res if not inferred: - raise exceptions.InferenceError() + raise exceptions.InferenceError(node=self, caller=caller, + context=context) def __repr__(self): return '' % (self._proxied.root().name, @@ -221,7 +226,7 @@ class Instance(Proxy): try: self._proxied.getattr('__call__', class_context=False) return True - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError: return False def pytype(self): @@ -248,11 +253,11 @@ class Instance(Proxy): try: result = _infer_method_result_truth(self, BOOL_SPECIAL_METHOD, context) - except (exceptions.InferenceError, exceptions.NotFoundError): + except (exceptions.InferenceError, exceptions.AttributeInferenceError): # Fallback to __len__. try: result = _infer_method_result_truth(self, '__len__', context) - except (exceptions.NotFoundError, exceptions.InferenceError): + except (exceptions.AttributeInferenceError, exceptions.InferenceError): return True return result diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py index c6245be..eb61b70 100644 --- a/astroid/brain/brain_builtin_inference.py +++ b/astroid/brain/brain_builtin_inference.py @@ -5,8 +5,8 @@ import sys from textwrap import dedent import six -from astroid import (MANAGER, UseInferenceDefault, NotFoundError, - inference_tip, InferenceError, UnresolvableName) +from astroid import (MANAGER, UseInferenceDefault, AttributeInferenceError, + inference_tip, InferenceError, NameInferenceError) from astroid import arguments from astroid.builder import AstroidBuilder from astroid import helpers @@ -193,7 +193,7 @@ def _get_elts(arg, context): (nodes.List, nodes.Tuple, nodes.Set)) try: inferred = next(arg.infer(context)) - except (InferenceError, UnresolvableName): + except (InferenceError, NameInferenceError): raise UseInferenceDefault() if isinstance(inferred, nodes.Dict): items = inferred.items @@ -356,7 +356,7 @@ def infer_getattr(node, context=None): try: return next(obj.igetattr(attr, context=context)) - except (StopIteration, InferenceError, NotFoundError): + except (StopIteration, InferenceError, AttributeInferenceError): if len(node.args) == 3: # Try to infer the default and return it instead. try: @@ -384,7 +384,7 @@ def infer_hasattr(node, context=None): except UseInferenceDefault: # Can't infer something from this function call. return util.YES - except NotFoundError: + except AttributeInferenceError: # Doesn't have it. return nodes.Const(False) return nodes.Const(True) diff --git a/astroid/brain/brain_gi.py b/astroid/brain/brain_gi.py index f8acb42..0860207 100644 --- a/astroid/brain/brain_gi.py +++ b/astroid/brain/brain_gi.py @@ -114,7 +114,7 @@ def _gi_build_stub(parent): def _import_gi_module(modname): # we only consider gi.repository submodules if not modname.startswith('gi.repository.'): - raise AstroidBuildingException() + raise AstroidBuildingException(modname=modname) # build astroid representation unless we already tried so if modname not in _inspected_modules: modnames = [modname] @@ -155,7 +155,7 @@ def _import_gi_module(modname): else: astng = _inspected_modules[modname] if astng is None: - raise AstroidBuildingException('Failed to import module %r' % modname) + raise AstroidBuildingException(modname=modname) return astng def _looks_like_require_version(node): diff --git a/astroid/brain/brain_six.py b/astroid/brain/brain_six.py index a1043ea..3b2b945 100644 --- a/astroid/brain/brain_six.py +++ b/astroid/brain/brain_six.py @@ -254,7 +254,7 @@ def six_moves_transform(): def _six_fail_hook(modname): if modname != 'six.moves': - raise AstroidBuildingException + raise AstroidBuildingException(modname=modname) module = AstroidBuilder(MANAGER).string_build(_IMPORTS) module.name = 'six.moves' return module diff --git a/astroid/builder.py b/astroid/builder.py index 9bb78b1..4db4051 100644 --- a/astroid/builder.py +++ b/astroid/builder.py @@ -51,8 +51,9 @@ if sys.version_info >= (3, 0): data = stream.read() except UnicodeError: # wrong encoding # detect_encoding returns utf-8 if no encoding specified - msg = 'Wrong (%s) or no encoding specified' % encoding - util.reraise(exceptions.AstroidBuildingException(msg)) + util.reraise(exceptions.AstroidBuildingException( + 'Wrong ({encoding}) or no encoding specified for {filename}.', + encoding=encoding, filename=filename)) return stream, encoding, data else: @@ -123,11 +124,13 @@ class AstroidBuilder(raw_building.InspectBuilder): try: stream, encoding, data = open_source_file(path) except IOError as exc: - msg = 'Unable to load file %r (%s)' % (path, exc) - util.reraise(exceptions.AstroidBuildingException(msg)) + util.reraise(exceptions.AstroidBuildingException( + 'Unable to load file {path}:\n{error}', + modname=modname, path=path, error=exc)) except (SyntaxError, LookupError) as exc: - # Python 3 encoding specification error or unknown encoding - util.reraise(exceptions.AstroidBuildingException(*exc.args)) + util.reraise(exceptions.AstroidBuildingException( + 'Python 3 encoding specification error or unknown encoding:\n' + '{error}', modname=modname, path=path, error=exc)) with stream: # get module name if necessary if modname is None: @@ -169,12 +172,16 @@ class AstroidBuilder(raw_building.InspectBuilder): try: node = _parse(data + '\n') except (TypeError, ValueError) as exc: - util.reraise(exceptions.AstroidBuildingException(*exc.args)) + util.reraise(exceptions.AstroidBuildingException( + 'Parsing Python code failed:\n{error}', + source=data, modname=modname, path=path, error=exc)) except SyntaxError as exc: # Pass the entire exception object to AstroidBuildingException, # since pylint uses this as an introspection method, # in order to find what error happened. - util.reraise(exceptions.AstroidBuildingException(exc)) + util.reraise(exceptions.AstroidBuildingException( + 'Syntax error in Python source: {error}', + source=data, modname=modname, path=path, error=exc)) if path is not None: node_file = os.path.abspath(path) else: diff --git a/astroid/decorators.py b/astroid/decorators.py index 0709744..27ce983 100644 --- a/astroid/decorators.py +++ b/astroid/decorators.py @@ -117,9 +117,17 @@ 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. + ''' inferred = False - for node in func(*args, **kwargs): - inferred = True - yield node + fields = {} + try: + for node in func(*args, **kwargs): + inferred = True + yield node + except exceptions.DefaultStop as e: + fields = vars(e) if not inferred: - raise exceptions.InferenceError() + raise exceptions.InferenceError(**fields) diff --git a/astroid/exceptions.py b/astroid/exceptions.py index b308258..9f49753 100644 --- a/astroid/exceptions.py +++ b/astroid/exceptions.py @@ -16,88 +16,176 @@ # You should have received a copy of the GNU Lesser General Public License along # with astroid. If not, see . """this module contains exceptions used in the astroid library - """ +from astroid import util + class AstroidError(Exception): - """base exception class for all astroid related exceptions""" + """base exception class for all astroid related exceptions + + AstroidError and its subclasses are structured, intended to hold + objects representing state when the exception is thrown. Field + values are passed to the constructor as keyword-only arguments. + Each subclass has its own set of standard fields, but use your + best judgment to decide whether a specific exception instance + needs more or fewer fields for debugging. Field values may be + used to lazily generate the error message: self.message.format() + will be called with the field names and values supplied as keyword + arguments. + """ + def __init__(self, message='', **kws): + self.message = message + for key, value in kws.items(): + setattr(self, key, value) + + def __str__(self): + return self.message.format(**vars(self)) + class AstroidBuildingException(AstroidError): - """exception class when we are unable to build an astroid representation""" + """exception class when we are unable to build an astroid representation + + Standard attributes: + modname: Name of the module that AST construction failed for. + error: Exception raised during construction. + """ + + def __init__(self, message='Failed to import module {modname}.', **kws): + super(AstroidBuildingException, self).__init__(message, **kws) + + +class NoDefault(AstroidError): + """raised by function's `default_value` method when an argument has + no default value + + Standard attributes: + func: Function node. + name: Name of argument without a default. + """ + func = None + name = None + + def __init__(self, message='{func!r} has no default for {name!r}.', **kws): + 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""" + """Base class of astroid resolution/inference error. + + ResolveError is not intended to be raised. + + Standard attributes: + context: InferenceContext object. + """ + context = None + class MroError(ResolveError): - """Error raised when there is a problem with method resolution of a class.""" + """Error raised when there is a problem with method resolution of a class. + Standard attributes: + mros: A sequence of sequences containing ClassDef nodes. + cls: ClassDef node whose MRO resolution failed. + context: InferenceContext object. + """ + mros = () + cls = None + + def __str__(self): + mro_names = ", ".join("({})".format(", ".join(b.name for b in m)) + for m in self.mros) + return self.message.format(mros=mro_names, cls=self.cls) class DuplicateBasesError(MroError): """Error raised when there are duplicate bases in the same class bases.""" - class InconsistentMroError(MroError): """Error raised when a class's MRO is inconsistent.""" class SuperError(ResolveError): - """Error raised when there is a problem with a super call.""" + """Error raised when there is a problem with a super call. -class SuperArgumentTypeError(SuperError): - """Error raised when the super arguments are invalid.""" + Standard attributes: + super_: The Super instance that raised the exception. + context: InferenceContext object. + """ + super_ = None + def __str__(self): + return self.message.format(**vars(self.super_)) -class NotFoundError(ResolveError): - """raised when we are unable to resolve a name""" class InferenceError(ResolveError): - """raised when we are unable to infer a node""" + """raised when we are unable to infer a node -class UseInferenceDefault(Exception): - """exception to be raised in custom inference function to indicate that it - should go back to the default behaviour + Standard attributes: + node: The node inference was called on. + context: InferenceContext object. """ + node = None + context= None -class UnresolvableName(InferenceError): - """raised when we are unable to resolve a name""" - -class NoDefault(AstroidError): - """raised by function's `default_value` method when an argument has - no default value - """ + def __init__(self, message='Inference failed for {node!r}.', **kws): + super(InferenceError, self).__init__(message, **kws) -class OperationError(object): - """Object which describes a TypeError occurred somewhere in the inference chain +# Why does this inherit from InferenceError rather than ResolveError? +# Changing it causes some inference tests to fail. +class NameInferenceError(InferenceError): + """Raised when a name lookup fails, corresponds to NameError. - This is not an exception, but a container object which holds the types and - the error which occurred. + Standard attributes: + name: The name for which lookup failed, as a string. + scope: The node representing the scope in which the lookup occurred. + context: InferenceContext object. """ + name = None + scope = None + def __init__(self, message='{name!r} not found in {scope!r}.', **kws): + super(NameInferenceError, self).__init__(message, **kws) -class UnaryOperationError(OperationError): - """Object which describes operational failures on UnaryOps.""" - def __init__(self, operand, op, error): - self.operand = operand - self.op = op - self.error = error +class AttributeInferenceError(ResolveError): + """Raised when an attribute lookup fails, corresponds to AttributeError. - def __str__(self): - operand_type = self.operand.name - msg = "bad operand type for unary {}: {}" - return msg.format(self.op, operand_type) + Standard attributes: + target: The node for which lookup failed. + attribute: The attribute for which lookup failed, as a string. + context: InferenceContext object. + """ + target = None + attribute = None + def __init__(self, message='{attribute!r} not found on {target!r}.', **kws): + super(AttributeInferenceError, self).__init__(message, **kws) -class BinaryOperationError(OperationError): - """Object which describes type errors for BinOps.""" - def __init__(self, left_type, op, right_type): - self.left_type = left_type - self.right_type = right_type - self.op = op +class UseInferenceDefault(Exception): + """exception to be raised in custom inference function to indicate that it + should go back to the default behaviour + """ - def __str__(self): - msg = "unsupported operand type(s) for {}: {!r} and {!r}" - return msg.format(self.op, self.left_type.name, self.right_type.name) + +# Backwards-compatibility aliases +OperationError = util.BadOperationMessage +UnaryOperationError = util.BadUnaryOperationMessage +BinaryOperationError = util.BadBinaryOperationMessage + +SuperArgumentTypeError = SuperError +UnresolvableName = NameInferenceError +NotFoundError = AttributeInferenceError diff --git a/astroid/helpers.py b/astroid/helpers.py index 00f4784..d4cd0dd 100644 --- a/astroid/helpers.py +++ b/astroid/helpers.py @@ -86,7 +86,7 @@ def object_type(node, context=None): This is used to implement the ``type`` builtin, which means that it's used for inferring type calls, as well as used in a couple of other places - in the inference. + in the inference. The node will be inferred first, so this function can support all sorts of objects, as long as they support inference. """ diff --git a/astroid/inference.py b/astroid/inference.py index 2943596..b5e45df 100644 --- a/astroid/inference.py +++ b/astroid/inference.py @@ -89,7 +89,9 @@ def infer_name(self, context=None): _, stmts = parent_function.lookup(self.name) if not stmts: - raise exceptions.UnresolvableName(self.name) + raise exceptions.NameInferenceError(name=self.name, + scope=self.scope(), + context=context) context = context.clone() context.lookupname = self.name return bases._infer_stmts(stmts, context, frame) @@ -116,6 +118,7 @@ def infer_call(self, context=None): except exceptions.InferenceError: ## XXX log error ? continue + raise exceptions.DefaultStop(node=self, context=context) nodes.Call._infer = infer_call @@ -124,7 +127,7 @@ def infer_import(self, context=None, asname=True): """infer an Import node: return the imported module/object""" name = context.lookupname if name is None: - raise exceptions.InferenceError() + raise exceptions.InferenceError(node=self, context=context) if asname: yield self.do_import_module(self.real_name(name)) else: @@ -144,7 +147,7 @@ def infer_import_from(self, context=None, asname=True): """infer a ImportFrom node: return the imported module/object""" name = context.lookupname if name is None: - raise exceptions.InferenceError() + raise exceptions.InferenceError(node=self, context=context) if asname: name = self.real_name(name) module = self.do_import_module() @@ -153,8 +156,9 @@ def infer_import_from(self, context=None, asname=True): context.lookupname = name stmts = module.getattr(name, ignore_locals=module is self.root()) return bases._infer_stmts(stmts, context) - except exceptions.NotFoundError: - util.reraise(exceptions.InferenceError(name)) + except exceptions.AttributeInferenceError as error: + util.reraise(exceptions.InferenceError( + error.message, target=self, attribute=name, context=context)) nodes.ImportFrom._infer = infer_import_from @@ -170,11 +174,12 @@ def infer_attribute(self, context=None): for obj in owner.igetattr(self.attrname, context): yield obj context.boundnode = None - except (exceptions.NotFoundError, exceptions.InferenceError): + except (exceptions.AttributeInferenceError, exceptions.InferenceError): context.boundnode = None except AttributeError: # XXX method / function context.boundnode = None + raise exceptions.DefaultStop(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 @@ -182,12 +187,13 @@ nodes.AssignAttr.infer_lhs = infer_attribute # # won't work with a path wrapper @decorators.path_wrapper def infer_global(self, context=None): if context.lookupname is None: - raise exceptions.InferenceError() + raise exceptions.InferenceError(node=self, context=context) try: return bases._infer_stmts(self.root().getattr(context.lookupname), context) - except exceptions.NotFoundError: - util.reraise(exceptions.InferenceError()) + except exceptions.AttributeInferenceError as error: + util.reraise(exceptions.InferenceError( + error.message, target=self, attribute=name, context=context)) nodes.Global._infer = infer_global @@ -257,15 +263,16 @@ def infer_subscript(self, context=None): if index: index_value = index.value else: - raise exceptions.InferenceError() + raise exceptions.InferenceError(node=self, context=context) if index_value is _SLICE_SENTINEL: - raise exceptions.InferenceError + raise exceptions.InferenceError(node=self, context=context) try: assigned = value.getitem(index_value, context) except (IndexError, TypeError, AttributeError) as exc: - util.reraise(exceptions.InferenceError(*exc.args)) + util.reraise(exceptions.InferenceError(node=self, error=exc, + context=context)) # Prevent inferring if the inferred subscript # is the same as the original subscripted object. @@ -275,6 +282,8 @@ def infer_subscript(self, context=None): for inferred in assigned.infer(context): yield inferred + raise exceptions.DefaultStop(node=self, context=context) + nodes.Subscript._infer = decorators.path_wrapper(infer_subscript) nodes.Subscript.infer_lhs = infer_subscript @@ -329,6 +338,8 @@ def _infer_boolop(self, context=None): else: yield value + raise exceptions.DefaultStop(node=self, context=context) + nodes.BoolOp._infer = _infer_boolop @@ -352,7 +363,7 @@ def _infer_unaryop(self, context=None): yield operand.infer_unary_op(self.op) except TypeError as exc: # The operand doesn't support this operation. - yield exceptions.UnaryOperationError(operand, self.op, exc) + yield util.BadUnaryOperationMessage(operand, self.op, exc) except AttributeError as exc: meth = protocols.UNARY_OP_METHOD[self.op] if meth is None: @@ -368,7 +379,7 @@ def _infer_unaryop(self, context=None): if not isinstance(operand, bases.Instance): # The operation was used on something which # doesn't support it. - yield exceptions.UnaryOperationError(operand, self.op, exc) + yield util.BadUnaryOperationMessage(operand, self.op, exc) continue try: @@ -386,9 +397,9 @@ def _infer_unaryop(self, context=None): yield operand else: yield result - except exceptions.NotFoundError as exc: + except exceptions.AttributeInferenceError as exc: # The unary operation special method was not found. - yield exceptions.UnaryOperationError(operand, self.op, exc) + yield util.BadUnaryOperationMessage(operand, self.op, exc) except exceptions.InferenceError: yield util.YES @@ -397,8 +408,10 @@ def _infer_unaryop(self, context=None): @decorators.path_wrapper def infer_unaryop(self, context=None): """Infer what an UnaryOp should return when evaluated.""" - return _filter_operation_errors(self, _infer_unaryop, context, - exceptions.UnaryOperationError) + for inferred in _filter_operation_errors(self, _infer_unaryop, context, + util.BadUnaryOperationMessage): + yield inferred + raise exceptions.DefaultStop(node=self, context=context) nodes.UnaryOp._infer_unaryop = _infer_unaryop nodes.UnaryOp._infer = infer_unaryop @@ -544,7 +557,7 @@ def _infer_binary_operation(left, right, op, context, flow_factory): results = list(method()) except AttributeError: continue - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError: continue except exceptions.InferenceError: yield util.YES @@ -569,9 +582,9 @@ def _infer_binary_operation(left, right, op, context, flow_factory): for result in results: yield result return - # TODO(cpopa): yield a BinaryOperationError here, + # TODO(cpopa): yield a BadBinaryOperationMessage here, # since the operation is not supported - yield exceptions.BinaryOperationError(left_type, op, right_type) + yield util.BadBinaryOperationMessage(left_type, op, right_type) def _infer_binop(self, context): @@ -610,7 +623,7 @@ def _infer_binop(self, context): @decorators.path_wrapper def infer_binop(self, context=None): return _filter_operation_errors(self, _infer_binop, context, - exceptions.BinaryOperationError) + util.BadBinaryOperationMessage) nodes.BinOp._infer_binop = _infer_binop nodes.BinOp._infer = infer_binop @@ -649,7 +662,7 @@ def _infer_augassign(self, context=None): @decorators.path_wrapper def infer_augassign(self, context=None): return _filter_operation_errors(self, _infer_augassign, context, - exceptions.BinaryOperationError) + util.BadBinaryOperationMessage) nodes.AugAssign._infer_augassign = _infer_augassign nodes.AugAssign._infer = infer_augassign @@ -660,7 +673,7 @@ nodes.AugAssign._infer = infer_augassign def infer_arguments(self, context=None): name = context.lookupname if name is None: - raise exceptions.InferenceError() + raise exceptions.InferenceError(node=self, context=context) return protocols._arguments_infer_argname(self, name, context) nodes.Arguments._infer = infer_arguments @@ -715,11 +728,11 @@ def instance_getitem(self, index, context=None): method = next(self.igetattr('__getitem__', context=context)) if not isinstance(method, bases.BoundMethod): - raise exceptions.InferenceError + raise exceptions.InferenceError(node=self, context=context) try: return next(method.infer_call_result(self, new_context)) except StopIteration: - util.reraise(exceptions.InferenceError()) + util.reraise(exceptions.InferenceError(node=self, context=context)) bases.Instance.getitem = instance_getitem diff --git a/astroid/manager.py b/astroid/manager.py index 07d7543..c14125f 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -90,7 +90,7 @@ class AstroidManager(object): elif fallback and modname: return self.ast_from_module_name(modname) raise exceptions.AstroidBuildingException( - 'unable to get astroid for file %s' % filepath) + 'Unable to build an AST for {path}.', path=filepath) def _build_stub_module(self, modname): from astroid.builder import AstroidBuilder @@ -127,15 +127,18 @@ class AstroidManager(object): try: module = modutils.load_module_from_name(modname) except Exception as ex: # pylint: disable=broad-except - msg = 'Unable to load module %s (%s)' % (modname, ex) - util.reraise(exceptions.AstroidBuildingException(msg)) + util.reraise(exceptions.AstroidBuildingException( + 'Loading {modname} failed with:\n{error}', + modname=modname, path=filepath, error=ex)) return self.ast_from_module(module, modname) elif mp_type == imp.PY_COMPILED: - msg = "Unable to load compiled module %s" % (modname,) - raise exceptions.AstroidBuildingException(msg) + raise exceptions.AstroidBuildingException( + "Unable to load compiled module {modname}.", + modname=modname, path=filepath) if filepath is None: - msg = "Unable to load module %s" % (modname,) - raise exceptions.AstroidBuildingException(msg) + raise exceptions.AstroidBuildingException( + "Can't find a file for module {modname}.", + modname=modname) return self.ast_from_file(filepath, modname, fallback=False) except exceptions.AstroidBuildingException as e: for hook in self._failed_import_hooks: @@ -179,8 +182,9 @@ class AstroidManager(object): modname.split('.'), context_file=contextfile) traceback = sys.exc_info()[2] except ImportError as ex: - msg = 'Unable to load module %s (%s)' % (modname, ex) - value = exceptions.AstroidBuildingException(msg) + value = exceptions.AstroidBuildingException( + 'Failed to import module {modname} with error:\n{error}.', + modname=modname, error=ex) traceback = sys.exc_info()[2] self._mod_file_cache[(modname, contextfile)] = value if isinstance(value, exceptions.AstroidBuildingException): @@ -209,8 +213,9 @@ class AstroidManager(object): try: modname = klass.__module__ except AttributeError: - msg = 'Unable to get module for class %s' % safe_repr(klass) - util.reraise(exceptions.AstroidBuildingException(msg)) + util.reraise(exceptions.AstroidBuildingException( + 'Unable to get module for class {class_name}.', + cls=klass, class_repr=safe_repr(klass), modname=modname)) modastroid = self.ast_from_module_name(modname) return modastroid.getattr(klass.__name__)[0] # XXX @@ -223,21 +228,23 @@ class AstroidManager(object): try: modname = klass.__module__ except AttributeError: - msg = 'Unable to get module for %s' % safe_repr(klass) - util.reraise(exceptions.AstroidBuildingException(msg)) + util.reraise(exceptions.AstroidBuildingException( + 'Unable to get module for {class_repr}.', + cls=klass, class_repr=safe_repr(klass))) except Exception as ex: # pylint: disable=broad-except - msg = ('Unexpected error while retrieving module for %s: %s' - % (safe_repr(klass), ex)) - util.reraise(exceptions.AstroidBuildingException(msg)) + util.reraise(exceptions.AstroidBuildingException( + 'Unexpected error while retrieving module for {class_repr}:\n' + '{error}', cls=klass, class_repr=safe_repr(klass), error=ex)) try: name = klass.__name__ except AttributeError: - msg = 'Unable to get name for %s' % safe_repr(klass) - util.reraise(exceptions.AstroidBuildingException(msg)) + util.reraise(exceptions.AstroidBuildingException( + 'Unable to get name for {class_repr}:\n', + cls=klass, class_repr=safe_repr(klass))) except Exception as ex: # pylint: disable=broad-except - exc = ('Unexpected error while retrieving name for %s: %s' - % (safe_repr(klass), ex)) - util.reraise(exceptions.AstroidBuildingException(exc)) + util.reraise(exceptions.AstroidBuildingException( + 'Unexpected error while retrieving name for {class_repr}:\n' + '{error}', cls=klass, class_repr=safe_repr(klass), error=ex)) # take care, on living object __module__ is regularly wrong :( modastroid = self.ast_from_module_name(modname) if klass is obj: diff --git a/astroid/mixins.py b/astroid/mixins.py index 9f5f953..6ee7b4d 100644 --- a/astroid/mixins.py +++ b/astroid/mixins.py @@ -129,11 +129,19 @@ class ImportFromMixin(FilterStmtsMixin): return mymodule.import_module(modname, level=level, relative_only=level and level >= 1) except exceptions.AstroidBuildingException as ex: - if isinstance(ex.args[0], SyntaxError): - util.reraise(exceptions.InferenceError(str(ex))) - util.reraise(exceptions.InferenceError(modname)) + if isinstance(getattr(ex, 'error', None), SyntaxError): + util.reraise(exceptions.InferenceError( + 'Could not import {modname} because of SyntaxError:\n' + '{syntax_error}', modname=modname, syntax_error=ex.error, + import_node=self)) + util.reraise(exceptions.InferenceError('Could not import {modname}.', + modname=modname, + import_node=self)) except SyntaxError as ex: - util.reraise(exceptions.InferenceError(str(ex))) + util.reraise(exceptions.InferenceError( + 'Could not import {modname} because of SyntaxError:\n' + '{syntax_error}', modname=modname, syntax_error=ex, + import_node=self)) def real_name(self, asname): """get name from 'as' name""" @@ -145,4 +153,6 @@ class ImportFromMixin(FilterStmtsMixin): _asname = name if asname == _asname: return name - raise exceptions.NotFoundError(asname) + raise exceptions.AttributeInferenceError( + 'Could not find original name for {attribute} in {target!r}', + target=self, attribute=asname) diff --git a/astroid/node_classes.py b/astroid/node_classes.py index 7da41f5..5a92210 100644 --- a/astroid/node_classes.py +++ b/astroid/node_classes.py @@ -412,7 +412,8 @@ class NodeNG(object): def _infer(self, context=None): """we don't know how to resolve a statement by default""" # this method is overridden by most concrete classes - raise exceptions.InferenceError(self.__class__.__name__) + raise exceptions.InferenceError('No inference function for {node!r}.', + node=self, context=context) def inferred(self): '''return list of inferred values for a more simple inference usage''' @@ -894,7 +895,7 @@ class Arguments(mixins.AssignTypeMixin, NodeNG): i = _find_arg(argname, self.kwonlyargs)[0] if i is not None and self.kw_defaults[i] is not None: return self.kw_defaults[i] - raise exceptions.NoDefault() + raise exceptions.NoDefault(func=self.parent, name=argname) def is_argument(self, name): """return True if the name is defined in arguments""" @@ -1011,13 +1012,13 @@ class AugAssign(mixins.AssignTypeMixin, Statement): def type_errors(self, context=None): """Return a list of TypeErrors which can occur during inference. - Each TypeError is represented by a :class:`BinaryOperationError`, + Each TypeError is represented by a :class:`BadBinaryOperationMessage`, which holds the original exception. """ try: results = self._infer_augassign(context=context) return [result for result in results - if isinstance(result, exceptions.BinaryOperationError)] + if isinstance(result, util.BadBinaryOperationMessage)] except exceptions.InferenceError: return [] @@ -1053,13 +1054,13 @@ class BinOp(NodeNG): def type_errors(self, context=None): """Return a list of TypeErrors which can occur during inference. - Each TypeError is represented by a :class:`BinaryOperationError`, + Each TypeError is represented by a :class:`BadBinaryOperationMessage`, which holds the original exception. """ try: results = self._infer_binop(context=context) return [result for result in results - if isinstance(result, exceptions.BinaryOperationError)] + if isinstance(result, util.BadBinaryOperationMessage)] except exceptions.InferenceError: return [] @@ -1745,13 +1746,13 @@ class UnaryOp(NodeNG): def type_errors(self, context=None): """Return a list of TypeErrors which can occur during inference. - Each TypeError is represented by a :class:`UnaryOperationError`, + Each TypeError is represented by a :class:`BadUnaryOperationMessage`, which holds the original exception. """ try: results = self._infer_unaryop(context=context) return [result for result in results - if isinstance(result, exceptions.UnaryOperationError)] + if isinstance(result, util.BadUnaryOperationMessage)] except exceptions.InferenceError: return [] diff --git a/astroid/objects.py b/astroid/objects.py index 3ab0a65..c880a4d 100644 --- a/astroid/objects.py +++ b/astroid/objects.py @@ -86,8 +86,9 @@ class Super(node_classes.NodeNG): def super_mro(self): """Get the MRO which will be used to lookup attributes in this super.""" if not isinstance(self.mro_pointer, scoped_nodes.ClassDef): - raise exceptions.SuperArgumentTypeError( - "The first super argument must be type.") + raise exceptions.SuperError( + "The first argument to super must be a subtype of " + "type, not {mro_pointer}.", super_=self) if isinstance(self.type, scoped_nodes.ClassDef): # `super(type, type)`, most likely in a class method. @@ -96,18 +97,20 @@ class Super(node_classes.NodeNG): else: mro_type = getattr(self.type, '_proxied', None) if not isinstance(mro_type, (bases.Instance, scoped_nodes.ClassDef)): - raise exceptions.SuperArgumentTypeError( - "super(type, obj): obj must be an " - "instance or subtype of type") + raise exceptions.SuperError( + "The second argument to super must be an " + "instance or subtype of type, not {type}.", + super_=self) if not mro_type.newstyle: - raise exceptions.SuperError("Unable to call super on old-style classes.") + raise exceptions.SuperError("Unable to call super on old-style classes.", super_=self) mro = mro_type.mro() if self.mro_pointer not in mro: - raise exceptions.SuperArgumentTypeError( - "super(type, obj): obj must be an " - "instance or subtype of type") + raise exceptions.SuperError( + "The second argument to super must be an " + "instance or subtype of type, not {type}.", + super_=self) index = mro.index(self.mro_pointer) return mro[index + 1:] @@ -138,11 +141,19 @@ class Super(node_classes.NodeNG): try: mro = self.super_mro() - except (exceptions.MroError, exceptions.SuperError) as exc: - # Don't let invalid MROs or invalid super calls - # to leak out as is from this function. - util.reraise(exceptions.NotFoundError(*exc.args)) - + # Don't let invalid MROs or invalid super calls + # leak out as is from this function. + except exceptions.SuperError as exc: + util.reraise(exceptions.AttributeInferenceError( + ('Lookup for {name} on {target!r} because super call {super!r} ' + 'is invalid.'), + target=self, attribute=name, context=context, super_=exc.super_)) + except exceptions.MroError as exc: + util.reraise(exceptions.AttributeInferenceError( + ('Lookup for {name} on {target!r} failed because {cls!r} has an ' + 'invalid MRO.'), + target=self, attribute=name, context=context, mros=exc.mros, + cls=exc.cls)) found = False for cls in mro: if name not in cls.locals: @@ -166,7 +177,9 @@ class Super(node_classes.NodeNG): yield bases.BoundMethod(inferred, cls) if not found: - raise exceptions.NotFoundError(name) + raise exceptions.AttributeInferenceError(target=self, + attribute=name, + context=context) def getattr(self, name, context=None): return list(self.igetattr(name, context=context)) diff --git a/astroid/protocols.py b/astroid/protocols.py index 7c9e4b9..2e01124 100644 --- a/astroid/protocols.py +++ b/astroid/protocols.py @@ -238,7 +238,6 @@ def _resolve_looppart(parts, asspath, context): except exceptions.InferenceError: break - @decorators.raise_if_nothing_inferred def for_assigned_stmts(self, node, context=None, asspath=None): if asspath is None: @@ -250,6 +249,9 @@ 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) + nodes.For.assigned_stmts = for_assigned_stmts nodes.Comprehension.assigned_stmts = for_assigned_stmts @@ -335,6 +337,8 @@ 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) nodes.Assign.assigned_stmts = assign_assigned_stmts nodes.AugAssign.assigned_stmts = assign_assigned_stmts @@ -375,6 +379,9 @@ 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) + nodes.ExceptHandler.assigned_stmts = excepthandler_assigned_stmts @@ -415,7 +422,7 @@ def _infer_context_manager(self, mgr, context): elif isinstance(inferred, bases.Instance): try: enter = next(inferred.igetattr('__enter__', context=context)) - except (exceptions.InferenceError, exceptions.NotFoundError): + except (exceptions.InferenceError, exceptions.AttributeInferenceError): return if not isinstance(enter, bases.BoundMethod): return @@ -443,8 +450,13 @@ def with_assigned_stmts(self, node, context=None, asspath=None): pass # ContextManager().infer() will return ContextManager # f.infer() will return 42. - """ + Arguments: + self: nodes.With + node: The target of the assignment, `as (a, b)` in `with foo as (a, b)`. + context: TODO + asspath: TODO + """ mgr = next(mgr for (mgr, vars) in self.items if vars == node) if asspath is None: for result in _infer_context_manager(self, mgr, context): @@ -455,30 +467,49 @@ def with_assigned_stmts(self, node, context=None, asspath=None): obj = result for index in asspath: if not hasattr(obj, 'elts'): - raise exceptions.InferenceError + raise exceptions.InferenceError( + 'Wrong type ({targets!r}) for {node!r} assignment', + node=self, targets=node, assign_path=asspath, + context=context) try: obj = obj.elts[index] except IndexError: - util.reraise(exceptions.InferenceError()) + util.reraise(exceptions.InferenceError( + 'Tried to infer a nonexistent target with index {index} ' + '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) nodes.With.assigned_stmts = with_assigned_stmts @decorators.yes_if_nothing_inferred def starred_assigned_stmts(self, node=None, context=None, asspath=None): + """ + Arguments: + self: nodes.Starred + node: TODO + context: TODO + asspath: TODO + """ stmt = self.statement() if not isinstance(stmt, (nodes.Assign, nodes.For)): - raise exceptions.InferenceError() + raise exceptions.InferenceError('Statement {stmt!r} enclosing {node!r} ' + 'must be an Assign or For node.', + node=self, stmt=stmt, unknown=node, + context=context) if isinstance(stmt, nodes.Assign): value = stmt.value lhs = stmt.targets[0] if sum(1 for node in lhs.nodes_of_class(nodes.Starred)) > 1: - # Too many starred arguments in the expression. - raise exceptions.InferenceError() + raise exceptions.InferenceError('Too many starred arguments in the ' + ' assignment targets {lhs!r}.', + node=self, targets=lhs, + unknown=node, context=context) if context is None: context = contextmod.InferenceContext() @@ -494,8 +525,11 @@ def starred_assigned_stmts(self, node=None, context=None, asspath=None): elts = collections.deque(rhs.elts[:]) if len(lhs.elts) > len(rhs.elts): - # a, *b, c = (1, 2) - raise exceptions.InferenceError() + raise exceptions.InferenceError('More targets, {targets!r}, than ' + 'values to unpack, {values!r}.', + node=self, targets=lhs, + values=rhs, unknown=node, + context=context) # Unpack iteratively the values from the rhs of the assignment, # until the find the starred node. What will remain will diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py index e6e3324..eb7baf7 100644 --- a/astroid/scoped_nodes.py +++ b/astroid/scoped_nodes.py @@ -43,7 +43,7 @@ BUILTINS = six.moves.builtins.__name__ ITER_METHODS = ('__iter__', '__getitem__') -def _c3_merge(sequences): +def _c3_merge(sequences, cls, context): """Merges MROs in *sequences* to a single MRO using the C3 algorithm. Adapted from http://www.python.org/download/releases/2.3/mro/. @@ -65,12 +65,10 @@ def _c3_merge(sequences): if not candidate: # Show all the remaining bases, which were considered as # candidates for the next mro sequence. - bases = ["({})".format(", ".join(base.name - for base in subsequence)) - for subsequence in sequences] raise exceptions.InconsistentMroError( - "Cannot create a consistent method resolution " - "order for bases %s" % ", ".join(bases)) + message="Cannot create a consistent method resolution order " + "for MROs {mros} of class {cls!r}.", + mros=sequences, cls=cls, context=context) result.append(candidate) # remove the chosen candidate @@ -79,11 +77,13 @@ def _c3_merge(sequences): del seq[0] -def _verify_duplicates_mro(sequences): +def _verify_duplicates_mro(sequences, cls, context): for sequence in sequences: names = [node.qname() for node in sequence] if len(names) != len(set(names)): - raise exceptions.DuplicateBasesError('Duplicates found in the mro.') + raise exceptions.DuplicateBasesError( + message='Duplicates found in MROs {mros} for {cls!r}.', + mros=sequences, cls=cls, context=context) def remove_nodes(cls): @@ -91,7 +91,10 @@ def remove_nodes(cls): def decorator(func, instance, args, kwargs): nodes = [n for n in func(*args, **kwargs) if not isinstance(n, cls)] if not nodes: - raise exceptions.NotFoundError() + # TODO: no way to access the name or context when raising + # this error. + raise exceptions.AttributeInferenceError( + 'No nodes left after filtering.', target=instance) return nodes return decorator @@ -116,7 +119,8 @@ def std_special_attributes(self, name, add_locals=True): return [node_classes.const_factory(self.doc)] + locals.get(name, []) if name == '__dict__': return [node_classes.Dict()] + locals.get(name, []) - raise exceptions.NotFoundError(name) + # TODO: missing context + raise exceptions.AttributeInferenceError(target=self, attribute=name) MANAGER = manager.AstroidManager() @@ -344,7 +348,7 @@ class Module(LocalsDictNodeNG): if name in self.scope_attrs and name not in self.locals: try: return self, self.getattr(name) - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError: return self, () return self._scope_lookup(node, name, offset) @@ -368,8 +372,11 @@ class Module(LocalsDictNodeNG): try: return [self.import_module(name, relative_only=True)] except (exceptions.AstroidBuildingException, SyntaxError): - util.reraise(exceptions.NotFoundError(name)) - raise exceptions.NotFoundError(name) + util.reraise(exceptions.AttributeInferenceError(target=self, + attribute=name, + context=context)) + raise exceptions.AttributeInferenceError(target=self, attribute=name, + context=context) def igetattr(self, name, context=None): """inferred getattr""" @@ -380,8 +387,9 @@ class Module(LocalsDictNodeNG): try: return bases._infer_stmts(self.getattr(name, context), context, frame=self) - except exceptions.NotFoundError: - util.reraise(exceptions.InferenceError(name)) + except exceptions.AttributeInferenceError as error: + util.reraise(exceptions.InferenceError( + error.message, target=self, attribute=name, context=context)) def fully_defined(self): """return True if this module has been built from a .py file @@ -853,8 +861,9 @@ class FunctionDef(node_classes.Statement, Lambda): try: return bases._infer_stmts(self.getattr(name, context), context, frame=self) - except exceptions.NotFoundError: - util.reraise(exceptions.InferenceError(name)) + except exceptions.AttributeInferenceError as error: + util.reraise(exceptions.InferenceError( + error.message, target=self, attribute=name, context=context)) def is_method(self): """return true if the function node should be considered as a method""" @@ -1306,7 +1315,7 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG, """return the list of assign node associated to name in this class locals or in its parents - :raises `NotFoundError`: + :raises `AttributeInferenceError`: if no attribute with this name has been find in this class or its parent classes """ @@ -1315,14 +1324,15 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG, except KeyError: for class_node in self.local_attr_ancestors(name, context): return class_node.locals[name] - raise exceptions.NotFoundError(name) + 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 - :raises `NotFoundError`: + :raises `AttributeInferenceError`: if no attribute with this name has been find in this class or its parent classes """ @@ -1333,7 +1343,8 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG, for class_node in self.instance_attr_ancestors(name, context): values += class_node.instance_attrs[name] if not values: - raise exceptions.NotFoundError(name) + raise exceptions.AttributeInferenceError(target=self, attribute=name, + context=context) return values def instanciate_class(self): @@ -1378,7 +1389,8 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG, if class_context: values += self._metaclass_lookup_attribute(name, context) if not values: - raise exceptions.NotFoundError(name) + raise exceptions.AttributeInferenceError(target=self, attribute=name, + context=context) return values def _metaclass_lookup_attribute(self, name, context): @@ -1397,7 +1409,7 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG, try: attrs = cls.getattr(name, context=context, class_context=True) - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError: return for attr in bases._infer_stmts(attrs, context, frame=cls): @@ -1439,18 +1451,19 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG, and isinstance(inferred, bases.Instance)): try: inferred._proxied.getattr('__get__', context) - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError: yield inferred else: yield util.YES else: yield function_to_method(inferred, self) - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError as error: if not name.startswith('__') and self.has_dynamic_getattr(context): # class handle some dynamic attributes, return a YES object yield util.YES else: - util.reraise(exceptions.InferenceError(name)) + util.reraise(exceptions.InferenceError( + error.message, target=self, attribute=name, context=context)) def has_dynamic_getattr(self, context=None): """ @@ -1467,12 +1480,12 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG, try: return _valid_getattr(self.getattr('__getattr__', context)[0]) - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError: #if self.newstyle: XXX cause an infinite recursion error try: getattribute = self.getattr('__getattribute__', context)[0] return _valid_getattr(getattribute) - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError: pass return False @@ -1592,7 +1605,7 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG, try: slots.getattr(meth) break - except exceptions.NotFoundError: + except exceptions.AttributeInferenceError: continue else: continue @@ -1722,8 +1735,8 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG, bases_mro.append(ancestors) unmerged_mro = ([[self]] + bases_mro + [bases]) - _verify_duplicates_mro(unmerged_mro) - return _c3_merge(unmerged_mro) + _verify_duplicates_mro(unmerged_mro, self, context) + return _c3_merge(unmerged_mro, self, context) def bool_value(self): return True diff --git a/astroid/tests/unittest_brain.py b/astroid/tests/unittest_brain.py index 3520b49..ca47d52 100644 --- a/astroid/tests/unittest_brain.py +++ b/astroid/tests/unittest_brain.py @@ -134,7 +134,7 @@ class NamedTupleTest(unittest.TestCase): instance = next(result.infer()) self.assertEqual(len(instance.getattr('scheme')), 1) self.assertEqual(len(instance.getattr('port')), 1) - with self.assertRaises(astroid.NotFoundError): + with self.assertRaises(astroid.AttributeInferenceError): instance.getattr('foo') self.assertEqual(len(instance.getattr('geturl')), 1) self.assertEqual(instance.name, 'ParseResult') diff --git a/astroid/tests/unittest_builder.py b/astroid/tests/unittest_builder.py index 84bf50c..485963b 100644 --- a/astroid/tests/unittest_builder.py +++ b/astroid/tests/unittest_builder.py @@ -451,7 +451,7 @@ class BuilderTest(unittest.TestCase): self.assertIsInstance(astroid.getattr('CSTE')[0], nodes.AssignName) self.assertEqual(astroid.getattr('CSTE')[0].fromlineno, 2) self.assertEqual(astroid.getattr('CSTE')[1].fromlineno, 6) - with self.assertRaises(exceptions.NotFoundError): + with self.assertRaises(exceptions.AttributeInferenceError): astroid.getattr('CSTE2') with self.assertRaises(exceptions.InferenceError): next(astroid['global_no_effect'].ilookup('CSTE2')) diff --git a/astroid/tests/unittest_lookup.py b/astroid/tests/unittest_lookup.py index 805efd9..7f9c43a 100644 --- a/astroid/tests/unittest_lookup.py +++ b/astroid/tests/unittest_lookup.py @@ -174,7 +174,7 @@ class LookupTest(resources.SysPathSetup, unittest.TestCase): if sys.version_info < (3, 0): self.assertEqual(var.inferred(), [util.YES]) else: - self.assertRaises(exceptions.UnresolvableName, var.inferred) + self.assertRaises(exceptions.NameInferenceError, var.inferred) def test_dict_comps(self): astroid = builder.parse(""" @@ -210,7 +210,7 @@ class LookupTest(resources.SysPathSetup, unittest.TestCase): var """) var = astroid.body[1].value - self.assertRaises(exceptions.UnresolvableName, var.inferred) + self.assertRaises(exceptions.NameInferenceError, var.inferred) def test_generator_attributes(self): tree = builder.parse(""" @@ -250,7 +250,7 @@ class LookupTest(resources.SysPathSetup, unittest.TestCase): self.assertTrue(p2.getattr('__name__')) self.assertTrue(astroid['NoName'].getattr('__name__')) p3 = next(astroid['p3'].infer()) - self.assertRaises(exceptions.NotFoundError, p3.getattr, '__name__') + self.assertRaises(exceptions.AttributeInferenceError, p3.getattr, '__name__') def test_function_module_special(self): astroid = builder.parse(''' diff --git a/astroid/tests/unittest_nodes.py b/astroid/tests/unittest_nodes.py index ade8a92..55c7268 100644 --- a/astroid/tests/unittest_nodes.py +++ b/astroid/tests/unittest_nodes.py @@ -331,13 +331,13 @@ class ImportNodeTest(resources.SysPathSetup, unittest.TestCase): self.assertEqual(from_.real_name('NameNode'), 'Name') imp_ = self.module['os'] self.assertEqual(imp_.real_name('os'), 'os') - self.assertRaises(exceptions.NotFoundError, imp_.real_name, 'os.path') + self.assertRaises(exceptions.AttributeInferenceError, imp_.real_name, 'os.path') imp_ = self.module['NameNode'] self.assertEqual(imp_.real_name('NameNode'), 'Name') - self.assertRaises(exceptions.NotFoundError, imp_.real_name, 'Name') + self.assertRaises(exceptions.AttributeInferenceError, imp_.real_name, 'Name') imp_ = self.module2['YO'] self.assertEqual(imp_.real_name('YO'), 'YO') - self.assertRaises(exceptions.NotFoundError, imp_.real_name, 'data') + self.assertRaises(exceptions.AttributeInferenceError, imp_.real_name, 'data') def test_as_string(self): ast = self.module['modutils'] @@ -502,7 +502,7 @@ class UnboundMethodNodeTest(unittest.TestCase): meth = A.test ''') node = next(ast['meth'].infer()) - with self.assertRaises(exceptions.NotFoundError): + with self.assertRaises(exceptions.AttributeInferenceError): node.getattr('__missssing__') name = node.getattr('__name__')[0] self.assertIsInstance(name, nodes.Const) diff --git a/astroid/tests/unittest_objects.py b/astroid/tests/unittest_objects.py index e0a04d5..dc91d94 100644 --- a/astroid/tests/unittest_objects.py +++ b/astroid/tests/unittest_objects.py @@ -233,15 +233,15 @@ class SuperTests(unittest.TestCase): self.assertIsInstance(first, objects.Super) with self.assertRaises(exceptions.SuperError) as cm: first.super_mro() - self.assertEqual(str(cm.exception), "The first super argument must be type.") - for node in ast_nodes[1:]: + self.assertIsInstance(cm.exception.super_.mro_pointer, nodes.Const) + self.assertEqual(cm.exception.super_.mro_pointer.value, 1) + for node, invalid_type in zip(ast_nodes[1:], + (nodes.Const, bases.Instance)): inferred = next(node.infer()) self.assertIsInstance(inferred, objects.Super, node) - with self.assertRaises(exceptions.SuperArgumentTypeError) as cm: + with self.assertRaises(exceptions.SuperError) as cm: inferred.super_mro() - self.assertEqual(str(cm.exception), - "super(type, obj): obj must be an instance " - "or subtype of type", node) + self.assertIsInstance(cm.exception.super_.type, invalid_type) def test_proxied(self): node = test_utils.extract_node(''' @@ -338,9 +338,9 @@ class SuperTests(unittest.TestCase): with self.assertRaises(exceptions.InferenceError): next(ast_nodes[2].infer()) fourth = next(ast_nodes[3].infer()) - with self.assertRaises(exceptions.NotFoundError): + with self.assertRaises(exceptions.AttributeInferenceError): fourth.getattr('test3') - with self.assertRaises(exceptions.NotFoundError): + with self.assertRaises(exceptions.AttributeInferenceError): next(fourth.igetattr('test3')) first_unbound = next(ast_nodes[4].infer()) @@ -362,7 +362,7 @@ class SuperTests(unittest.TestCase): super(Super, self) #@ ''') inferred = next(node.infer()) - with self.assertRaises(exceptions.NotFoundError): + with self.assertRaises(exceptions.AttributeInferenceError): next(inferred.getattr('test')) def test_super_complex_mro(self): @@ -491,7 +491,7 @@ class SuperTests(unittest.TestCase): inferred = next(node.infer()) with self.assertRaises(exceptions.SuperError): inferred.super_mro() - with self.assertRaises(exceptions.SuperArgumentTypeError): + with self.assertRaises(exceptions.SuperError): inferred.super_mro() diff --git a/astroid/tests/unittest_scoped_nodes.py b/astroid/tests/unittest_scoped_nodes.py index 329aa69..47807aa 100644 --- a/astroid/tests/unittest_scoped_nodes.py +++ b/astroid/tests/unittest_scoped_nodes.py @@ -24,12 +24,13 @@ from functools import partial import unittest import warnings +import astroid from astroid import builder from astroid import nodes from astroid import scoped_nodes from astroid import util from astroid.exceptions import ( - InferenceError, NotFoundError, + InferenceError, AttributeInferenceError, NoDefault, ResolveError, MroError, InconsistentMroError, DuplicateBasesError, ) @@ -75,7 +76,7 @@ class ModuleNodeTest(ModuleLoader, unittest.TestCase): os.path.abspath(resources.find('data/module.py'))) self.assertEqual(len(self.module.getattr('__dict__')), 1) self.assertIsInstance(self.module.getattr('__dict__')[0], nodes.Dict) - self.assertRaises(NotFoundError, self.module.getattr, '__path__') + self.assertRaises(AttributeInferenceError, self.module.getattr, '__path__') self.assertEqual(len(self.pack.getattr('__path__')), 1) self.assertIsInstance(self.pack.getattr('__path__')[0], nodes.List) @@ -101,7 +102,6 @@ class ModuleNodeTest(ModuleLoader, unittest.TestCase): self.assertEqual(cnx.name, 'Connection') self.assertEqual(cnx.root().name, 'data.SSL1.Connection1') self.assertEqual(len(self.nonregr.getattr('enumerate')), 2) - # raise ResolveError self.assertRaises(InferenceError, self.nonregr.igetattr, 'YOAA') def test_wildcard_import_names(self): @@ -574,7 +574,7 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase): self.assertEqual(cls.getattr('__module__')[0].value, 'data.module') self.assertEqual(len(cls.getattr('__dict__')), 1) if not cls.newstyle: - self.assertRaises(NotFoundError, cls.getattr, '__mro__') + self.assertRaises(AttributeInferenceError, cls.getattr, '__mro__') for cls in (nodes.List._proxied, nodes.Const(1)._proxied): self.assertEqual(len(cls.getattr('__bases__')), 1) self.assertEqual(len(cls.getattr('__name__')), 1) @@ -620,9 +620,9 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase): def test_instance_special_attributes(self): for inst in (Instance(self.module['YO']), nodes.List(), nodes.Const(1)): - self.assertRaises(NotFoundError, inst.getattr, '__mro__') - self.assertRaises(NotFoundError, inst.getattr, '__bases__') - self.assertRaises(NotFoundError, inst.getattr, '__name__') + self.assertRaises(AttributeInferenceError, inst.getattr, '__mro__') + self.assertRaises(AttributeInferenceError, inst.getattr, '__bases__') + self.assertRaises(AttributeInferenceError, inst.getattr, '__name__') self.assertEqual(len(inst.getattr('__dict__')), 1) self.assertEqual(len(inst.getattr('__doc__')), 1) @@ -718,7 +718,7 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase): method_locals = klass2.local_attr('method') self.assertEqual(len(method_locals), 1) self.assertEqual(method_locals[0].name, 'method') - self.assertRaises(NotFoundError, klass2.local_attr, 'nonexistant') + self.assertRaises(AttributeInferenceError, klass2.local_attr, 'nonexistant') methods = {m.name for m in klass2.methods()} self.assertTrue(methods.issuperset(expected_methods)) @@ -1297,18 +1297,16 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase): self.assertEqualMro(astroid['E1'], ['E1', 'C1', 'B1', 'A1', 'object']) with self.assertRaises(InconsistentMroError) as cm: astroid['F1'].mro() - self.assertEqual(str(cm.exception), - "Cannot create a consistent method resolution order " - "for bases (B1, C1, A1, object), " - "(C1, B1, A1, object)") - + A1 = astroid.getattr('A1')[0] + B1 = astroid.getattr('B1')[0] + C1 = astroid.getattr('C1')[0] + object_ = builder.MANAGER.astroid_cache[BUILTINS].getattr('object')[0] + self.assertEqual(cm.exception.mros, [[B1, C1, A1, object_], + [C1, B1, A1, object_]]) with self.assertRaises(InconsistentMroError) as cm: astroid['G1'].mro() - self.assertEqual(str(cm.exception), - "Cannot create a consistent method resolution order " - "for bases (C1, B1, A1, object), " - "(B1, C1, A1, object)") - + self.assertEqual(cm.exception.mros, [[C1, B1, A1, object_], + [B1, C1, A1, object_]]) self.assertEqualMro( astroid['PedalWheelBoat'], ["PedalWheelBoat", "EngineLess", @@ -1329,9 +1327,10 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase): with self.assertRaises(DuplicateBasesError) as cm: astroid['Duplicates'].mro() - self.assertEqual(str(cm.exception), "Duplicates found in the mro.") - self.assertTrue(issubclass(cm.exception.__class__, MroError)) - self.assertTrue(issubclass(cm.exception.__class__, ResolveError)) + Duplicates = astroid.getattr('Duplicates')[0] + self.assertEqual(cm.exception.cls, Duplicates) + self.assertIsInstance(cm.exception, MroError) + self.assertIsInstance(cm.exception, ResolveError) def test_generator_from_infer_call_result_parent(self): func = test_utils.extract_node(""" @@ -1357,7 +1356,7 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase): self.assertEqual(first["a"].value, 1) self.assertIsInstance(first["b"], nodes.Const) self.assertEqual(first["b"].value, 2) - with self.assertRaises(NotFoundError): + with self.assertRaises(AttributeInferenceError): first.getattr("missing") def test_implicit_metaclass(self): @@ -1376,7 +1375,7 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase): instance = cls.instanciate_class() func = cls.getattr('mro') self.assertEqual(len(func), 1) - self.assertRaises(NotFoundError, instance.getattr, 'mro') + self.assertRaises(AttributeInferenceError, instance.getattr, 'mro') def test_metaclass_lookup_using_same_class(self): # Check that we don't have recursive attribute access for metaclass diff --git a/astroid/util.py b/astroid/util.py index 20c44d7..90ea7d6 100644 --- a/astroid/util.py +++ b/astroid/util.py @@ -49,6 +49,41 @@ class YES(object): return self +class BadOperationMessage(object): + """Object which describes a TypeError occurred somewhere in the inference chain + + This is not an exception, but a container object which holds the types and + the error which occurred. + """ + + +class BadUnaryOperationMessage(BadOperationMessage): + """Object which describes operational failures on UnaryOps.""" + + def __init__(self, operand, op, error): + self.operand = operand + self.op = op + self.error = error + + def __str__(self): + operand_type = self.operand.name + msg = "bad operand type for unary {}: {}" + return msg.format(self.op, operand_type) + + +class BadBinaryOperationMessage(BadOperationMessage): + """Object which describes type errors for BinOps.""" + + def __init__(self, left_type, op, right_type): + self.left_type = left_type + self.right_type = right_type + self.op = op + + def __str__(self): + msg = "unsupported operand type(s) for {}: {!r} and {!r}" + return msg.format(self.op, self.left_type.name, self.right_type.name) + + def _instancecheck(cls, other): wrapped = cls.__wrapped__ other_cls = other.__class__ diff --git a/tox.ini b/tox.ini index 53c797e..8b23ed2 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ # official list is # envlist = py27, py33, py34, pypy, jython envlist = py27, py33, pylint +# envlist = py27, py34 [testenv:pylint] deps = @@ -29,4 +30,4 @@ deps = six wrapt -commands = python -m unittest discover -s {envsitepackagesdir}/astroid/tests -p "unittest*.py" +commands = python -m unittest discover -s {envsitepackagesdir}/astroid/tests -p "unittest*.py" -- cgit v1.2.1