Skip to content

Commit 96ad2f0

Browse files
neuralsorcerermeta-codesync[bot]
authored andcommitted
Validate CLI comma-separated column arguments (#361)
Summary: Pull Request resolved: #361 Reviewed By: sahil350 Differential Revision: D96316234 Pulled By: talgalili fbshipit-source-id: 931ecabd7b6953f127aa21c3f7a96054f80ba2f4
1 parent 31f10c3 commit 96ad2f0

3 files changed

Lines changed: 113 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@
7979
Series objects and returns `None` when both `below` and `above` are `None`,
8080
avoiding ambiguous concat inputs while preserving existing behavior for valid
8181
threshold sets.
82+
- **Validated and normalized comma-separated CLI column arguments**
83+
- CLI column-list arguments now trim surrounding whitespace and reject empty
84+
entries (for example, `"id,,weight"`) with clear `ValueError` messages,
85+
preventing malformed column specifications from silently propagating.
86+
- Applied to `--covariate_columns`, `--covariate_columns_for_diagnostics`,
87+
`--batch_columns`, `--keep_columns`, and `--outcome_columns` parsing.
8288

8389
## Tests
8490

balance/cli.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import logging
1313
from argparse import ArgumentParser, Namespace
1414
from pathlib import Path
15-
from typing import Any, Dict, List, Tuple, Type
15+
from typing import Any, Dict, List, Optional, Tuple, Type
1616

1717
import balance
1818
import pandas as pd
@@ -25,6 +25,30 @@
2525
logger: logging.Logger = logging.getLogger(__package__)
2626

2727

28+
def _parse_csv_columns_arg(value: Optional[str], arg_name: str) -> List[str]:
29+
"""Parse a comma-separated CLI columns argument into a validated list.
30+
31+
Args:
32+
value: Raw argparse string value.
33+
arg_name: Argument name used for error context.
34+
35+
Returns:
36+
A list of non-empty, trimmed column names.
37+
38+
Raises:
39+
ValueError: If the value is missing or contains empty column names.
40+
"""
41+
if value is None:
42+
raise ValueError(f"{arg_name} cannot be None")
43+
44+
columns = [c.strip() for c in value.split(",")]
45+
if not columns or any(c == "" for c in columns):
46+
raise ValueError(
47+
f"{arg_name} must be a comma-separated list of non-empty column names"
48+
)
49+
return columns
50+
51+
2852
class BalanceCLI:
2953
"""Helper class that encapsulates CLI argument handling and execution.
3054
@@ -195,7 +219,9 @@ def covariate_columns(self) -> List[str]:
195219
BalanceCLI(Namespace(covariate_columns="x,y")).covariate_columns()
196220
# ['x', 'y']
197221
"""
198-
return self.args.covariate_columns.split(",")
222+
return _parse_csv_columns_arg(
223+
self.args.covariate_columns, "--covariate_columns"
224+
)
199225

200226
def covariate_columns_for_diagnostics(self) -> List[str] | None:
201227
"""Return covariate columns used for diagnostics reporting.
@@ -212,7 +238,11 @@ def covariate_columns_for_diagnostics(self) -> List[str] | None:
212238
# ['x', 'y']
213239
"""
214240
out = self.args.covariate_columns_for_diagnostics
215-
return None if out is None else out.split(",")
241+
return (
242+
None
243+
if out is None
244+
else _parse_csv_columns_arg(out, "--covariate_columns_for_diagnostics")
245+
)
216246

217247
def rows_to_keep_for_diagnostics(self) -> str | None:
218248
"""Return the diagnostics row-filter expression.
@@ -275,7 +305,7 @@ def batch_columns(self) -> List[str]:
275305
BalanceCLI(Namespace(batch_columns="region,team")).batch_columns()
276306
# ['region', 'team']
277307
"""
278-
return self.args.batch_columns.split(",")
308+
return _parse_csv_columns_arg(self.args.batch_columns, "--batch_columns")
279309

280310
def has_keep_columns(self) -> bool:
281311
"""Return True when output keep columns are supplied.
@@ -318,8 +348,8 @@ def keep_columns(self) -> List[str] | None:
318348
BalanceCLI(Namespace(keep_columns="id,weight")).keep_columns()
319349
# ['id', 'weight']
320350
"""
321-
if self.args.keep_columns:
322-
return self.args.keep_columns.split(",")
351+
if self.args.keep_columns is not None:
352+
return _parse_csv_columns_arg(self.args.keep_columns, "--keep_columns")
323353
return None
324354

325355
def has_keep_row_column(self) -> bool:
@@ -376,8 +406,10 @@ def outcome_columns(self) -> List[str] | None:
376406
BalanceCLI(Namespace(outcome_columns="y,z")).outcome_columns()
377407
# ['y', 'z']
378408
"""
379-
if self.args.outcome_columns:
380-
return self.args.outcome_columns.split(",")
409+
if self.args.outcome_columns is not None:
410+
return _parse_csv_columns_arg(
411+
self.args.outcome_columns, "--outcome_columns"
412+
)
381413
return None
382414

383415
def max_de(self) -> float | None:

tests/test_cli.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,32 @@ def test_keep_columns_returns_none_when_not_set(self) -> None:
14461446
result = cli.keep_columns()
14471447
self.assertIsNone(result)
14481448

1449+
def test_keep_columns_strips_whitespace(self) -> None:
1450+
"""Test keep_columns trims whitespace around comma-separated names."""
1451+
args = Namespace(keep_columns=" id, weight ,extra ")
1452+
cli = BalanceCLI(args)
1453+
self.assertEqual(cli.keep_columns(), ["id", "weight", "extra"])
1454+
1455+
def test_keep_columns_raises_for_empty_column_name(self) -> None:
1456+
"""Test keep_columns rejects empty names in comma-separated input."""
1457+
args = Namespace(keep_columns="id,,weight")
1458+
cli = BalanceCLI(args)
1459+
with self.assertRaisesRegex(
1460+
ValueError,
1461+
"--keep_columns must be a comma-separated list of non-empty column names",
1462+
):
1463+
cli.keep_columns()
1464+
1465+
def test_keep_columns_raises_for_empty_string(self) -> None:
1466+
"""Test keep_columns rejects an explicitly provided empty string."""
1467+
args = Namespace(keep_columns="")
1468+
cli = BalanceCLI(args)
1469+
with self.assertRaisesRegex(
1470+
ValueError,
1471+
"--keep_columns must be a comma-separated list of non-empty column names",
1472+
):
1473+
cli.keep_columns()
1474+
14491475
def test_has_keep_columns_with_keep_columns(self) -> None:
14501476
"""Test has_keep_columns returns True when keep_columns is set."""
14511477
args = Namespace(keep_columns="id,weight")
@@ -1493,6 +1519,47 @@ def test_check_input_columns_raises_when_keep_column_missing(self) -> None:
14931519
with self.assertRaises(AssertionError):
14941520
cli.check_input_columns(columns)
14951521

1522+
1523+
class TestBalanceCLI_csv_column_parsing(balance.testutil.BalanceTestCase):
1524+
"""Test normalized parsing of comma-separated column arguments."""
1525+
1526+
def test_covariate_columns_strip_whitespace(self) -> None:
1527+
args = Namespace(covariate_columns=" a, b ,c ")
1528+
cli = BalanceCLI(args)
1529+
self.assertEqual(cli.covariate_columns(), ["a", "b", "c"])
1530+
1531+
def test_outcome_columns_raise_for_empty_name(self) -> None:
1532+
args = Namespace(outcome_columns="y,,z")
1533+
cli = BalanceCLI(args)
1534+
with self.assertRaisesRegex(
1535+
ValueError,
1536+
"--outcome_columns must be a comma-separated list of non-empty column names",
1537+
):
1538+
cli.outcome_columns()
1539+
1540+
def test_outcome_columns_raise_for_empty_string(self) -> None:
1541+
args = Namespace(outcome_columns="")
1542+
cli = BalanceCLI(args)
1543+
with self.assertRaisesRegex(
1544+
ValueError,
1545+
"--outcome_columns must be a comma-separated list of non-empty column names",
1546+
):
1547+
cli.outcome_columns()
1548+
1549+
def test_batch_columns_raise_for_empty_name(self) -> None:
1550+
args = Namespace(batch_columns="region,")
1551+
cli = BalanceCLI(args)
1552+
with self.assertRaisesRegex(
1553+
ValueError,
1554+
"--batch_columns must be a comma-separated list of non-empty column names",
1555+
):
1556+
cli.batch_columns()
1557+
1558+
def test_covariate_columns_for_diagnostics_strip_whitespace(self) -> None:
1559+
args = Namespace(covariate_columns_for_diagnostics=" x, y ")
1560+
cli = BalanceCLI(args)
1561+
self.assertEqual(cli.covariate_columns_for_diagnostics(), ["x", "y"])
1562+
14961563
def test_keep_columns_preserved_in_adjusted_output(self) -> None:
14971564
"""Test that --keep_columns columns survive adjustment via ignore_columns.
14981565

0 commit comments

Comments
 (0)