Skip to content

Commit

Permalink
Fix a bug in update_context. Add UpdateContext. Improve documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
ynikitenko committed Apr 21, 2020
1 parent 109e052 commit 09d81c2
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 27 deletions.
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def setup(app):
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
# 'sphinx.ext.intersphinx',
# 'sphinx_automodapi.automodapi',
'sphinx.ext.coverage',
# 'sphinx-prompt', # doesn't show command output.
Expand Down
18 changes: 12 additions & 6 deletions docs/source/context.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
Context
=======
**Context:**
**Elements:**

.. currentmodule:: lena.context.context
.. currentmodule:: lena.context
.. autosummary::

Context
UpdateContext

**Functions:**

Expand All @@ -31,12 +32,17 @@ Context
update_nested
update_recursively
Context
-------
.. automodule:: lena.context.context
:special-members: __call__, __getitem__
Elements
--------
.. currentmodule:: lena.context
.. autoclass:: Context
:special-members: __call__
:show-inheritance:

.. autoclass:: UpdateContext
:special-members: __call__


.. _context_functions:

Functions
Expand Down
43 changes: 26 additions & 17 deletions lena/context/context.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
"""Make better output 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
}
}
"""
""":class:`Context` provides a better representation for context."""
from __future__ import print_function

import json
Expand All @@ -19,10 +8,23 @@


