Index: recipe_engine/util.py |
diff --git a/recipe_engine/util.py b/recipe_engine/util.py |
index 91c5192c8c7ccad7d5da3e1b04b327b3a796e8df..7fe739ee862c6d66a3ebfd833dd5e05049ddae29 100644 |
--- a/recipe_engine/util.py |
+++ b/recipe_engine/util.py |
@@ -140,7 +140,8 @@ BUG_LINK = ( |
'https://code.google.com/p/chromium/issues/entry?%s' % urllib.urlencode({ |
'summary': 'Recipe engine bug: unexpected failure', |
'comment': 'Link to the failing build and paste the exception here', |
- 'labels': 'Infra,Infra-Area-Recipes,Pri-1,Restrict-View-Google,Infra-Troopers', |
+ 'labels': 'Infra,Infra-Area-Recipes,Pri-1,Restrict-View-Google,' |
+ 'Infra-Troopers', |
'cc': 'martiniss@chromium.org,iannucci@chromium.org', |
})) |
@@ -228,3 +229,91 @@ class exponential_retry(object): |
time.sleep(retry_delay.total_seconds()) |
retry_delay *= 2 |
return wrapper |
+ |
+ |
+class MultiException(Exception): |
+ """An exception that aggregates multiple exceptions and summarizes them.""" |
+ |
+ class Builder(object): |
+ """Iteratively constructs a MultiException.""" |
+ |
+ def __init__(self): |
+ self._exceptions = [] |
+ |
+ def append(self, exc): |
+ if exc is not None: |
+ self._exceptions.append(exc) |
+ |
+ def get(self): |
+ """Returns (MultiException or None): The constructed MultiException. |
+ |
+ If no exceptions have been appended, None will be returned. |
+ """ |
+ return MultiException(*self._exceptions) if self._exceptions else (None) |
+ |
+ def raise_if_any(self): |
+ mexc = self.get() |
+ if mexc is not None: |
+ raise mexc |
+ |
+ @contextlib.contextmanager |
+ def catch(self, *exc_types): |
+ """ContextManager that catches any exception raised during its execution |
+ and adds them to the MultiException. |
+ |
+ Args: |
+ exc_types (list): A list of exception classes to catch. If empty, |
+ Exception will be used. |
+ """ |
+ exc_types = exc_types or (Exception,) |
+ try: |
+ yield |
+ except exc_types as exc: |
+ self.append(exc) |
+ |
+ |
+ def __init__(self, *base): |
+ super(MultiException, self).__init__() |
+ |
+ # Determine base Exception text. |
+ if len(base) == 0: |
+ self.message = 'No exceptions' |
+ elif len(base) == 1: |
+ self.message = str(base[0]) |
+ else: |
+ self.message = str(base[0]) + ', and %d more...' % (len(base)-1) |
+ self._inner = base |
+ |
+ def __nonzero__(self): |
+ return bool(self._inner) |
+ |
+ def __len__(self): |
+ return len(self._inner) |
+ |
+ def __getitem__(self, key): |
+ return self._inner[key] |
+ |
+ def __iter__(self): |
+ return iter(self._inner) |
+ |
+ def __str__(self): |
+ return '%s(%s)' % (type(self).__name__, self.message) |
+ |
+ |
+@contextlib.contextmanager |
+def map_defer_exceptions(fn, it, *exc_types): |
+ """Executes "fn" for each element in "it". Any exceptions thrown by "fn" will |
+ be deferred until the end of "it", then raised as a single MultiException. |
+ |
+ Args: |
+ fn (callable): A function to call for each element in "it". |
+ it (iterable): An iterable to traverse. |
+ exc_types (list): An optional list of specific exception types to defer. |
+ If empty, Exception will be used. Any Exceptions not referenced by this |
+ list will skip deferring and be immediately raised. |
+ """ |
+ mexc_builder = MultiException.Builder() |
+ for e in it: |
+ with mexc_builder.catch(*exc_types): |
+ fn(e) |
+ mexc_builder.raise_if_any() |