# requires jinja2
import copy
import re
import lena.core
import lena.context
# import lena.flow
_sentinel = object()
[документация]class UpdateContext():
"""Update context of passing values."""
def __init__(self, subcontext, update, value=False, default=_sentinel,
skip_on_missing=False, raise_on_missing=False,
recursively=True):
"""*subcontext* is a string representing the part of context
to be updated (for example, *"output.plot"*).
*subcontext* must be non-empty.
*update* will become the value of *subcontext*
during :meth:`__call__`.
It can be one of three different types:
- a simple value (not a string),
- a context formatting string,
- a context value (a string in curly braces).
A context formatting string is any string
with arguments enclosed in double braces
(for example, *"{{variable.type}}_{{variable.name}}"*).
Its argument values will be filled from context
during :meth:`__call__`.
If a formatting argument is missing in context, it will be
substituted with an empty string.
To set *update* to a value from context (not a string),
the keyword argument *value* must be set to ``True``
and the *update* format string must be
a non-empty single expression in double braces
(*"{{variable.compose}}"*).
If *update* corresponds to a context value and
a formatting argument is missing in the context,
:exc:`.LenaKeyError` will be raised unless a *default* is set.
In this case *default* will be used for the update value.
If *update* is a context formatting string, *default*
keyword argument can't be used.
To set a default value other than an empty string,
use a jinja2 filter. For example, if
*update* is *"{{variable.name|default('x')}}"*, then *update*
will be set to "x" both if *context.variable.name* is missing
and if *context.variable* is missing itself.
Other variants to deal with missing context values are:
- to skip update (don't change the context),
set by *skip_on_missing*, or
- to raise :exc:`.LenaKeyError` (set by *raise_on_missing*).
Only one of *default*, *skip_on_missing* or *raise_on_missing*
can be set, otherwise :exc:`.LenaValueError` is raised.
None of these options can be used if *update* is a simple value.
If *recursively* is ``True`` (default), not overwritten
existing values of *subcontext* are preserved.
Otherwise, all existing values of *subcontext* (at its lowest level)
are removed.
See also :func:`.update_recursively`.
Example:
>>> from lena.context import UpdateContext
>>> make_scatter = UpdateContext("output.plot", {"scatter": True})
>>> # call directly
>>> make_scatter(((0, 0), {}))
((0, 0), {'output': {'plot': {'scatter': True}}})
>>> # or use in a sequence
If *subcontext* is not a string, :exc:`.LenaTypeError` is raised.
If it is empty, :exc:`.LenaValueError` is raised.
If *value* is ``True``, braces can be only the first two
and the last two symbols of *update*,
otherwise :exc:`.LenaValueError` is raised.
"""
import jinja2
# The "Context" in the class name means any general context
# (not only :class:`.Context`).
# subcontext is a string, because it must have at most one value
# at each nesting level.
if not isinstance(subcontext, str):
raise lena.core.LenaTypeError(
"subcontext must be a string, {} provided".format(subcontext)
)
if not subcontext:
raise lena.core.LenaValueError(
"subcontext must be non-empty"
)
self._init_subcontext = subcontext
self._subcontext = lena.context.str_to_list(subcontext)
self._has_default = default is not _sentinel
n_of_active = (
int(self._has_default) + int(raise_on_missing)
+ int(skip_on_missing)
)
if n_of_active > 1:
raise lena.core.LenaValueError(
"only one of skip_on_missing, raise_on_missing "
"or default must be active"
)
self._skip_on_missing = skip_on_missing
self._raise_on_missing = raise_on_missing
value = bool(value)
if not isinstance(update, str):
# simple update
self._update = update
if n_of_active:
raise lena.core.LenaValueError(
"for simple update skip_on_missing, default "
"and raise_on_missing must not be set"
)
elif value and re.match('{{[^{}]+}}$', update):
# context value update
# {{at least one symbol in between, no { or } in between}}
self._context_value = True
self._update = update[2:-2]
if not self._has_default and not self._skip_on_missing:
self._raise_on_missing = True
else:
# context format update
if value:
raise lena.core.LenaValueError(
"fix braces for template string '{}' or set value to False"
.format(update)
)
self._context_value = False
if self._has_default:
raise lena.core.LenaValueError(
"default for a formatting string must be set inside it"
)
try:
if raise_on_missing or skip_on_missing:
self._update = jinja2.Template(
update, undefined=jinja2.StrictUndefined
)
else:
# ChainableUndefined appeared in jinja2 2.11.0
if '{' in update:
self._update = jinja2.Template(
update, undefined=jinja2.ChainableUndefined
)
else:
self._update = update
except jinja2.exceptions.TemplateSyntaxError as err:
raise lena.core.LenaValueError(
"template syntax error, {}".format(err.message)
)
self._value = value
self._default = default
self._recursively = bool(recursively)
[документация] def __call__(self, value):
"""Update *value*'s context.
If the *value* is updated,
*subcontext* is always created
(also if the *value* contains no context).
:exc:`.LenaKeyError` is raised if
*raise_on_missing* is ``True`` and
the update argument is missing in *value*'s context.
"""
import jinja2
data, context = value
# data, context = lena.flow.get_data_context(value)
if isinstance(self._update, (str, jinja2.Template)):
if self._context_value:
if not self._has_default:
try:
update = lena.context.get_recursively(context,
self._update)
except lena.core.LenaKeyError as err:
if self._skip_on_missing:
return value
else:
assert self._raise_on_missing
raise err
else:
update = lena.context.get_recursively(
context, self._update, self._default
)
assert not self._raise_on_missing
assert not self._skip_on_missing
update = copy.deepcopy(update)
else:
# context format update
try:
if isinstance(self._update, str):
update = self._update
else:
update = self._update.render(context)
except jinja2.exceptions.UndefinedError as err:
if self._raise_on_missing:
raise lena.core.LenaKeyError(
"{}, context={}".format(err.message, context)
)
else:
# todo: any better way to check it?
assert self._skip_on_missing
return value
else:
update = copy.deepcopy(self._update)
# now empty context is prohibited.
# May be skipped in runtime in the future.
assert self._subcontext
keys = self._subcontext
subdict = context
for key in keys[:-1]:
if key not in subdict or not isinstance(subdict[key], dict):
subdict[key] = {}
subdict = subdict[key]
if self._recursively:
lena.context.update_recursively(subdict, {keys[-1]: update})
else:
subdict[keys[-1]] = update
return (data, context)
def __eq__(self, other):
if not isinstance(other, UpdateContext):
return NotImplemented
if self._has_default != other._has_default:
return False
if self._has_default:
# we don't compare sentinels,
# which hypothetically could be different
if self._default != other._default:
return False
return (
self._init_subcontext == other._init_subcontext and
self._update == other._update and
self._value == other._value and
self._skip_on_missing == other._skip_on_missing and
self._raise_on_missing == other._raise_on_missing and
self._recursively == other._recursively
)
def __repr__(self):
args = ['UpdateContext("{}"'.format(self._init_subcontext)]
if isinstance(self._update, str):
args.append('"{}"'.format(self._update))
else:
args.append(str(self._update))
if self._value:
args += "value={}".format(self._value)
if self._skip_on_missing:
args += "skip_on_missing={}".format(self._skip_on_missing)
if self._raise_on_missing:
args += "raise_on_missing={}".format(self._raise_on_missing)
if not self._recursively:
args += "recursively={}".format(self._recursively)
return ", ".join(args) + ")"