From e42ff9e762dcf3e3fec21bd061bb787a1326a871 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 13 Mar 2013 23:21:15 +0100 Subject: [PATCH 1/6] Add common.rethrow_uncaught --- jedi/_compatibility.py | 10 ++++++++++ jedi/common.py | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index 78abf874..5f03f736 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -81,6 +81,16 @@ 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')) + # StringIO (Python 2.5 has no io module), so use io only for py3k try: from StringIO import StringIO diff --git a/jedi/common.py b/jedi/common.py index 35a10f62..83337eef 100644 --- a/jedi/common.py +++ b/jedi/common.py @@ -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 @@ -33,6 +35,17 @@ class MultiLevelAttributeError(Exception): return 'Original:\n\n' + ''.join(tb) +def rethrow_uncaught(func): + @functools.wraps(func) + def wrapper(*args, **kwds): + try: + return func(*args, **kwds) + except AttributeError: + exc_info = sys.exc_info() + reraise(MultiLevelAttributeError, exc_info[2]) + return wrapper + + class PushBackIterator(object): def __init__(self, iterator): self.pushes = [] From eca0f01cfb79a718834574413d0d117783b110a9 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 13 Mar 2013 23:21:49 +0100 Subject: [PATCH 2/6] Use @common.rethrow_uncaught and stop manual re-raise --- jedi/evaluate.py | 8 ++--- jedi/evaluate_representation.py | 55 +++++++++++++++------------------ 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/jedi/evaluate.py b/jedi/evaluate.py index 8d68b050..3ad076bc 100644 --- a/jedi/evaluate.py +++ b/jedi/evaluate.py @@ -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`. diff --git a/jedi/evaluate_representation.py b/jedi/evaluate_representation.py index 82d2a034..0b5995e7 100644 --- a/jedi/evaluate_representation.py +++ b/jedi/evaluate_representation.py @@ -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() From 3b78b52204de48e09e726f5c1d15480bf846ee79 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 13 Mar 2013 23:24:26 +0100 Subject: [PATCH 3/6] Rename MultiLevelAttributeErro to UncaughtAttributeError --- jedi/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jedi/common.py b/jedi/common.py index 83337eef..37d3bb21 100644 --- a/jedi/common.py +++ b/jedi/common.py @@ -16,7 +16,7 @@ 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__`). @@ -42,7 +42,7 @@ def rethrow_uncaught(func): return func(*args, **kwds) except AttributeError: exc_info = sys.exc_info() - reraise(MultiLevelAttributeError, exc_info[2]) + reraise(UncaughtAttributeError, exc_info[2]) return wrapper From 2b89dda5a67f1bf65f8b5890b2957f2226705c22 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 13 Mar 2013 23:28:53 +0100 Subject: [PATCH 4/6] Use reraise when re-raising MultiLevelStopIteration --- jedi/evaluate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jedi/evaluate.py b/jedi/evaluate.py index 3ad076bc..9c4e30c6 100644 --- a/jedi/evaluate.py +++ b/jedi/evaluate.py @@ -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) From 93bd00bba438142f11120ada9161dff28ed4320f Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 13 Mar 2013 23:49:51 +0100 Subject: [PATCH 5/6] Document rethrow_uncaught/reraise --- jedi/_compatibility.py | 9 +++++++++ jedi/common.py | 24 ++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index 5f03f736..9a704304 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -91,6 +91,15 @@ 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 diff --git a/jedi/common.py b/jedi/common.py index 37d3bb21..62b88220 100644 --- a/jedi/common.py +++ b/jedi/common.py @@ -21,8 +21,9 @@ 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(). """ @@ -36,6 +37,25 @@ class UncaughtAttributeError(Exception): 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: From 7a9e374a656d5fa1a12a8e4ed8195d7361aa0812 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 13 Mar 2013 23:54:19 +0100 Subject: [PATCH 6/6] Use original exception value when reraise --- jedi/common.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/jedi/common.py b/jedi/common.py index 62b88220..d50db623 100644 --- a/jedi/common.py +++ b/jedi/common.py @@ -27,13 +27,6 @@ class UncaughtAttributeError(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): @@ -62,7 +55,7 @@ def rethrow_uncaught(func): return func(*args, **kwds) except AttributeError: exc_info = sys.exc_info() - reraise(UncaughtAttributeError, exc_info[2]) + reraise(UncaughtAttributeError(exc_info[1]), exc_info[2]) return wrapper