callable_util.py 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. # Copyright 2015 gRPC authors.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Utilities for working with callables."""
  15. from abc import ABC
  16. import collections
  17. import enum
  18. import functools
  19. import logging
  20. _LOGGER = logging.getLogger(__name__)
  21. class Outcome(ABC):
  22. """A sum type describing the outcome of some call.
  23. Attributes:
  24. kind: One of Kind.RETURNED or Kind.RAISED respectively indicating that the
  25. call returned a value or raised an exception.
  26. return_value: The value returned by the call. Must be present if kind is
  27. Kind.RETURNED.
  28. exception: The exception raised by the call. Must be present if kind is
  29. Kind.RAISED.
  30. """
  31. @enum.unique
  32. class Kind(enum.Enum):
  33. """Identifies the general kind of the outcome of some call."""
  34. RETURNED = object()
  35. RAISED = object()
  36. class _EasyOutcome(
  37. collections.namedtuple('_EasyOutcome',
  38. ['kind', 'return_value', 'exception']), Outcome):
  39. """A trivial implementation of Outcome."""
  40. def _call_logging_exceptions(behavior, message, *args, **kwargs):
  41. try:
  42. return _EasyOutcome(Outcome.Kind.RETURNED, behavior(*args, **kwargs),
  43. None)
  44. except Exception as e: # pylint: disable=broad-except
  45. _LOGGER.exception(message)
  46. return _EasyOutcome(Outcome.Kind.RAISED, None, e)
  47. def with_exceptions_logged(behavior, message):
  48. """Wraps a callable in a try-except that logs any exceptions it raises.
  49. Args:
  50. behavior: Any callable.
  51. message: A string to log if the behavior raises an exception.
  52. Returns:
  53. A callable that when executed invokes the given behavior. The returned
  54. callable takes the same arguments as the given behavior but returns a
  55. future.Outcome describing whether the given behavior returned a value or
  56. raised an exception.
  57. """
  58. @functools.wraps(behavior)
  59. def wrapped_behavior(*args, **kwargs):
  60. return _call_logging_exceptions(behavior, message, *args, **kwargs)
  61. return wrapped_behavior
  62. def call_logging_exceptions(behavior, message, *args, **kwargs):
  63. """Calls a behavior in a try-except that logs any exceptions it raises.
  64. Args:
  65. behavior: Any callable.
  66. message: A string to log if the behavior raises an exception.
  67. *args: Positional arguments to pass to the given behavior.
  68. **kwargs: Keyword arguments to pass to the given behavior.
  69. Returns:
  70. An Outcome describing whether the given behavior returned a value or raised
  71. an exception.
  72. """
  73. return _call_logging_exceptions(behavior, message, *args, **kwargs)