[+] add stop_at parameter to parse_args for subcommand routing, bump pr34 v0.1.5.69

1. add stop_at param to commands_typed/argparse.py parse_args();
  2. when a token from stop_at is found, parsing stops and remainder is returned inclusive;
  3. enables main parser to stop before subcommand so -h passes through to subcommand;
  4. add test_argparse.py with 10 tests: default, double-dash, stop_at, edge cases;
  5. add test_argparse to pr34 test_names config;
  6. bump pr34 to v0.1.5.69;
This commit is contained in:
LLM 2026-04-17 09:00:00 +00:00
parent c491da0bb9
commit 2dac844087
3 changed files with 104 additions and 2 deletions

@ -11,7 +11,20 @@ from typing import (
def parse_args(
parser: argparse.ArgumentParser,
args: Optional[list[str]] = None,
stop_at: Optional[list[str]] = None,
) -> tuple[argparse.Namespace, list[str]]:
"""Parse args with support for early termination.
stop_at: list of tokens that act like '--' when encountered.
When any token from stop_at is found in args, parsing stops
at that position: everything from that token onward (inclusive)
goes into the returned remainder list, and the parser only
sees args before it.
This allows e.g. a main parser with --log-level to stop at
a subcommand name like 'cve', so that 'cve -h' is not consumed
by the main parser.
"""
if args is None:
args = sys.argv[1:]
@ -20,9 +33,12 @@ def parse_args(
for i, o in enumerate(args):
if o == '--':
argv.extend(args[i + 1 :])
del args[i:]
break
if stop_at is not None and o in stop_at:
argv.extend(args[i:])
del args[i:]
break
return parser.parse_args(args), argv

@ -0,0 +1,85 @@
import argparse
import unittest
from ..argparse import parse_args
class TestParseArgsDefault(unittest.TestCase):
def test_basic(self) -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--foo', default='bar')
opts, rest = parse_args(parser, ['--foo', 'baz'])
self.assertEqual(opts.foo, 'baz')
self.assertEqual(rest, [])
def test_double_dash(self) -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--foo', default='bar')
opts, rest = parse_args(parser, ['--foo', 'baz', '--', 'extra1', 'extra2'])
self.assertEqual(opts.foo, 'baz')
self.assertEqual(rest, ['extra1', 'extra2'])
def test_no_args(self) -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--x', default='1')
opts, rest = parse_args(parser, [])
self.assertEqual(opts.x, '1')
self.assertEqual(rest, [])
class TestStopAt(unittest.TestCase):
def test_stop_at_command(self) -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--log-level', default='INFO')
opts, rest = parse_args(
parser, ['--log-level', 'DEBUG', 'cve', '-h'], stop_at=['cve', 'compile']
)
self.assertEqual(opts.log_level, 'DEBUG')
self.assertEqual(rest, ['cve', '-h'])
def test_stop_at_preserves_all_after(self) -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', action='store_true')
opts, rest = parse_args(
parser,
['--verbose', 'download', '--progress', '-j', '4'],
stop_at=['download', 'compile'],
)
self.assertTrue(opts.verbose)
self.assertEqual(rest, ['download', '--progress', '-j', '4'])
def test_stop_at_first_arg(self) -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--x', default='1')
opts, rest = parse_args(parser, ['cve', 'sync', '--cache-dir', '/tmp'], stop_at=['cve'])
self.assertEqual(opts.x, '1')
self.assertEqual(rest, ['cve', 'sync', '--cache-dir', '/tmp'])
def test_stop_at_no_match(self) -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--x', default='1')
opts, rest = parse_args(parser, ['--x', '2'], stop_at=['cve'])
self.assertEqual(opts.x, '2')
self.assertEqual(rest, [])
def test_stop_at_with_double_dash(self) -> None:
"""Double dash takes priority over stop_at."""
parser = argparse.ArgumentParser()
parser.add_argument('--x', default='1')
opts, rest = parse_args(parser, ['--x', '2', '--', 'cve', '-h'], stop_at=['cve'])
self.assertEqual(opts.x, '2')
self.assertEqual(rest, ['cve', '-h'])
def test_stop_at_help_not_consumed_by_main(self) -> None:
"""The key use case: -h after command goes to subcommand, not main."""
parser = argparse.ArgumentParser()
parser.add_argument('--log-level', default='INFO')
opts, rest = parse_args(parser, ['cve', '-h'], stop_at=['cve'])
self.assertEqual(rest, ['cve', '-h'])
def test_stop_at_empty_stop_list(self) -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--x', default='1')
opts, rest = parse_args(parser, ['--x', '2'], stop_at=[])
self.assertEqual(opts.x, '2')
self.assertEqual(rest, [])

@ -9,7 +9,7 @@ classifiers = [
]
name = 'online.fxreader.pr34'
version = '0.1.5.68'
version = '0.1.5.69'
dynamic = []
dependencies = [
@ -86,6 +86,7 @@ modules = [
search_paths = ['.']
test_names = [
'online.fxreader.pr34.commands_typed.tests',
'online.fxreader.pr34.commands_typed.tests.test_argparse',
]
discovery_paths = ['.']