objects.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. # -*- coding: utf-8 -*-
  2. """Object related utilities, including introspection, etc."""
  3. from __future__ import absolute_import, unicode_literals
  4. __all__ = ['Bunch', 'FallbackContext', 'mro_lookup']
  5. class Bunch(object):
  6. """Object that enables you to modify attributes."""
  7. def __init__(self, **kwargs):
  8. self.__dict__.update(kwargs)
  9. def mro_lookup(cls, attr, stop=set(), monkey_patched=[]):
  10. """Return the first node by MRO order that defines an attribute.
  11. Arguments:
  12. cls (Any): Child class to traverse.
  13. attr (str): Name of attribute to find.
  14. stop (Set[Any]): A set of types that if reached will stop
  15. the search.
  16. monkey_patched (Sequence): Use one of the stop classes
  17. if the attributes module origin isn't in this list.
  18. Used to detect monkey patched attributes.
  19. Returns:
  20. Any: The attribute value, or :const:`None` if not found.
  21. """
  22. for node in cls.mro():
  23. if node in stop:
  24. try:
  25. value = node.__dict__[attr]
  26. module_origin = value.__module__
  27. except (AttributeError, KeyError):
  28. pass
  29. else:
  30. if module_origin not in monkey_patched:
  31. return node
  32. return
  33. if attr in node.__dict__:
  34. return node
  35. class FallbackContext(object):
  36. """Context workaround.
  37. The built-in ``@contextmanager`` utility does not work well
  38. when wrapping other contexts, as the traceback is wrong when
  39. the wrapped context raises.
  40. This solves this problem and can be used instead of ``@contextmanager``
  41. in this example::
  42. @contextmanager
  43. def connection_or_default_connection(connection=None):
  44. if connection:
  45. # user already has a connection, shouldn't close
  46. # after use
  47. yield connection
  48. else:
  49. # must've new connection, and also close the connection
  50. # after the block returns
  51. with create_new_connection() as connection:
  52. yield connection
  53. This wrapper can be used instead for the above like this::
  54. def connection_or_default_connection(connection=None):
  55. return FallbackContext(connection, create_new_connection)
  56. """
  57. def __init__(self, provided, fallback, *fb_args, **fb_kwargs):
  58. self.provided = provided
  59. self.fallback = fallback
  60. self.fb_args = fb_args
  61. self.fb_kwargs = fb_kwargs
  62. self._context = None
  63. def __enter__(self):
  64. if self.provided is not None:
  65. return self.provided
  66. context = self._context = self.fallback(
  67. *self.fb_args, **self.fb_kwargs
  68. ).__enter__()
  69. return context
  70. def __exit__(self, *exc_info):
  71. if self._context is not None:
  72. return self._context.__exit__(*exc_info)