Context

Elements:

Context([d, formatter]) Dictionary with easy-to-read formatting.
UpdateContext(subcontext, update[, value, …]) Update context of passing values.

Functions:

contains(d, s) Check that a dictionary d contains a subdictionary defined by a string s.
difference(d1, d2[, level]) Return a dictionary with items from d1 not contained in d2.
format_context(format_str) Create a function that formats a given string using a context.
get_recursively(d, keys[, default]) Get value from a dictionary d recursively.
intersection(*dicts, **kwargs) Return a dictionary, such that each of its items are contained in all dicts (recursively).
str_to_dict(s[, value]) Create a dictionary from a dot-separated string s.
str_to_list(s) Like str_to_dict(), but return a flat list.
update_nested(key, d, other) Update d[key] with the other dictionary preserving data.
update_recursively(d, other[, value]) Update dictionary d with items from other dictionary.

Elements

class Context(d=None, formatter=None)[source]

Bases: dict

Dictionary with easy-to-read formatting.

Context provides a better representation for context. Example:

>>> from lena.context import Context
>>> c = Context({"1": 1, "2": {"3": 4}})
>>> print(c) # doctest: +NORMALIZE_WHITESPACE
{
    "1": 1,
    "2": {
        "3": 4
    }
}

Initialize from a dictionary d (empty by default).

Representation is defined by the formatter. That must be a callable accepting a dictionary and returning a string. The default is json.dumps.

All public attributes of a Context can be got or set using dot notation (for example, context[“data_path”] is equal to context.data_path). Only one level of nesting is accessible using dot notation.

Tip

JSON and Python representations are different. In particular, JSON True is written as lowercase true. To convert JSON back to Python, use json.loads(string).

If formatter is given but is not callable, LenaTypeError is raised. If the attribute to be got is missing, LenaAttributeError is raised. An attempt to get a private attribute raises AttributeError.

__call__(value)[source]

Convert value’s context to Context on the fly.

If the value is a (data, context) pair, convert its context part to Context. If the value doesn’t contain a context, it is created as an empty Context.

When a Context is used as a sequence element, its initialization argument d has no effect on the produced values.

class UpdateContext(subcontext, update, value=False, default=<object object>, skip_on_missing=False, raise_on_missing=False, recursively=True)[source]

Update context of passing values.

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 __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 __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, 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 LenaKeyError (set by raise_on_missing).

Only one of default, skip_on_missing or raise_on_missing can be set, otherwise 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 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, LenaTypeError is raised. If it is empty, LenaValueError is raised. If value is True, braces can be only the first two and the last two symbols of update, otherwise LenaValueError is raised.

__call__(value)[source]

Update value’s context.

If the value is updated, subcontext is always created (also if the value contains no context).

LenaKeyError is raised if raise_on_missing is True and the update argument is missing in value’s context.

Functions

contains(d, s)[source]

Check that a dictionary d contains a subdictionary defined by a string s.

True if d contains a subdictionary that is represented by s. Dots in s mean nested subdictionaries. A string without dots means a key in d.

Example:

>>> d = {'fit': {'coordinate': 'x'}}
>>> contains(d, "fit")
True
>>> contains(d, "fit.coordinate.x")
True
>>> contains(d, "fit.coordinate.y")
False

If the most nested element of d to be compared with s is not a string, its string representation is used for comparison. See also str_to_dict().

difference(d1, d2, level=-1)[source]

Return a dictionary with items from d1 not contained in d2.

level sets the maximum depth of recursion. For infinite recursion, set that to -1. For level 1, if a key is present both in d1 and d2 but has different values, it is included into the difference. See intersection() for more details.

d1 and d2 remain unchanged. However, d1 or some of its subdictionaries may be returned directly. Make a deep copy of the result when appropriate.

New in version 0.5: add keyword argument level.

format_context(format_str)[source]

Create a function that formats a given string using a context.

It is recommended to use jinja2.Template. Use this function only if you don’t have jinja2.

format_str is a Python format string with double braces instead of single ones. It must contain all non-empty replacement fields, and only simplest formatting without attribute lookup. Example:

>>> f = format_context("{{x}}")
>>> f({"x": 10})
'10'

When calling format_context, arguments are bound and a new function is returned. When called with a context, its keys are extracted and formatted in format_str.

Keys can be nested using a dot, for example:

>>> f = format_context("{{x.y}}_{{z}}")
>>> f({"x": {"y": 10}, "z": 1})
'10_1'

This function does not work with unbalanced braces. If a simple check fails, LenaValueError is raised. If format_str is not a string, LenaTypeError is raised. All other errors are raised only during formatting. If context doesn’t contain the needed key, LenaKeyError is raised. Note that string formatting can also raise a ValueError, so it is recommended to test your formatters before using them.

