
On 23 June 2017 at 09:29, Cameron Simpson <cs@zip.com.au> wrote:
This is so common that I actually keep around a special hack:
def prop(func): ''' The builtin @property decorator lets internal AttributeErrors escape. While that can support properties that appear to exist conditionally, in practice this is almost never what I want, and it masks deeper errors. Hence this wrapper for @property that transmutes internal AttributeErrors into RuntimeErrors. ''' def wrapper(*a, **kw): try: return func(*a, **kw) except AttributeError as e: e2 = RuntimeError("inner function %s raised %s" % (func, e)) if sys.version_info[0] >= 3: try: eval('raise e2 from e', globals(), locals()) except: # FIXME: why does this raise a SyntaxError? raise e else: raise e2 return property(wrapper)
Slight tangent, but I do sometimes wonder if adding a decorator factory like the following to functools might be useful: def raise_when_returned(return_exc): def decorator(f): @wraps(f) def wrapper(*args, **kwds): try: result = f(*args, **kwds) except selective_exc as unexpected_exc: msg = "inner function {} raised {}".format(f, unexpected_exc) raise RuntimeError(msg) from unexpected_exc if isinstance(result, return_exc): raise result return result It's essentially a generalisation of PEP 479 to arbitrary exception types, since it lets you mark a particular exception type as being communicated back to the wrapper via the return channel rather than as a regular exception: def with_traceback(exc): try: raise exc except BaseException as caught_exc: return caught_exc @property @raise_when_returned(AttributeError) def target(self): if len(self.targets) == 1: return self.targets[0] return with_traceback(AttributeError('only exists when this has exactly one target')) The part I don't like about that approach is the fact that you need to mess about with the exception internals to get a halfway decent traceback on the AttributeError. The main alternative would be to add a "convert_exception" context manager in contextlib, so you could write the example property as: @property def target(self): with convert_exception(AttributeError): if len(self.targets) == 1: return self.targets[0] raise AttributeError('only exists when this has exactly one target') Where "convert_exception" would be something like: def convert_exception(exc_type): """Prevents the given exception type from escaping a region of code by converting it to RuntimeError""" if not issubclass(exc_type, Exception): raise TypeError("Only Exception subclasses can be flagged as unexpected") try: yield except exc_type as unexpected_exc: new_exc = RuntimeError("Unexpected exception") raise new_exc from unexpected_exc The API for this could potentially be made more flexible to allow easy substition of lookup errors with attribute errors and vice-versa (e.g. via additional keyword-only parameters) To bring the tangent back closer to Sven's original point, there are probably also some parts of the import system (such as executing the body of a found module) where the case can be made that we should be converting ImportError to RuntimeError, rather than letting the ImportError escape (with essentially the same rationale as PEP 479). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia