1
0
forked from VimPlug/jedi

Merge pull request #166 from tkf/rethrow_uncaught

Better way of re-raising AttributeError
This commit is contained in:
David Halter
2013-03-13 21:43:14 -07:00
4 changed files with 84 additions and 48 deletions

View File

@@ -81,6 +81,25 @@ else:
eval(compile("""def exec_function(source, global_map):
exec source in global_map """, 'blub', 'exec'))
# re-raise function
if is_py3k:
def reraise(exception, traceback):
raise exception.with_traceback(traceback)
else:
eval(compile("""
def reraise(exception, traceback):
raise exception, None, traceback
""", 'blub', 'exec'))
reraise.__doc__ = """
Re-raise `exception` with a `traceback` object.
Usage::
reraise(Exception, sys.exc_info()[2])
"""
# StringIO (Python 2.5 has no io module), so use io only for py3k
try:
from StringIO import StringIO

View File

@@ -1,8 +1,10 @@
""" A universal module with functions / classes without dependencies. """
import sys
import contextlib
import functools
import tokenize
from _compatibility import next
from _compatibility import next, reraise
import debug
import settings
@@ -14,23 +16,47 @@ class MultiLevelStopIteration(Exception):
pass
class MultiLevelAttributeError(Exception):
class UncaughtAttributeError(Exception):
"""
Important, because `__getattr__` and `hasattr` catch AttributeErrors
implicitly. This is really evil (mainly because of `__getattr__`).
`hasattr` in Python 2 is even more evil, because it catches ALL exceptions.
Therefore this class has to be a `BaseException` and not an `Exception`.
But because I rewrote hasattr, we can now switch back to `Exception`.
Therefore this class originally had to be derived from `BaseException`
instead of `Exception`. But because I removed relevant `hasattr` from
the code base, we can now switch back to `Exception`.
:param base: return values of sys.exc_info().
"""
def __init__(self, base=None):
self.base = base
def __str__(self):
import traceback
tb = traceback.format_exception(*self.base)
return 'Original:\n\n' + ''.join(tb)
def rethrow_uncaught(func):
"""
Re-throw uncaught `AttributeError`.
Usage: Put ``@rethrow_uncaught`` in front of the function
which does **not** suppose to raise `AttributeError`.
AttributeError is easily get caught by `hasattr` and another
``except AttributeError`` clause. This becomes problem when you use
a lot of "dynamic" attributes (e.g., using ``@property``) because you
can't distinguish if the property does not exist for real or some code
inside of the "dynamic" attribute through that error. In a well
written code, such error should not exist but getting there is very
difficult. This decorator is to help us getting there by changing
`AttributeError` to `UncaughtAttributeError` to avoid unexpected catch.
This helps us noticing bugs earlier and facilitates debugging.
.. note:: Treating StopIteration here is easy.
Add that feature when needed.
"""
@functools.wraps(func)
def wrapper(*args, **kwds):
try:
return func(*args, **kwds)
except AttributeError:
exc_info = sys.exc_info()
reraise(UncaughtAttributeError(exc_info[1]), exc_info[2])
return wrapper
class PushBackIterator(object):

View File

@@ -68,7 +68,7 @@ backtracking algorithm.
.. todo:: nonlocal statement, needed or can be ignored? (py3k)
"""
from _compatibility import next, hasattr, is_py3k, unicode, utf8
from _compatibility import next, hasattr, is_py3k, unicode, reraise
import sys
import itertools
@@ -175,7 +175,7 @@ def get_names_of_scope(scope, position=None, star_search=True,
yield scope, get_defined_names_for_position(scope,
position, in_func_scope)
except StopIteration:
raise common.MultiLevelStopIteration('StopIteration raised')
reraise(common.MultiLevelStopIteration, sys.exc_info()[2])
if scope.isinstance(pr.ForFlow) and scope.is_list_comp:
# is a list comprehension
yield scope, scope.get_set_vars(is_internal_call=True)
@@ -581,12 +581,7 @@ def follow_statement(stmt, seek_name=None):
commands = stmt.get_commands()
debug.dbg('calls: %s' % commands)
try:
result = follow_call_list(commands)
except AttributeError:
# This is so evil! But necessary to propagate errors. The attribute
# errors here must not be catched, because they shouldn't exist.
raise common.MultiLevelAttributeError(sys.exc_info())
result = follow_call_list(commands)
# Assignment checking is only important if the statement defines multiple
# variables.
@@ -598,6 +593,7 @@ def follow_statement(stmt, seek_name=None):
return set(result)
@common.rethrow_uncaught
def follow_call_list(call_list, follow_array=False):
"""
`call_list` can be either `pr.Array` or `list of list`.

View File

@@ -9,7 +9,6 @@ instantiated. This class represents these cases.
So, why is there also a ``Class`` class here? Well, there are decorators and
they change classes in Python 3.
"""
import sys
import copy
import itertools
@@ -668,6 +667,7 @@ class Execution(Executable):
"""
return self.get_params() + pr.Scope.get_set_vars(self)
@common.rethrow_uncaught
def copy_properties(self, prop):
"""
Literally copies a property of a Function. Copying is very expensive,
@@ -675,22 +675,19 @@ class Execution(Executable):
objects can be used for the executions, as if they were in the
execution.
"""
try:
# Copy all these lists into this local function.
attr = getattr(self.base, prop)
objects = []
for element in attr:
if element is None:
copied = element
else:
copied = helpers.fast_parent_copy(element)
copied.parent = self._scope_copy(copied.parent)
if isinstance(copied, pr.Function):
copied = Function(copied)
objects.append(copied)
return objects
except AttributeError:
raise common.MultiLevelAttributeError(sys.exc_info())
# Copy all these lists into this local function.
attr = getattr(self.base, prop)
objects = []
for element in attr:
if element is None:
copied = element
else:
copied = helpers.fast_parent_copy(element)
copied.parent = self._scope_copy(copied.parent)
if isinstance(copied, pr.Function):
copied = Function(copied)
objects.append(copied)
return objects
def __getattr__(self, name):
if name not in ['start_pos', 'end_pos', 'imports', '_sub_module']:
@@ -698,21 +695,19 @@ class Execution(Executable):
return getattr(self.base, name)
@cache.memoize_default()
@common.rethrow_uncaught
def _scope_copy(self, scope):
try:
""" Copies a scope (e.g. if) in an execution """
# TODO method uses different scopes than the subscopes property.
""" Copies a scope (e.g. if) in an execution """
# TODO method uses different scopes than the subscopes property.
# just check the start_pos, sometimes it's difficult with closures
# to compare the scopes directly.
if scope.start_pos == self.start_pos:
return self
else:
copied = helpers.fast_parent_copy(scope)
copied.parent = self._scope_copy(copied.parent)
return copied
except AttributeError:
raise common.MultiLevelAttributeError(sys.exc_info())
# just check the start_pos, sometimes it's difficult with closures
# to compare the scopes directly.
if scope.start_pos == self.start_pos:
return self
else:
copied = helpers.fast_parent_copy(scope)
copied.parent = self._scope_copy(copied.parent)
return copied
@property
@cache.memoize_default()