get_recursively(d, keys, default=<object object>)[source]

Get value from a dictionary d recursively.

keys can be a list of simple keys (strings), a dot-separated string or a dictionary with at most one key at each level. A string is split by dots and used as a list. A list of keys is searched in the dictionary recursively (it represents nested dictionaries). If any of them is not found, default is returned if “default” is given, otherwise LenaKeyError is raised.

If keys is empty, d is returned.

Examples:

>>> context = {"output": {"latex": {"name": "x"}}}
>>> get_recursively(context, ["output", "latex", "name"], default="y")
'x'
>>> get_recursively(context, "output.latex.name")
'x'

Note

Python’s dict.get in case of a missing value returns None and never raises an error. We implement it differently, because it allows more flexibility.

If d is not a dictionary or if keys is not a string, a dict or a list, LenaTypeError is raised. If keys is a dictionary with more than one key at some level, LenaValueError is raised.

intersection(*dicts, **kwargs)[source]

Return a dictionary, such that each of its items are contained in all dicts (recursively).

dicts are several dictionaries. If dicts is empty, an empty dictionary is returned.

A keyword argument level sets maximum number of recursions. For example, if level is 0, all dicts must be equal (otherwise an empty dict is returned). If level is 1, the result contains those subdictionaries which are equal. For arbitrarily nested subdictionaries set level to -1 (default).

Example:

>>> from lena.context import intersection
>>> d1 = {1: "1", 2: {3: "3", 4: "4"}}
>>> d2 = {2: {4: "4"}}
>>> # by default level is -1, which means infinite recursion
>>> intersection(d1, d2) == d2
True
>>> intersection(d1, d2, level=0)
{}
>>> intersection(d1, d2, level=1)
{}
>>> intersection(d1, d2, level=2)
{2: {4: '4'}}

This function always returns a dictionary or its subtype (copied from dicts[0]). All values are deeply copied. No dictionary or subdictionary is changed.

If any of dicts is not a dictionary or if some kwargs are unknown, LenaTypeError is raised.

str_to_dict(s, value=<object object>)[source]

Create a dictionary from a dot-separated string s.

If the value is provided, it becomes the value of the deepest key represented by s.

Dots represent nested dictionaries. If s is non-empty and value is not provided, then s must have at least two dot-separated parts (“a.b”), otherwise LenaValueError is raised. If a value is provided, s must be non-empty.

If s is empty, an empty dictionary is returned.

Examples:

>>> str_to_dict("a.b.c d")
{'a': {'b': 'c d'}}
>>> str_to_dict("output.changed", True)
{'output': {'changed': True}}
str_to_list(s)[source]

Like str_to_dict(), but return a flat list.

If the string s is empty, an empty list is returned. This is different from str.split: the latter would return a list with one empty string. Contrarily to str_to_dict(), this function allows an arbitrary number of dots in s (or none).

update_nested(key, d, other)[source]

Update d[key] with the other dictionary preserving data.

If d doesn’t contain the key, it is updated with {key: other}. If d contains the key, d[key] is inserted into other[key] (so that it is not overriden). If other contains key (and possibly more nested key-s), then d[key] is inserted into the deepest level of other.key.key… Finally, d[key] becomes other.

Example:

>>> context = {"variable": {"name": "x"}}
>>> new_var_context = {"name": "n"}
>>> update_nested("variable", context, copy.deepcopy(new_var_context))
>>> context == {'variable': {'name': 'n', 'variable': {'name': 'x'}}}
True
>>>
>>> update_nested("variable", context, {"name": "top"})
>>> context == {
...    'variable': {'name': 'top',
...                 'variable': {'name': 'n', 'variable': {'name': 'x'}}}
... }
True

other is modified in general. Create that on the fly or use copy.deepcopy when appropriate.

Recursive dictionaries (containing references to themselves) are strongly discouraged and meaningless when nesting. If other[key] is recursive, LenaValueError may be raised.

update_recursively(d, other, value=<object object>)[source]

Update dictionary d with items from other dictionary.

other can be a dot-separated string. In this case str_to_dict() is used to convert it and the value to a dictionary. A value argument is allowed only when other is a string, otherwise LenaValueError is raised.

Existing values are updated recursively, that is including nested subdictionaries. Example:

>>> d1 = {"a": 1, "b": {"c": 3}}
>>> d2 = {"b": {"d": 4}}
>>> update_recursively(d1, d2)
>>> d1 == {'a': 1, 'b': {'c': 3, 'd': 4}}
True
>>> # Usual update would have made d1["b"] = {"d": 4}, erasing "c".

Non-dictionary items from other overwrite those in d:

>>> update_recursively(d1, {"b": 2})
>>> d1 == {'a': 1, 'b': 2}
True