[BugFix] CLI parser splits comma-separated flag values into separate args (#7420)

* fix: preserve comma-separated values for flagged CLI arguments

The CLI argument parser was splitting all comma-separated values into
separate positional args before argparse could process them. This caused
multi-symbol queries like --symbol AAPL,MSFT,GOOGL to fail with
'args couldn't be interpreted' for all symbols after the first.

Flag values are now identified by checking whether the preceding token
is a known option string with nargs != 0, and their commas are preserved
so the provider receives the original comma-separated string.

* test: add coverage for comma-split fix in parse_known_args_and_warn

---------

Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>
This commit is contained in:
John Seong
2026-03-22 13:00:08 -07:00
committed by GitHub
parent 3c105ba9d3
commit 86ec79d75f
2 changed files with 151 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
"""Test the base controller."""
import argparse
from unittest.mock import MagicMock, patch
import pytest
@@ -77,3 +78,114 @@ def test_call_exit():
with patch.object(controller, "save_class", MagicMock()):
controller.queue = ["quit"]
controller.call_exit(None)
@pytest.fixture
def mock_base_session():
"""Mock the session for parse_known_args_and_warn tests."""
with patch("openbb_cli.controllers.base_controller.session") as mock_session:
mock_session.settings.USE_CLEAR_AFTER_CMD = False
yield mock_session
def _make_parser(*args_spec):
"""Create an argparse parser from a list of add_argument kwargs."""
parser = argparse.ArgumentParser(add_help=False)
for spec in args_spec:
flags = spec.pop("flags")
parser.add_argument(*flags, **spec)
return parser
def test_comma_split_flagged_value_not_split(mock_base_session):
"""Simple test: --symbol AAPL,MSFT must stay as one value."""
parser = _make_parser({"flags": ["--symbol", "-s"], "dest": "symbol", "type": str})
result = BaseController.parse_known_args_and_warn(parser, ["--symbol", "AAPL,MSFT"])
assert result is not None
assert result.symbol == "AAPL,MSFT"
def test_comma_split_short_flag_not_split(mock_base_session):
"""Short flag -s AAPL,MSFT must also stay as one value."""
parser = _make_parser({"flags": ["--symbol", "-s"], "dest": "symbol", "type": str})
result = BaseController.parse_known_args_and_warn(parser, ["-s", "AAPL,MSFT"])
assert result is not None
assert result.symbol == "AAPL,MSFT"
def test_comma_split_equals_syntax_not_split(mock_base_session):
"""--symbol=AAPL,MSFT must not be split."""
parser = _make_parser({"flags": ["--symbol", "-s"], "dest": "symbol", "type": str})
result = BaseController.parse_known_args_and_warn(parser, ["--symbol=AAPL,MSFT"])
assert result is not None
assert result.symbol == "AAPL,MSFT"
def test_comma_split_nargs_plus_all_values_protected(mock_base_session):
"""nargs='+': all consecutive values after --symbols are protected."""
parser = _make_parser(
{"flags": ["--symbols"], "dest": "symbols", "nargs": "+", "type": str}
)
result = BaseController.parse_known_args_and_warn(
parser, ["--symbols", "AAPL,MSFT", "GOOG,AMZN"]
)
assert result is not None
assert result.symbols == ["AAPL,MSFT", "GOOG,AMZN"]
def test_comma_split_nargs_star_values_protected(mock_base_session):
"""nargs='*': consecutive values after --tags are protected."""
parser = _make_parser(
{"flags": ["--tags"], "dest": "tags", "nargs": "*", "type": str}
)
result = BaseController.parse_known_args_and_warn(parser, ["--tags", "a,b", "c,d"])
assert result is not None
assert result.tags == ["a,b", "c,d"]
def test_comma_split_nargs_int_values_protected(mock_base_session):
"""nargs=2: both values after --pair are protected."""
parser = _make_parser(
{"flags": ["--pair"], "dest": "pair", "nargs": 2, "type": str}
)
result = BaseController.parse_known_args_and_warn(parser, ["--pair", "a,b", "c,d"])
assert result is not None
assert result.pair == ["a,b", "c,d"]
def test_comma_split_store_true_not_confused(mock_base_session):
"""store_true flags (nargs=0) should not protect the next token."""
parser = _make_parser(
{"flags": ["--symbol", "-s"], "dest": "symbol", "type": str},
{"flags": ["--raw"], "dest": "raw", "action": "store_true", "default": False},
)
result = BaseController.parse_known_args_and_warn(
parser, ["--raw", "--symbol", "AAPL,MSFT"]
)
assert result is not None
assert result.raw is True
assert result.symbol == "AAPL,MSFT"
def test_comma_split_no_comma_values_unchanged(mock_base_session):
"""Values without commas pass through unaffected."""
parser = _make_parser({"flags": ["--symbol", "-s"], "dest": "symbol", "type": str})
result = BaseController.parse_known_args_and_warn(parser, ["--symbol", "AAPL"])
assert result is not None
assert result.symbol == "AAPL"
def test_comma_split_multiple_flags_each_protected(mock_base_session):
"""Multiple flags each protect their own values independently."""
parser = _make_parser(
{"flags": ["--symbol", "-s"], "dest": "symbol", "type": str},
{"flags": ["--raw"], "dest": "raw", "action": "store_true", "default": False},
{"flags": ["--provider"], "dest": "provider", "type": str},
)
result = BaseController.parse_known_args_and_warn(
parser,
["--symbol", "AAPL,MSFT", "--provider", "yfinance,polygon"],
)
assert result is not None
assert result.symbol == "AAPL,MSFT"
assert result.provider == "yfinance,polygon"