Skip to content

Commit 4ed10f0

Browse files
committed
feat: support lists in 'resolve_keyed_by'
1 parent 17cb096 commit 4ed10f0

File tree

4 files changed

+85
-23
lines changed

4 files changed

+85
-23
lines changed

src/taskgraph/util/keyed_by.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,40 @@
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 not in container:
28+
return
29+
30+
if isinstance(container[f], list):
31+
for item in container[f]:
32+
yield from iter_dot_path(item, subfield)
33+
return
34+
35+
container = container[f]
36+
37+
if isinstance(container, dict) and subfield in container:
38+
yield container, subfield
739

840

941
def evaluate_keyed_by(

src/taskgraph/util/schema.py

+9-22
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

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
),
25+
)
26+
def test_iter_dot_paths(container, subfield, expected):
27+
result = list(iter_dot_path(container, subfield))
28+
pprint(result, indent=2)
29+
assert result == expected

test/test_util_schema.py

+14
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)