Skip to content

Commit b1deea4

Browse files
fmeumcopybara-github
authored andcommitted
Starlark: Add dict union operators | and |=
Implements the Starlark spec addition of bazelbuild/starlark#215. Work towards #12457 Closes #14540. PiperOrigin-RevId: 431737269
1 parent 44e8d07 commit b1deea4

File tree

4 files changed

+82
-1
lines changed

4 files changed

+82
-1
lines changed

src/main/java/net/starlark/java/eval/Dict.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
+ "0 in d, \"a\" in d # (True, False)\n"
7373
+ "[(k, v) for k, v in d.items()] # [(0, \"a\"), (1, 1), (2, \"b\")]\n"
7474
+ "</pre>\n"
75-
+ "<p>There are three ways to construct a dictionary:\n"
75+
+ "<p>There are four ways to construct a dictionary:\n"
7676
+ "<ol>\n"
7777
+ "<li>A dictionary expression <code>{k: v, ...}</code> yields a new dictionary with"
7878
+ " the specified key/value entries, inserted in the order they appear in the"
@@ -90,6 +90,17 @@
9090
+ " a dictionary containing the specified entries, which are inserted in argument"
9191
+ " order, positional arguments before named. As with comprehensions, duplicate keys"
9292
+ " are permitted.\n"
93+
+ "<li>The union expression <code>x | y</code> yields a new dictionary by combining two"
94+
+ " existing dictionaries. If the two dictionaries have a key <code>k</code> in common,"
95+
+ " the right hand side dictionary's value of the key (in other words,"
96+
+ " <code>y[k]</code>) wins. The <code>|=</code> variant of the union operator modifies"
97+
+ " a dictionary in-place. Example:<br>"
98+
+ "<pre class=language-python>"
99+
+ "d = {\"foo\": \"FOO\", \"bar\": \"BAR\"} | {\"foo\": \"FOO2\", \"baz\": \"BAZ\"}\n"
100+
+ "# d == {\"foo\": \"FOO2\", \"bar\": \"BAR\", \"baz\": \"BAZ\"}\n"
101+
+ "d = {\"a\": 1, \"b\": 2}\n"
102+
+ "d |= {\"b\": 3, \"c\": 4}\n"
103+
+ "# d == {\"a\": 1, \"b\": 3, \"c\": 4}</pre>"
93104
+ "</ol>")
94105
public class Dict<K, V>
95106
implements Map<K, V>,

src/main/java/net/starlark/java/eval/Eval.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,14 @@ private static Object inplaceBinaryOp(StarlarkThread.Frame fr, TokenKind op, Obj
458458
StarlarkList<?> list = (StarlarkList) x;
459459
list.extend(y);
460460
return list;
461+
} else if (op == TokenKind.PIPE && x instanceof Dict && y instanceof Dict) {
462+
// dict |= dict merges the contents of the second dict into the first.
463+
@SuppressWarnings("unchecked")
464+
Dict<Object, Object> xDict = (Dict<Object, Object>) x;
465+
@SuppressWarnings("unchecked")
466+
Dict<Object, Object> yDict = (Dict<Object, Object>) y;
467+
xDict.putEntries(yDict);
468+
return xDict;
461469
}
462470
return EvalUtils.binaryOp(op, x, y, fr.thread);
463471
}

src/main/java/net/starlark/java/eval/EvalUtils.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ static Object binaryOp(TokenKind op, Object x, Object y, StarlarkThread starlark
126126
// int | int
127127
return StarlarkInt.or((StarlarkInt) x, (StarlarkInt) y);
128128
}
129+
} else if (x instanceof Dict) {
130+
if (y instanceof Dict) {
131+
// dict | dict
132+
return Dict.builder().putAll((Dict<?, ?>) x).putAll((Dict<?, ?>) y).build(mu);
133+
}
129134
}
130135
break;
131136

src/test/java/net/starlark/java/eval/testdata/dict.star

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,63 @@ d.update((["f", 0],), f = 6)
7575
expected = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6}
7676
assert_eq(d, expected)
7777

78+
# dict union operator |
79+
80+
empty_dict = dict()
81+
dict_with_a_b = dict(a=1, b=[1, 2])
82+
dict_with_b = dict(b=[1, 2])
83+
dict_with_other_b = dict(b=[3, 4])
84+
85+
assert_eq(empty_dict | dict_with_a_b, dict_with_a_b)
86+
# Verify iteration order.
87+
assert_eq((empty_dict | dict_with_a_b).items(), dict_with_a_b.items())
88+
assert_eq(dict_with_a_b | empty_dict, dict_with_a_b)
89+
assert_eq((dict_with_a_b | empty_dict).items(), dict_with_a_b.items())
90+
assert_eq(dict_with_b | dict_with_a_b, dict_with_a_b)
91+
assert_eq((dict_with_b | dict_with_a_b).items(), dict(b=[1, 2], a=1).items())
92+
assert_eq(dict_with_a_b | dict_with_b, dict_with_a_b)
93+
assert_eq((dict_with_a_b | dict_with_b).items(), dict_with_a_b.items())
94+
assert_eq(dict_with_b | dict_with_other_b, dict_with_other_b)
95+
assert_eq((dict_with_b | dict_with_other_b).items(), dict_with_other_b.items())
96+
assert_eq(dict_with_other_b | dict_with_b, dict_with_b)
97+
assert_eq((dict_with_other_b | dict_with_b).items(), dict_with_b.items())
98+
99+
assert_eq(empty_dict, dict())
100+
assert_eq(dict_with_b, dict(b=[1,2]))
101+
102+
assert_fails(lambda: dict() | [], "unsupported binary operation")
103+
104+
# dict union assignment operator |=
105+
106+
def test_dict_union_assignment():
107+
empty_dict = dict()
108+
empty_dict |= {"a": 1}
109+
empty_dict |= {"b": 2}
110+
empty_dict |= {"c": "3", 7: 4}
111+
empty_dict |= {"b": "5", "e": 6}
112+
expected_1 = {"a": 1, "b": "5", "c": "3", 7: 4, "e": 6}
113+
assert_eq(empty_dict, expected_1)
114+
assert_eq(empty_dict.items(), expected_1.items())
115+
116+
dict_a = {8: 1, "b": 2}
117+
dict_b = {"b": 1, "c": 6}
118+
dict_c = {"d": 7}
119+
dict_tuple = {(5, "a"): ("c", 8)}
120+
dict_a |= dict_b
121+
dict_c |= dict_a
122+
dict_c |= dict_tuple
123+
expected_2 = {"d": 7, 8: 1, "b": 1, "c": 6, (5, "a"): ("c", 8)}
124+
assert_eq(dict_c, expected_2)
125+
assert_eq(dict_c.items(), expected_2.items())
126+
assert_eq(dict_b, {"b": 1, "c": 6})
127+
128+
test_dict_union_assignment()
129+
130+
def dict_union_assignment_type_mismatch():
131+
some_dict = dict()
132+
some_dict |= []
133+
134+
assert_fails(dict_union_assignment_type_mismatch, "unsupported binary operation")
78135

79136
# creation with repeated keys
80137

0 commit comments

Comments
 (0)