"""Functions to work with context (dictionary)."""
from __future__ import print_function
import copy
import lena.core
# pylint: disable=invalid-name
# d is a good name for dictionary,
# used in Python documentation for dict.
[документация]def contains(d, s):
"""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 :func:`str_to_dict`.
"""
# todo: s can be a list, or a dict?
levels = s.split(".")
if len(levels) < 2:
return s in d
subdict = d
for key in levels[:-1]:
if key not in subdict:
return False
subdict = subdict[key]
last_val = levels[-1]
if isinstance(subdict, dict):
return last_val in subdict
else:
# just a value
try:
# it's better to test for an object to be cast to str
# than to disallow "dim.1"
subd = str(subdict)
except Exception:
return False
else:
return subd == last_val
[документация]def difference(d1, d2):
"""Return a dictionary with items from *d1* not contained in *d2*.
If a key is present both in *d1* and *d2* but has different values,
it is included into the difference.
"""
result = {}
for key in d1:
if key not in d2 or d1[key] != d2[key]:
result[key] = d1[key]
return result
[документация]def format_context(format_str):
"""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, :exc:`.LenaValueError` is raised.
If *format_str* is not a string, :exc:`.LenaTypeError` is raised.
All other errors are raised only during formatting.
If context doesn't contain the needed key,
:exc:`.LenaKeyError` is raised.
Note that string formatting can also raise a :exc:`ValueError`,
so it is recommended to test your formatters before using them.
"""
if not isinstance(format_str, str):
raise lena.core.LenaTypeError(
"format_str must be a string, {} given".format(format_str)
)
# prohibit single or unbalanced braces
if format_str.count('{') != format_str.count('}'):
raise lena.core.LenaValueError("unbalanced braces in '{}'".format(format_str))
if '{' in format_str and not '{{' in format_str:
raise lena.core.LenaValueError(
"double braces must be used for formatting instead of '{}'"
.format(format_str)
)
# new format: now double braces instead of single ones.
# but the algorithm may be left unchanged.
format_str = format_str.replace("{{", "{").replace("}}", "}")
new_str = []
new_args = []
prev_char = ''
ind = 0
within_field = False
while ind < len(format_str):
c = format_str[ind]
if c != '{' and not within_field:
prev_char = c
new_str.append(c)
ind += 1
continue
while c == '{' and ind < len(format_str):
new_str.append(c)
# literal formatting { are not allowed
# if prev_char == '{':
# prev_char = ''
# within_field = False
# else:
prev_char = c
within_field = True
ind += 1
c = format_str[ind]
if within_field:
new_arg = []
while ind < len(format_str):
if c in '}!:':
prev_char = c
within_field = False
new_args.append(''.join(new_arg))
break
new_arg.append(c)
ind += 1
c = format_str[ind]
format_str = ''.join(new_str)
args = new_args
def _format_context(context):
new_args = []
for arg in args:
# LenaKeyError may be raised
new_args.append(lena.context.get_recursively(context, arg))
# other exceptions, like ValueError
# (for bad string formatting) may be raised.
s = format_str.format(*new_args)
return s
return _format_context
_sentinel = object()
[документация]def get_recursively(d, keys, default=_sentinel):
"""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 :exc:`.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, :exc:`.LenaTypeError` is raised.
If *keys* is a dictionary with more than one key at some level,
:exc:`.LenaValueError` is raised.
"""
has_default = default is not _sentinel
if not isinstance(d, dict):
raise lena.core.LenaTypeError(
"need a dictionary, {} provided".format(d)
)
if isinstance(keys, str):
# here empty substrings are skipped, but this is undefined.
keys = [key for key in keys.split('.') if key]
# todo: create dict_to_list and disable dict keys here?
elif isinstance(keys, dict):
new_keys = []
while keys:
if isinstance(keys, dict) and len(keys) != 1:
raise lena.core.LenaValueError(
"keys must have exactly one key at each level, "
"{} given".format(keys)
)
else:
if not isinstance(keys, dict):
new_keys.append(keys)
break
for key in keys:
new_keys.append(key)
keys = keys[key]
break
keys = new_keys
elif isinstance(keys, list):
if not all(isinstance(k, str) for k in keys):
raise lena.core.LenaTypeError(
"all simple keys must be strings, "
"{} given".format(keys)
)
else:
raise lena.core.LenaTypeError(
"keys must be a dict, a string or a list of keys, "
"{} given".format(keys)
)
for key in keys[:-1]:
if key in d and isinstance(d.get(key), dict):
d = d[key]
elif has_default:
return default
else:
raise lena.core.LenaKeyError(
"nested dict {} not found in {}".format(key, d)
)
if not keys:
return d
if keys[-1] in d:
return d[keys[-1]]
elif has_default:
return default
else:
raise lena.core.LenaKeyError(
"nested key {} not found in {}".format(keys[-1], d)
)
[документация]def intersection(*dicts, **kwargs):
"""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,
:exc:`.LenaTypeError` is raised.
"""
if not all([isinstance(d, dict) for d in dicts]):
raise lena.core.LenaTypeError(
"all dicts must be dictionaries, "
"{} given".format(dicts)
)
level = kwargs.pop("level", -1)
if kwargs:
raise lena.core.LenaTypeError(
"unknown kwargs {}".format(kwargs)
)
if not dicts:
return {}
res = copy.deepcopy(dicts[0])
for d in dicts[1:]:
if level == 0:
if d == res and d:
continue
else:
return {}
to_delete = []
for key in res:
if key in d:
if d[key] != res[key]:
if level == 1:
to_delete.append(key)
elif isinstance(res[key], dict) and isinstance(d[key], dict):
res[key] = intersection(res[key], d[key], level=level-1)
else:
to_delete.append(key)
else:
# keys can't be deleted during iteration
to_delete.append(key)
for key in to_delete:
del res[key]
if not res:
# res was calculated empty
return res
return res
def iterate_update(d, updates):
"""Iterate on updates of *d* with *updates*.
*d* is a dictionary. It remains unchanged.
*updates* is a list of dictionaries.
For each element *update*
a copy of *d* updated with *update* is yielded.
If *updates* is empty, nothing is yielded.
"""
# todo: do I need this function?
for update in updates:
d_copy = copy.deepcopy(d)
update_recursively(d_copy, update)
yield d_copy
def make_context(obj, *attrs):
"""Return context for object *obj*.
*attrs* is a list of attributes of *obj* to be inserted
into the context.
If an attribute starts with an underscore '_',
it is inserted without the underscore.
If an attribute is absent or None, it is skipped.
"""
# todo: rename to to_dict
# not used anywhere, change it freely.
# add examples.
context = {}
for attr in attrs:
val = getattr(obj, attr, None)
if val is not None:
if attr.startswith("_"):
attr = attr[1:]
context.update({attr: val})
return context
[документация]def str_to_dict(s):
"""Create a dictionary from a dot-separated string *s*.
Dots represent nested dictionaries.
*s*, if not empty, must have at least two dot-separated parts
(*a.b*), otherwise :exc:`.LenaValueError` is raised.
If *s* is empty, an empty dictionary is returned.
*s* can be a dictionary. In this case it is returned as it is.
Example:
>>> str_to_dict("a.b.c d")
{'a': {'b': 'c d'}}
"""
# todo: add a parameter to recover ints from ints?
if s == "":
return {}
elif isinstance(s, dict):
return s
parts = s.split(".")
d = {}
def nest_list(d, l):
"""Convert list *l* to nested dictionaries in *d*."""
len_l = len(l)
if len_l == 2:
d.update([(l[0], l[1])])
elif len_l < 2:
raise lena.core.LenaValueError(
"to make a dict, provide at least two dot-separated values."
)
else:
d.update([(l[0], nest_list({}, l[1:]))])
return d
nest_list(d, parts)
return d
[документация]def str_to_list(s):
"""Like :func:`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 :func:`str_to_dict`, this function allows
arbitrary number of dots in *s* (or none).
"""
if s == "":
return []
# s can't be a list. This function is not used as a general
# interface (as str_to_dict could be).
# s may contain empty substrings, like in "a..b"
# this is not encouraged, of course, but may suit:
# if there are two errors in some user's context logic,
# they may compensate and not destroy all.
# Another variant would be to treat empty strings
# as whole context. The variant with '' seems more understandable
# to the user.
return s.split(".")
[документация]def update_nested(d, other):
"""Update dictionary *d* with items from *other* dictionary.
*other* must be a dictionary of one element, which is used
as a key. If *d* doesn't contain the key,
*d* is updated with *other*.
If *d* contains the key, the value with that key is nested
inside the copy of *other* at the level
which doesn't contain the key. *d* is updated.
If *d[key]* is not a dictionary
or if there is not one key in *other*,
:exc:`.LenaValueError` is raised.
"""
if not isinstance(other, dict) or len(other) != 1:
raise lena.core.LenaValueError(
"other must be a dictionary of size one, "
"{} provided".format(other)
)
def get_most_nested_subdict_with(key, d):
while True:
if key in d:
d = d[key]
else:
return d
for val in other:
key = val
if key in d:
if not isinstance(d[key], dict):
raise lena.core.LenaValueError(
"d[{}] must be a dict, {} given"
.format(key, d[key])
)
other_most_nested = get_most_nested_subdict_with(key, other)
# d[key] must be a dict
other_most_nested.update(d[key])
d.update(other)
[документация]def update_recursively(d, other):
"""Update dictionary *d* with items from *other* dictionary.
*other* can be a dot-separated string. In this case
:func:`str_to_dict` is used to convert it to a dictionary.
Existing values are updated recursively,
that is including nested subdictionaries.
For 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
Both *d* and *other* must be dictionaries,
otherwise :exc:`.LenaTypeError` is raised.
"""
if isinstance(other, str):
other = str_to_dict(other)
if not isinstance(d, dict) or not isinstance(other, dict):
raise lena.core.LenaTypeError(
"d and other must be dicts, {} and {} provided".format(d, other)
)
for key, val in other.items():
if not isinstance(val, dict):
d[key] = val
else:
if key in d:
if not isinstance(d[key], dict):
d[key] = {}
update_recursively(d[key], other[key])
else:
d[key] = val