LayeredContext

class privex.helpers.common.LayeredContext(wrapped_class: K, max_layers: Optional[int] = 1, fail: bool = False)[source]

A wrapper class for context manager classes / functions which allows you to control how many with layers that a context manager can have - and allow for the previous layer’s context manager __enter__ / yield result to be passed down when max_layers is hit.

(context managers are classes/functions with the methods __enter__ / __exit__ / __aenter__ / __aexit__ etc.)

Works with context manager classes, asyncio context manager classes, and contextlib.contextmanager() functions.

By default, LayeredContext sets max_layers to 1, meaning after 1 layer of with or async with statements, all additional layers will simply get given the same context result as the 1st layer, plus both __enter__ and __exit__ will only be called once (at the start and end of the first layer).

Using with class-based context managers:

>>> class Hello:
...     def __enter__(self):
...         print('entering Hello')
...         return self
...     def __exit__(self, exc_type, exc_val, exc_tb):
...         print('exiting Hello')
>>> ctx_a = LayeredContext(Hello())
>>> with ctx_a as a:
...     print('class manager layer 1')
...     with ctx_a as b:
...         print('class manager layer 2')
...     print('back to class layer 1')
entering Hello
class manager layer 1
class manager layer 2
back to class layer 1
exiting Hello

We can see that entering Hello and exiting Hello were only outputted at the end of the first context block with ctx_a as a, showing that Hello was only entered/exited as a context manager for the first with block.

Using with function-based :func:`contextlib.contextmanager` context managers:

>>> from contextlib import contextmanager
>>> @contextmanager
>>> def lorem():
...     print('entering lorem contextmanager')
...     yield 'hello world'
...     print('exiting lorem contextmanager')
>>> ctx_b = LayeredContext(lorem())
>>> with ctx_b as c:
...     print('function manager layer 1 - context is:', c)
...     with ctx_b as d:
...         print('function manager layer 2 - context is:', d)
...     print('back to function layer 1')
entering lorem contextmanager
function manager layer 1 - context is: hello world
function manager layer 2 - context is: hello world
back to function layer 1
exiting lorem contextmanager

We can see the default max_layers of 1 was respected, as the 2nd layer with ctx_b as d only printed function manager layer 2 (thus lorem’s enter/exit methods were not called), and it shows the context is still hello world (the context yielded by lorem in layer 1).

Example usage

First we need an example class which can be used as a context manager, so we create Example with a very simple __enter__ and __exit__ method, which simply adds and subtracts from self.ctx_layer respectively:

>>> class Example:
...     def __init__(self):
...         self.ctx_layer = 0
...     def __enter__(self):
...         self.ctx_layer += 1
...         return self
...     def __exit__(self, exc_type, exc_val, exc_tb):
...         if self.ctx_layer <= 0: raise ValueError('ctx_layer <= 0 !!!')
...         self.ctx_layer -= 1
...         return None

If we then create an instance of Example, and use it as a context manager in a 2 layer nested with exp, we can see ctx_layer gets increased each time we use it as a context manager, and decreases after the context manager block:

>>> exp = Example()
>>> with exp as x:
...     print(x.ctx_layer)       # prints: 1
...     with exp as y:
...         print(y.ctx_layer)   # prints: 2
...     print(x.ctx_layer)       # prints: 1
>>> exp.ctx_layer
0

Now, lets wrap it with LayeredContext, and set the maximum amount of layers to 1. If we start using ctx as a context manager, it works as if we used the example instance exp as a context manager. But, unlike the real instance, __enter__ is only really called for the first with block, and __exit__ is only really called once we finish the first layer with ctx as x

>>> ctx = LayeredContext(exp, max_layers=1)
>>> with ctx as x:
...     print(x.ctx_layer)             # prints: 1
...     with ctx as y:
...         print(y.ctx_layer)         # prints: 1
...         print(ctx.virtual_layer)   # prints: 2
...     print(x.ctx_layer)         # prints: 1
...     print(ctx.virtual_layer)   # prints: 1
>>> exp.ctx_layer
0
>>> print(ctx.layer, ctx.virtual_layer)
0 0
__init__(wrapped_class: K, max_layers: Optional[int] = 1, fail: bool = False)[source]

Construct a LayeredContext instance, wrapping the context manager class instance or func:contextlib.contextmanager manager function wrapped_class.

Parameters
  • wrapped_class (K|object) – A context manager class or contextlib.contextmanager() manager function to wrap

  • max_layers (int) – Maximum layers of (async) with blocks before silently consuming further attempts to enter/exit the context manager for wrapped_class

  • fail (bool) – (default: False) When True, will raise NestedContextException when an enter() call is going to cause more than max_layers context manager layers to be active.

Methods

Methods

__init__(wrapped_class[, max_layers, fail])

Construct a LayeredContext instance, wrapping the context manager class instance or func:contextlib.contextmanager manager function wrapped_class.

aenter()

aexit([exc_type, exc_val, exc_tb])

enter()

exit([exc_type, exc_val, exc_tb])

Attributes

Attributes

class_name