class Context(dict):
"""Dictionary with easy-to-read formatting."""
"""Dictionary with easy-to-read formatting.
:class:`Context` provides a better representation for context. Example:
def __init__(self, d={}, formatter=None):
"""Initialize from a dictionary *d*.
>>> from lena.context import Context
>>> c = Context({"1": 1, "2": {"3": 4}})
>>> print(c) # doctest: +NORMALIZE_WHITESPACE
{
"1": 1,
"2": {
"3": 4
}
}
"""

def __init__(self, d=None, formatter=None):
"""Initialize from a dictionary *d* (empty by default).
Representation is defined by the *formatter*.
That must be a callable,
Expand All @@ -32,12 +34,15 @@ def __init__(self, d={}, formatter=None):
Tip
---
JSON and Python representations are different.
In particular, JSON *True* is written lowercase *true*.
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,
:exc:`~lena.core.LenaTypeError` is raised.
:exc:`.LenaTypeError` is raised.
"""
# todo: maybe add intersphinx reference to json
if d is None:
d = {}
super(Context, self).__init__(d)
if formatter is not None:
if not callable(formatter):
Expand All @@ -60,6 +65,10 @@ def __call__(self, value):
convert its context part to :class:`Context`.
If the *value* doesn't contain a context,
it is created as an empty :class:`Context`.
When a :class:`Context` is used as a sequence element,
its initialization argument *d*
has no effect on the produced values.
"""
data, context = lena.flow.get_data_context(value)
return (data, Context(context))
7 changes: 6 additions & 1 deletion lena/context/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ def str_to_list(s):
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
Contrarily to :func:`str_to_dict`, this function allows
arbitrary number of dots in *s* (or none).
"""
if s == "":
Expand All @@ -321,6 +321,9 @@ def str_to_list(s):
# 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(".")


Expand Down Expand Up @@ -400,6 +403,8 @@ def update_recursively(d, other):
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
85 changes: 85 additions & 0 deletions lena/context/update_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from __future__ import print_function

import copy

import lena.core
import lena.flow


class UpdateContext():
"""Update context of passing values."""

def __init__(self, subcontext, update, recursively=True):
"""*subcontext* is a string representing the part of context
to be updated (for example, *output.plot*).
If *subcontext* is an empty string,
all context will be overwritten.
*update* is a dictionary that will become
the value of *subcontext*.
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})
>>> # use it in a sequence
The context in the class name means any general context
(not only :class:`.Context`).
In case of wrong types of *subcontext* or *update*
:exc:`.LenaTypeError` is raised.
"""
# subcontext is a string, because it must have at most one value
# at each nesting level.
# todo. update may be made a string in the future.
# todo. also, subcontext may be done a format string "{variable.name}",
# but this is not implemented (think about it in the future).
if not isinstance(subcontext, str):
raise lena.core.LenaTypeError(
"subcontext must be a string, {} provided".format(subcontext)
)
if not isinstance(update, dict):
raise lena.core.LenaTypeError(
"update must be a dict, {} provided".format(update)
)
self._subcontext = lena.context.str_to_list(subcontext)
self._update = update
self._recursively = bool(recursively)

def __call__(self, value):
"""Update context of the *value*.
If *value*'s context doesn't contain *subcontext*, it is created.
If the *value* contains no context, it is also created.
"""
data, context = lena.flow.get_data_context(value)
if self._subcontext == []:
# overwrite all context. This may be undesirable,
# but better than throwing an error
# in the middle of calculations.
# Context is not so much important.
# Overwrite all context, for uniformity with other cases.
if self._recursively:
lena.context.update_recursively(context, self._update)
else:
context.clear()
context.update(copy.deepcopy(self._update))
return (data, context)
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]: self._update})
else:
subdict[keys[-1]] = self._update
return (data, context)
8 changes: 5 additions & 3 deletions tests/context/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,12 @@ def test_update_recursively():
d1 = {"a": 1, "b": {"c": 3}}
d2 = {"b": {"d": 4}}
update_recursively(d1, d2)
assert d1 == {'a': 1, 'b': {'c': 3, 'd': 4}}
assert d1 == {"a": 1, "b": {"c": 3, "d": 4}}
update_recursively(d1, {"b": 2})
assert d1 == {'a': 1, 'b': 2}
assert d1 == {"a": 1, "b": 2}
update_recursively(d1, {"e": {"f": 2}})
assert d1 == {'a': 1, 'b': 2, 'e': {'f': 2}}
assert d1 == {"a": 1, "b": 2, "e": {"f": 2}}
with pytest.raises(lena.core.LenaTypeError):
update_recursively(1, {})
update_recursively(d1, {"a": {"b": "c"}})
assert d1 == {"a": {"b": "c"}, "b": 2, "e": {"f": 2}}
52 changes: 52 additions & 0 deletions tests/context/test_update_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import copy
import pytest

import lena.core
from lena.context import UpdateContext


def test_update_context():
data = (0, {"data": "yes"})
orig_data = copy.deepcopy(data)

# empty subcontext overwrites all context
uc1 = UpdateContext("", {}, recursively=False)
assert uc1(data) == (0, {})
assert data == (0, {})
data = orig_data
# update all context recursively preserves existing data
uc12 = UpdateContext("", {"new_data": "no"})
assert uc12(copy.deepcopy(data)) == (0, {"data": "yes", "new_data": "no"})

# subcontext and update must be a string or a dict
with pytest.raises(lena.core.LenaTypeError):
UpdateContext(0, {})
with pytest.raises(lena.core.LenaTypeError):
UpdateContext("", 0)

# non-empty subcontext is a proper subcontext
uc2 = UpdateContext("new_data", {"new_data": "Yes"})
assert uc2(copy.deepcopy(data)) == (0, {"data": "yes", "new_data": {"new_data": "Yes"}})
uc3 = UpdateContext("data", {"new_data": "Yes"})
assert uc3(copy.deepcopy(data)) == (0, {"data": {"new_data": "Yes"}})

# nested subcontext works
data = (0, {"data": {"yes": {"Yes": "YES"}}})
uc4 = UpdateContext("data.yes", {"new_data": "Yes"})
# recursively preserves context
assert uc4(copy.deepcopy(data)) == (0, {"data": {"yes": {"Yes": "YES", "new_data": "Yes"}}})
# non-recursively overwrites context
uc5 = UpdateContext("data.yes", {"new_data": "Yes"}, recursively=False)
assert uc5(copy.deepcopy(data)) == (0, {"data": {"yes": {"new_data": "Yes"}}})
# key not in subdictionary
data = (0, {"data": {"_yes": {"Yes": "YES"}}})
uc6 = UpdateContext("data.yes.Yes", {"new_data": "Yes"}, recursively=False)
assert uc6(copy.deepcopy(data)) == (
0,
{
"data": {
"yes": {"Yes": {"new_data": "Yes"}},
"_yes": {"Yes": "YES"}
}
}
)

0 comments on commit 09d81c2

Please sign in to comment.