-
-
Notifications
You must be signed in to change notification settings - Fork 163
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #108 from nolar/recursive-labelling
Label sub-templates recursively
- Loading branch information
Showing
11 changed files
with
203 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
""" | ||
Some basic dicts and field-in-a-dict manipulation helpers. | ||
""" | ||
import collections.abc | ||
from typing import Any, Union, Mapping, Tuple, List, Text, Iterable, Optional | ||
|
||
FieldPath = Tuple[str, ...] | ||
FieldSpec = Union[None, Text, FieldPath, List[str]] | ||
|
||
_UNSET = object() | ||
|
||
|
||
def parse_field( | ||
field: FieldSpec, | ||
) -> FieldPath: | ||
""" | ||
Convert any field into a tuple of nested sub-fields. | ||
""" | ||
if field is None: | ||
return tuple() | ||
elif isinstance(field, str): | ||
return tuple(field.split('.')) | ||
elif isinstance(field, (list, tuple)): | ||
return tuple(field) | ||
else: | ||
raise ValueError(f"Field must be either a str, or a list/tuple. Got {field!r}") | ||
|
||
|
||
def resolve( | ||
d: Mapping, | ||
field: FieldSpec, | ||
default: Any = _UNSET, | ||
): | ||
""" | ||
Retrieve a nested sub-field from a dict. | ||
""" | ||
path = parse_field(field) | ||
try: | ||
result = d | ||
for key in path: | ||
result = result[key] | ||
return result | ||
except KeyError: | ||
if default is _UNSET: | ||
raise | ||
else: | ||
return default | ||
|
||
|
||
def walk( | ||
objs, | ||
nested: Optional[Iterable[FieldSpec]] = None, | ||
): | ||
""" | ||
Iterate over one or many dicts (and sub-dicts recursively). | ||
""" | ||
if objs is None: | ||
return | ||
elif isinstance(objs, collections.abc.Mapping): | ||
yield objs | ||
for subfield in (nested if nested is not None else []): | ||
try: | ||
yield resolve(objs, parse_field(subfield)) | ||
except KeyError: | ||
pass | ||
elif isinstance(objs, collections.abc.Iterable): | ||
for obj in objs: | ||
yield from walk(obj, nested=nested) | ||
else: | ||
yield objs # NB: not a mapping, no nested sub-fields. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import pytest | ||
|
||
from kopf.structs.dicts import parse_field | ||
|
||
|
||
def test_from_none(): | ||
path = parse_field(None) | ||
assert isinstance(path, tuple) | ||
assert len(path) == 0 | ||
|
||
|
||
def test_from_string_one_level(): | ||
path = parse_field('field') | ||
assert isinstance(path, tuple) | ||
assert path == ('field',) | ||
|
||
|
||
def test_from_string_two_levels(): | ||
path = parse_field('field.subfield') | ||
assert isinstance(path, tuple) | ||
assert path == ('field', 'subfield') | ||
|
||
|
||
def test_from_list(): | ||
path = parse_field(['field' , 'subfield']) | ||
assert isinstance(path, tuple) | ||
assert path == ('field', 'subfield') | ||
|
||
|
||
def test_from_tuple(): | ||
path = parse_field(('field' , 'subfield')) | ||
assert isinstance(path, tuple) | ||
assert path == ('field', 'subfield') | ||
|
||
|
||
@pytest.mark.parametrize('val', [dict(), set(), frozenset()]) | ||
def test_from_others_fails(val): | ||
with pytest.raises(ValueError): | ||
parse_field(val) |
File renamed without changes.
2 changes: 1 addition & 1 deletion
2
tests/diffs/test_resolving.py → tests/diffs-n-dicts/test_resolving.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
from kopf.structs.dicts import walk | ||
|
||
|
||
def test_over_a_none(): | ||
result = list(walk(None)) | ||
assert len(result) == 0 | ||
|
||
|
||
def test_over_a_dict(): | ||
obj = {} | ||
result = list(walk(obj)) | ||
assert len(result) == 1 | ||
assert result[0] is obj | ||
|
||
|
||
def test_over_a_list_of_dicts(): | ||
obj1 = {} | ||
obj2 = {} | ||
result = list(walk([obj1, obj2])) | ||
assert len(result) == 2 | ||
assert result[0] is obj1 | ||
assert result[1] is obj2 | ||
|
||
|
||
def test_over_a_tuple_of_dicts(): | ||
obj1 = {} | ||
obj2 = {} | ||
result = list(walk((obj1, obj2))) | ||
assert len(result) == 2 | ||
assert result[0] is obj1 | ||
assert result[1] is obj2 | ||
|
||
|
||
def test_none_is_ignored(): | ||
obj1 = {} | ||
obj2 = {} | ||
result = list(walk([obj1, None, obj2])) | ||
assert len(result) == 2 | ||
assert result[0] is obj1 | ||
assert result[1] is obj2 | ||
|
||
|
||
def test_simple_nested(): | ||
obj1 = {'field': {'subfield': 'val'}} | ||
obj2 = {'field': {}} | ||
result = list(walk([obj1, obj2], nested=['field.subfield'])) | ||
assert len(result) == 3 | ||
assert result[0] is obj1 | ||
assert result[1] == 'val' | ||
assert result[2] is obj2 | ||
|
||
|
||
def test_double_nested(): | ||
obj1 = {'field': {'subfield': 'val'}} | ||
obj2 = {'field': {}} | ||
result = list(walk([obj1, obj2], nested=['field.subfield', 'field'])) | ||
assert len(result) == 5 | ||
assert result[0] is obj1 | ||
assert result[1] == 'val' | ||
assert result[2] == {'subfield': 'val'} | ||
assert result[3] is obj2 | ||
assert result[4] == {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters