Skip to content

Commit

Permalink
Refactor UpdateContext. Add skip_on_missing, raise_on_missing init kw…
Browse files Browse the repository at this point in the history
…args. Jinja2 requirement now is newer than 2.11.0 (Jan 29, 2020).
  • Loading branch information
ynikitenko committed Apr 25, 2020
1 parent 9afb178 commit af029dc
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 57 deletions.
186 changes: 147 additions & 39 deletions lena/context/update_context.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import print_function

import copy
import re

import jinja2
import lena.core
import lena.context
import lena.flow
Expand All @@ -13,20 +15,57 @@
class UpdateContext():
"""Update context of passing values."""

def __init__(self, subcontext, update, default=_sentinel, recursively=True):
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*).
to be updated (for example, *"output.plot"*).
*subcontext* must be non-empty.
*update* will become the value of *subcontext*.
If it is a string enclosed in braces,
*update* will get its value from context during :meth:`__call__`
(example: "{variable.name}").
If the string is missing in the context, :exc:`.LenaKeyError`
will be raised unless a *default* is set.
In this case *default* will be used for update.
If *update* is a string containing no curly braces,
it will be used for update itself.
*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.
Expand All @@ -38,16 +77,20 @@ def __init__(self, subcontext, update, default=_sentinel, recursively=True):
>>> from lena.context import UpdateContext
>>> make_scatter = UpdateContext("output.plot", {"scatter": True})
>>> # use it in a sequence
The "Context" in the class name means any general context
(not only :class:`.Context`).
>>> # 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.
Braces can be only the first and the last symbols
of *update*, otherwise :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.
"""
# 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):
Expand All @@ -59,45 +102,110 @@ def __init__(self, subcontext, update, default=_sentinel, recursively=True):
"subcontext must be non-empty"
)
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
elif '{' in update or '}' in update:
braceless_str = update[1:-1]
if (update[0] != '{' or update[-1] != '}'
or '{' in braceless_str or '}' in braceless_str):
if n_of_active:
raise lena.core.LenaValueError(
"update can contain braces only as its first "
"and last symbols, {} provided".format(update)
"for simple update skip_on_missing, default "
"and raise_on_missing must not be set"
)
self._get_value = True
self._update = braceless_str
self._default = default
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:
self._get_value = False
self._update = update
# 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
self._update = jinja2.Template(
update, undefined=jinja2.ChainableUndefined
)
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 context of the *value*.
"""Update *value*'s context.
If the *value* is updated,
*subcontext* is always created
(also if the *value* contains no context).
If *value*'s context doesn't contain *subcontext*, it is created.
If the *value* contains no context, it is also created.
:exc:`.LenaKeyError` is raised if
*raise_on_missing* is ``True`` and
the update argument is missing in *value*'s context.
"""
data, context = lena.flow.get_data_context(value)
if isinstance(self._update, str):
if self._get_value:
# formatting braces are present
if self._default is _sentinel:
# no default is provided
update = lena.context.get_recursively(context,
self._update)
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:
update = self._update
# context format update
try:
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.
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
### Usage ###
# LaTeX (and other) templates
jinja2
jinja2 >= 2.11.0 # >=2.11.0 needed for UpdateContext (ChainableUndefined)
# numpy is optional, install if you want.
# numpy
#
Expand Down

0 comments on commit af029dc

Please sign in to comment.