Skip to content

Commit def8664

Browse files
authored
feat(options): add support for comma-separated options (#35580)
* feat(options): add support for comma-separated experiments and service options Implement _CommaSeparatedListAction to handle comma-separated values for experiments and dataflow_service_options, matching Java SDK behavior. This allows more flexible input formats while maintaining backward compatibility. * fix lint * lint * yapf * updated the test * updated changes.md * polished docstrings
1 parent 01768c3 commit def8664

4 files changed

Lines changed: 124 additions & 4 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@
7676
* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)).
7777
* Add pip-based install support for JupyterLab Sidepanel extension ([#35397](https://github.com/apache/beam/issues/#35397)).
7878
* [IcebergIO] Create tables with a specified table properties ([#35496](https://github.com/apache/beam/pull/35496))
79+
* Add support for comma-separated options in Python SDK (Python) ([#35580](https://github.com/apache/beam/pull/35580)).
80+
Python SDK now supports comma-separated values for experiments and dataflow_service_options,
81+
matching Java SDK behavior while maintaining backward compatibility.
7982
* Milvus enrichment handler added (Python) ([#35216](https://github.com/apache/beam/pull/35216)).
8083
Beam now supports Milvus enrichment handler capabilities for vector, keyword,
8184
and hybrid search operations.

sdks/python/apache_beam/options/pipeline_options.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,71 @@ def __call__(self, parser, namespace, values, option_string=None):
220220
% _GcsCustomAuditEntriesAction.MAX_ENTRIES)
221221

222222

223+
class _CommaSeparatedListAction(argparse.Action):
224+
"""
225+
Argparse Action that splits comma-separated values and appends them to
226+
a list. This allows options like --experiments=abc,def to be treated
227+
as separate experiments 'abc' and 'def', similar to how Java SDK handles
228+
them.
229+
230+
If there are key=value experiments in a raw argument, the remaining part of
231+
the argument are treated as values and won't split further. For example:
232+
'abc,def,master_key=k1=v1,k2=v2' becomes
233+
['abc', 'def', 'master_key=k1=v1,k2=v2'].
234+
"""
235+
def __call__(self, parser, namespace, values, option_string=None):
236+
if not hasattr(namespace, self.dest) or getattr(namespace,
237+
self.dest) is None:
238+
setattr(namespace, self.dest, [])
239+
240+
# Split comma-separated values and extend the list
241+
if isinstance(values, str):
242+
# Smart splitting: only split at commas that are not part of
243+
# key=value pairs
244+
split_values = self._smart_split(values)
245+
getattr(namespace, self.dest).extend(split_values)
246+
else:
247+
# If values is not a string, just append it
248+
getattr(namespace, self.dest).append(values)
249+
250+
def _smart_split(self, values):
251+
"""Split comma-separated values, but preserve commas within
252+
key=value pairs."""
253+
result = []
254+
current = []
255+
equals_depth = 0
256+
257+
i = 0
258+
while i < len(values):
259+
char = values[i]
260+
261+
if char == '=':
262+
equals_depth += 1
263+
current.append(char)
264+
elif char == ',' and equals_depth <= 1:
265+
# This comma is a top-level separator (not inside a complex value)
266+
if current:
267+
result.append(''.join(current).strip())
268+
current = []
269+
equals_depth = 0
270+
elif char == ',' and equals_depth > 1:
271+
# This comma is inside a complex value, keep it
272+
current.append(char)
273+
elif char == ' ' and not current:
274+
# Skip leading spaces
275+
pass
276+
else:
277+
current.append(char)
278+
279+
i += 1
280+
281+
# Add the last item
282+
if current:
283+
result.append(''.join(current).strip())
284+
285+
return [v for v in result if v] # Filter out empty values
286+
287+
223288
class PipelineOptions(HasDisplayData):
224289
"""This class and subclasses are used as containers for command line options.
225290
@@ -977,7 +1042,7 @@ def _add_argparse_args(cls, parser):
9771042
'--dataflow_service_option',
9781043
'--dataflow_service_options',
9791044
dest='dataflow_service_options',
980-
action='append',
1045+
action=_CommaSeparatedListAction,
9811046
default=None,
9821047
help=(
9831048
'Options to configure the Dataflow service. These '
@@ -1412,7 +1477,7 @@ def _add_argparse_args(cls, parser):
14121477
'--experiment',
14131478
'--experiments',
14141479
dest='experiments',
1415-
action='append',
1480+
action=_CommaSeparatedListAction,
14161481
default=None,
14171482
help=(
14181483
'Runners may provide a number of experimental features that can be '

sdks/python/apache_beam/options/pipeline_options_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,58 @@ def test_validation_bad_stg_bad_temp_no_default(self):
893893
'staging_location.'
894894
])
895895

896+
def test_comma_separated_experiments(self):
897+
"""Test that comma-separated experiments are parsed correctly."""
898+
# Test single experiment
899+
options = PipelineOptions(['--experiments=abc'])
900+
self.assertEqual(['abc'], options.get_all_options()['experiments'])
901+
902+
# Test comma-separated experiments
903+
options = PipelineOptions(['--experiments=abc,def,ghi'])
904+
self.assertEqual(['abc', 'def', 'ghi'],
905+
options.get_all_options()['experiments'])
906+
907+
# Test multiple flags with comma-separated values
908+
options = PipelineOptions(
909+
['--experiments=abc,def', '--experiments=ghi,jkl'])
910+
self.assertEqual(['abc', 'def', 'ghi', 'jkl'],
911+
options.get_all_options()['experiments'])
912+
913+
# Test with spaces around commas
914+
options = PipelineOptions(['--experiments=abc, def , ghi'])
915+
self.assertEqual(['abc', 'def', 'ghi'],
916+
options.get_all_options()['experiments'])
917+
918+
# Test empty values are filtered out
919+
options = PipelineOptions(['--experiments=abc,,def,'])
920+
self.assertEqual(['abc', 'def'], options.get_all_options()['experiments'])
921+
922+
def test_comma_separated_dataflow_service_options(self):
923+
"""Test that comma-separated dataflow service options are parsed
924+
correctly."""
925+
# Test single option
926+
options = PipelineOptions(['--dataflow_service_options=option1=value1'])
927+
self.assertEqual(['option1=value1'],
928+
options.get_all_options()['dataflow_service_options'])
929+
930+
# Test comma-separated options
931+
options = PipelineOptions([
932+
'--dataflow_service_options=option1=value1,option2=value2,'
933+
'option3=value3'
934+
])
935+
self.assertEqual(['option1=value1', 'option2=value2', 'option3=value3'],
936+
options.get_all_options()['dataflow_service_options'])
937+
938+
# Test multiple flags with comma-separated values
939+
options = PipelineOptions([
940+
'--dataflow_service_options=option1=value1,option2=value2',
941+
'--dataflow_service_options=option3=value3,option4=value4'
942+
])
943+
self.assertEqual([
944+
'option1=value1', 'option2=value2', 'option3=value3', 'option4=value4'
945+
],
946+
options.get_all_options()['dataflow_service_options'])
947+
896948

897949
if __name__ == '__main__':
898950
logging.getLogger().setLevel(logging.INFO)

sdks/python/apache_beam/options/value_provider_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,8 @@ def test_experiments_options_setup(self):
216216
options = PipelineOptions(['--experiments', 'a', '--experiments', 'b,c'])
217217
options = options.view_as(DebugOptions)
218218
self.assertIn('a', options.experiments)
219-
self.assertIn('b,c', options.experiments)
220-
self.assertNotIn('c', options.experiments)
219+
self.assertIn('b', options.experiments)
220+
self.assertIn('c', options.experiments)
221221

222222
def test_nested_value_provider_wrap_static(self):
223223
vp = NestedValueProvider(StaticValueProvider(int, 1), lambda x: x + 1)

0 commit comments

Comments
 (0)