Skip to content

Commit 7583cfd

Browse files
committed
feat: support lists in 'resolve_keyed_by'
1 parent 508d988 commit 7583cfd

File tree

4 files changed

+101
-23
lines changed

4 files changed

+101
-23
lines changed

src/taskgraph/util/keyed_by.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,44 @@
22
# License, v. 2.0. If a copy of the MPL was not distributed with this
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

5+
from typing import Any, Dict, Generator, Tuple
56

6-
from .attributes import keymatch
7+
from taskgraph.util.attributes import keymatch
8+
9+
10+
def iter_dot_path(
11+
container: Dict[str, Any], subfield: str
12+
) -> Generator[Tuple[Dict[str, Any], str], None, None]:
13+
"""Given a container and a subfield in dot path notation, yield the parent
14+
container of the dotpath's leaf node, along with the leaf node name that it
15+
contains.
16+
17+
If the dot path contains a list object, each item in the list will be
18+
yielded.
19+
20+
Args:
21+
container (dict): The container to search for the dot path.
22+
subfield (str): The dot path to search for.
23+
"""
24+
while "." in subfield:
25+
f, subfield = subfield.split(".", 1)
26+
27+
if f.endswith("[]"):
28+
f = f[0:-2]
29+
if not isinstance(container.get(f), list):
30+
return
31+
32+
for item in container[f]:
33+
yield from iter_dot_path(item, subfield)
34+
return
35+
36+
if f not in container:
37+
return
38+
39+
container = container[f]
40+
41+
if isinstance(container, dict) and subfield in container:
42+
yield container, subfield
743

844

945
def evaluate_keyed_by(

src/taskgraph/util/schema.py

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
import voluptuous
1111

1212
import taskgraph
13-
14-
from .keyed_by import evaluate_keyed_by
13+
from taskgraph.util.keyed_by import evaluate_keyed_by, iter_dot_path
1514

1615

1716
def validate_schema(schema, obj, msg_prefix):
@@ -125,26 +124,14 @@ def resolve_keyed_by(
125124
Returns:
126125
dict: item which has also been modified in-place.
127126
"""
128-
# find the field, returning the item unchanged if anything goes wrong
129-
container, subfield = item, field
130-
while "." in subfield:
131-
f, subfield = subfield.split(".", 1)
132-
if f not in container:
133-
return item
134-
container = container[f]
135-
if not isinstance(container, dict):
136-
return item
137-
138-
if subfield not in container:
139-
return item
140-
141-
container[subfield] = evaluate_keyed_by(
142-
value=container[subfield],
143-
item_name=f"`{field}` in `{item_name}`",
144-
defer=defer,
145-
enforce_single_match=enforce_single_match,
146-
attributes=dict(item, **extra_values),
147-
)
127+
for container, subfield in iter_dot_path(item, field):
128+
container[subfield] = evaluate_keyed_by(
129+
value=container[subfield],
130+
item_name=f"`{field}` in `{item_name}`",
131+
defer=defer,
132+
enforce_single_match=enforce_single_match,
133+
attributes=dict(item, **extra_values),
134+
)
148135

149136
return item
150137

test/test_util_keyed_by.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
from pprint import pprint
6+
7+
import pytest
8+
9+
from taskgraph.util.keyed_by import iter_dot_path
10+
11+
12+
@pytest.mark.parametrize(
13+
"container,subfield,expected",
14+
(
15+
pytest.param(
16+
{"a": {"b": {"c": 1}}, "d": 2}, "a.b.c", [({"c": 1}, "c")], id="simple case"
17+
),
18+
pytest.param(
19+
{"a": [{"b": 1}, {"b": 2}, {"b": 3}], "d": 2},
20+
"a[].b",
21+
[({"b": 1}, "b"), ({"b": 2}, "b"), ({"b": 3}, "b")],
22+
id="list case",
23+
),
24+
pytest.param(
25+
{"a": [{"b": 1}, {"b": 2}, {"c": 3}], "d": 2},
26+
"a[].b",
27+
[({"b": 1}, "b"), ({"b": 2}, "b")],
28+
id="list partial match case",
29+
),
30+
pytest.param(
31+
{"a": [{"b": {"c": 4}}, {"b": {"c": 5}}, {"b": {"c": 6}}], "d": 2},
32+
"a[].b.c",
33+
[({"c": 4}, "c"), ({"c": 5}, "c"), ({"c": 6}, "c")],
34+
id="deep list case",
35+
),
36+
),
37+
)
38+
def test_iter_dot_paths(container, subfield, expected):
39+
result = list(iter_dot_path(container, subfield))
40+
pprint(result, indent=2)
41+
assert result == expected

test/test_util_schema.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@ def test_nested(self):
106106
{"x": {"by-bar": {"B1": 11, "B2": 12}}},
107107
)
108108

109+
def test_list(self):
110+
item = {
111+
"y": {
112+
"by-foo": {
113+
"F1": 10,
114+
"F2": 20,
115+
},
116+
}
117+
}
118+
self.assertEqual(
119+
resolve_keyed_by({"x": [item, item]}, "x[].y", "name", foo="F1"),
120+
{"x": [{"y": 10}, {"y": 10}]},
121+
)
122+
109123
def test_no_by_empty_dict(self):
110124
self.assertEqual(resolve_keyed_by({"x": {}}, "x", "n"), {"x": {}})
111125

0 commit comments

Comments
 (0)