Skip to content

Commit 77d3a5f

Browse files
authored
Merge pull request #1621 from pypa/parse-kvs-colons
fix: improve parse_key_value_string to allow colons in values
2 parents 9535d16 + 6879810 commit 77d3a5f

File tree

2 files changed

+90
-5
lines changed

2 files changed

+90
-5
lines changed

cibuildwheel/util.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -731,23 +731,26 @@ def parse_key_value_string(
731731

732732
all_field_names = [*positional_arg_names, *kw_arg_names]
733733

734-
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";:")
734+
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";")
735735
shlexer.commenters = ""
736+
shlexer.whitespace_split = True
736737
parts = list(shlexer)
737738
# parts now looks like
738-
# ['docker', ';', 'create_args',':', '--some-option=value', 'another-option']
739+
# ['docker', ';', 'create_args:', '--some-option=value', 'another-option']
739740

740741
# split by semicolon
741742
fields = [list(group) for k, group in itertools.groupby(parts, lambda x: x == ";") if not k]
742743

743744
result: dict[str, list[str]] = defaultdict(list)
744745
for field_i, field in enumerate(fields):
745-
if len(field) > 1 and field[1] == ":":
746-
field_name = field[0]
747-
values = field[2:]
746+
# check to see if the option name is specified
747+
field_name, sep, first_value = field[0].partition(":")
748+
if sep:
748749
if field_name not in all_field_names:
749750
msg = f"Failed to parse {key_value_string!r}. Unknown field name {field_name!r}"
750751
raise ValueError(msg)
752+
753+
values = ([first_value] if first_value else []) + field[1:]
751754
else:
752755
try:
753756
field_name = positional_arg_names[field_i]

unit_test/utils_test.py

+82
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
find_compatible_wheel,
1010
fix_ansi_codes_for_github_actions,
1111
format_safe,
12+
parse_key_value_string,
1213
prepare_command,
1314
)
1415

@@ -124,3 +125,84 @@ def test_fix_ansi_codes_for_github_actions():
124125
output = fix_ansi_codes_for_github_actions(input)
125126

126127
assert output == expected
128+
129+
130+
def test_parse_key_value_string():
131+
assert parse_key_value_string("bar", positional_arg_names=["foo"]) == {"foo": ["bar"]}
132+
assert parse_key_value_string("foo:bar", kw_arg_names=["foo"]) == {"foo": ["bar"]}
133+
with pytest.raises(ValueError, match="Too many positional arguments"):
134+
parse_key_value_string("bar")
135+
with pytest.raises(ValueError, match="Unknown field name"):
136+
parse_key_value_string("foo:bar")
137+
assert parse_key_value_string("foo:bar", kw_arg_names=["foo"]) == {"foo": ["bar"]}
138+
assert parse_key_value_string("foo:bar", positional_arg_names=["foo"]) == {"foo": ["bar"]}
139+
assert parse_key_value_string("foo: bar", kw_arg_names=["foo"]) == {"foo": ["bar"]}
140+
assert parse_key_value_string("foo: bar", kw_arg_names=["foo"]) == {"foo": ["bar"]}
141+
assert parse_key_value_string("foo: bar; baz: qux", kw_arg_names=["foo", "baz"]) == {
142+
"foo": ["bar"],
143+
"baz": ["qux"],
144+
}
145+
146+
# some common options
147+
assert parse_key_value_string(
148+
"docker; create_args: --some-option --another-option=foo",
149+
positional_arg_names=["name"],
150+
kw_arg_names=["create_args"],
151+
) == {
152+
"name": ["docker"],
153+
"create_args": ["--some-option", "--another-option=foo"],
154+
}
155+
# semicolon in value
156+
assert parse_key_value_string(
157+
"docker; create_args: --some-option='this; that'",
158+
positional_arg_names=["name"],
159+
kw_arg_names=["create_args"],
160+
) == {
161+
"name": ["docker"],
162+
"create_args": ["--some-option=this; that"],
163+
}
164+
# colon in value
165+
assert parse_key_value_string(
166+
"docker; create_args: --mount a:b",
167+
positional_arg_names=["name"],
168+
kw_arg_names=["create_args"],
169+
) == {
170+
"name": ["docker"],
171+
"create_args": ["--mount", "a:b"],
172+
}
173+
assert parse_key_value_string(
174+
"docker;create_args:--mount a:b",
175+
positional_arg_names=["name"],
176+
kw_arg_names=["create_args"],
177+
) == {
178+
"name": ["docker"],
179+
"create_args": ["--mount", "a:b"],
180+
}
181+
# quoted value with spaces
182+
assert parse_key_value_string(
183+
"docker;create_args:'some string with spaces'",
184+
positional_arg_names=["name"],
185+
kw_arg_names=["create_args"],
186+
) == {
187+
"name": ["docker"],
188+
"create_args": ["some string with spaces"],
189+
}
190+
191+
# colon in positional value
192+
assert parse_key_value_string(
193+
"docker; --mount a:b",
194+
positional_arg_names=["name", "create_args"],
195+
) == {
196+
"name": ["docker"],
197+
"create_args": ["--mount", "a:b"],
198+
}
199+
200+
# empty option gives empty array
201+
assert parse_key_value_string(
202+
"docker;create_args:",
203+
positional_arg_names=["name"],
204+
kw_arg_names=["create_args"],
205+
) == {
206+
"name": ["docker"],
207+
"create_args": [],
208+
}

0 commit comments

Comments
 (0)