blob: 9fabbf6e81fd1f8df85571dce875df206eb6ab1c [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2019 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Random utilties from Python3's contextlib."""
from __future__ import division
from __future__ import print_function
import sys
class ExitStack(object):
"""https://docs.python.org/3/library/contextlib.html#contextlib.ExitStack"""
def __init__(self):
self._stack = []
self._is_entered = False
def _assert_is_entered(self):
# Strictly, entering has no effect on the operations that call this.
# However, if you're trying to e.g. push things to an ExitStack that hasn't
# yet been entered, that's likely a bug.
assert self._is_entered, 'ExitStack op performed before entering'
def __enter__(self):
self._is_entered = True
return self
def _perform_exit(self, exc_type, exc, exc_traceback):
# I suppose a better name for this is
# `take_exception_handling_into_our_own_hands`, but that's harder to type.
exception_handled = False
while self._stack:
fn = self._stack.pop()
# The except clause below is meant to run as-if it's a `finally` block,
# but `finally` blocks don't have easy access to exceptions currently in
# flight. Hence, we do need to catch things like KeyboardInterrupt,
# SystemExit, ...
# pylint: disable=bare-except
try:
# If an __exit__ handler returns a truthy value, we should assume that
# it handled the exception appropriately. Otherwise, we need to keep it
# with us. (PEP 343)
if fn(exc_type, exc, exc_traceback):
exc_type, exc, exc_traceback = None, None, None
exception_handled = True
except:
# Python2 doesn't appear to have the notion of 'exception causes',
# which is super unfortunate. In the case:
#
# @contextlib.contextmanager
# def foo()
# try:
# yield
# finally:
# raise ValueError
#
# with foo():
# assert False
#
# ...Python will only note the ValueError; nothing about the failing
# assertion is printed.
#
# I guess on the bright side, that means we don't have to fiddle with
# __cause__s/etc.
exc_type, exc, exc_traceback = sys.exc_info()
exception_handled = True
if not exception_handled:
return False
# Something changed. We either need to raise for ourselves, or note that
# the exception has been suppressed.
if exc_type is not None:
raise exc_type, exc, exc_traceback
# Otherwise, the exception was suppressed. Go us!
return True
def __exit__(self, exc_type, exc, exc_traceback):
return self._perform_exit(exc_type, exc, exc_traceback)
def close(self):
"""Unwinds the exit stack, unregistering all events"""
self._perform_exit(None, None, None)
def enter_context(self, cm):
"""Enters the given context manager, and registers it to be exited."""
self._assert_is_entered()
# The spec specifically notes that we should take __exit__ prior to calling
# __enter__.
exit_cleanup = cm.__exit__
result = cm.__enter__()
self._stack.append(exit_cleanup)
return result
# pylint complains about `exit` being redefined. `exit` is the documented
# name of this param, and renaming it would break portability if someone
# decided to `push(exit=foo)`, so just ignore the lint.
# pylint: disable=redefined-builtin
def push(self, exit):
"""Like `enter_context`, but won't enter the value given."""
self._assert_is_entered()
self._stack.append(exit.__exit__)
def callback(self, callback, *args, **kwargs):
"""Performs the given callback on exit"""
self._assert_is_entered()
def fn(_exc_type, _exc, _exc_traceback):
callback(*args, **kwargs)
self._stack.append(fn)