From e6f5d68917338cb4023267e74fc65563185c945f Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Sat, 14 Mar 2026 08:36:19 +0100 Subject: [PATCH 1/4] Document `fromfile_prefix_chars` in usage. `argparse.ArgumentParser(fromfile_prefix_chars)` allows specifying a filename prefixed by any of the characters included in the string. `fromfile_prefix_chars='@'` means `@file.txt` is interpreted not as a regular positional argument, but as an instruction to read `file.txt` for additional arguments (positional or named.) For now, only the first character in the string is documented such that the usage instructions don't get overloaded. --- sphinxarg/ext.py | 6 +++++- sphinxarg/parser.py | 1 + test/sample-directive-special.py | 1 + test/test_default_html.py | 1 + test/utils/xpath.py | 8 ++------ 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/sphinxarg/ext.py b/sphinxarg/ext.py index 847a187..e0e6a94 100644 --- a/sphinxarg/ext.py +++ b/sphinxarg/ext.py @@ -821,7 +821,11 @@ def run(self): domain = cast('ArgParseDomain', self.env.get_domain(ArgParseDomain.name)) domain.add_argparse_command(result, node_id, self.index_groups) - items.append(nodes.literal_block(text=result['usage'])) + usage = result['usage'] + if fromfile_prefix := result.get('fromfile_prefix_chars', ''): + usage += ' [{}file]'.format(fromfile_prefix[0]) + items.append(nodes.literal_block(text=usage)) + items.extend( self._print_action_groups( result, diff --git a/sphinxarg/parser.py b/sphinxarg/parser.py index dd18457..9a07f5a 100644 --- a/sphinxarg/parser.py +++ b/sphinxarg/parser.py @@ -76,6 +76,7 @@ def parse_parser(parser, data=None, **kwargs): } _try_add_parser_attribute(data, parser, 'description') _try_add_parser_attribute(data, parser, 'epilog') + _try_add_parser_attribute(data, parser, 'fromfile_prefix_chars') for action in parser._get_positional_actions(): if not isinstance(action, _SubParsersAction): continue diff --git a/test/sample-directive-special.py b/test/sample-directive-special.py index d7ff97f..2a4780e 100644 --- a/test/sample-directive-special.py +++ b/test/sample-directive-special.py @@ -5,6 +5,7 @@ def get_parser(): parser = argparse.ArgumentParser( prog='sample-directive-special', description='Support SphinxArgParse HTML testing (with defaults)', + fromfile_prefix_chars='@', ) parser.add_argument( diff --git a/test/test_default_html.py b/test/test_default_html.py index 27dc351..7644a2b 100644 --- a/test/test_default_html.py +++ b/test/test_default_html.py @@ -60,6 +60,7 @@ ('.//section/dl/dd/p/code/span', '420'), ('.//section/dl/dd/p/code/span', "'*.rst"), ('.//section/dl/dd/p/code/span', r"\['\*.rst',"), + (".//div[@class='highlight']", r'\[@file\]'), ], ), ( diff --git a/test/utils/xpath.py b/test/utils/xpath.py index e7f3ce4..6c74475 100644 --- a/test/utils/xpath.py +++ b/test/utils/xpath.py @@ -1,4 +1,5 @@ import re +from lxml import etree as lxmletree def check_xpath(etree, fname, path, check, be_found=True): @@ -16,12 +17,7 @@ def check_xpath(etree, fname, path, check, be_found=True): else: def get_text(node): - if node.text is not None: - # the node has only one text - return node.text - else: - # the node has tags and text; gather texts just under the node - return ''.join(n.tail or '' for n in node) + return lxmletree.tostring(node, encoding='unicode', method='text') rex = re.compile(check) if be_found: From 7e4ea4a43cebdd6a6c1b1bc68cc203ef791f456b Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Sat, 14 Mar 2026 08:33:28 +0100 Subject: [PATCH 2/4] Document all `fromfile_prefix_chars`. Add a separate paragraph *iff* `fromfile_prefix_chars` was more than one character to enumerate all possible prefixes. --- sphinxarg/ext.py | 13 ++++++++++++- test/sample-default-supressed.py | 3 ++- test/test_default_html.py | 3 +++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/sphinxarg/ext.py b/sphinxarg/ext.py index e0e6a94..4c17364 100644 --- a/sphinxarg/ext.py +++ b/sphinxarg/ext.py @@ -821,11 +821,22 @@ def run(self): domain = cast('ArgParseDomain', self.env.get_domain(ArgParseDomain.name)) domain.add_argparse_command(result, node_id, self.index_groups) + fromfile_prefix = result.get('fromfile_prefix_chars', '') + usage = result['usage'] - if fromfile_prefix := result.get('fromfile_prefix_chars', ''): + if fromfile_prefix: usage += ' [{}file]'.format(fromfile_prefix[0]) items.append(nodes.literal_block(text=usage)) + if len(fromfile_prefix) > 1: + children = [nodes.Text('Additional arguments will be read from files passed as ')] + for i, p in enumerate(fromfile_prefix): + if i > 0: + children.append(nodes.Text(' or ')) + children.append(nodes.literal(text='{}file'.format(p))) + children.append(nodes.Text('.')) + items.append(nodes.paragraph('', '', *children)) + items.extend( self._print_action_groups( result, diff --git a/test/sample-default-supressed.py b/test/sample-default-supressed.py index ab4a72d..2721303 100644 --- a/test/sample-default-supressed.py +++ b/test/sample-default-supressed.py @@ -3,7 +3,8 @@ def get_parser(): parser = ArgumentParser( - prog='sample-default-suppressed', description='Test suppression of version default' + prog='sample-default-suppressed', description='Test suppression of version default', + fromfile_prefix_chars='=@', ) parser.add_argument( '--version', help='print version number', action='version', version='1.2.3' diff --git a/test/test_default_html.py b/test/test_default_html.py index 7644a2b..0d55a49 100644 --- a/test/test_default_html.py +++ b/test/test_default_html.py @@ -61,6 +61,7 @@ ('.//section/dl/dd/p/code/span', "'*.rst"), ('.//section/dl/dd/p/code/span', r"\['\*.rst',"), (".//div[@class='highlight']", r'\[@file\]'), + ('.//p', 'Additional arguments will be read from files', False), ], ), ( @@ -70,6 +71,8 @@ ('.//h1', 'Default suppressed'), ('.//h2', 'Named Arguments'), ('.//section/dl/dd/p', 'Default', False), + (".//div[@class='highlight']", r'\[=file\]'), + ('.//p', 'Additional arguments will be read from files passed as =file or @file.'), ], ), ], From f656045440316d2a061457b9b4363a667f632caa Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Sat, 14 Mar 2026 20:27:41 +0100 Subject: [PATCH 3/4] Fix style. Linter fixes by `ruff`. --- sphinxarg/ext.py | 4 ++-- test/test_default_html.py | 5 ++++- test/utils/xpath.py | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/sphinxarg/ext.py b/sphinxarg/ext.py index 4c17364..3a286c0 100644 --- a/sphinxarg/ext.py +++ b/sphinxarg/ext.py @@ -825,7 +825,7 @@ def run(self): usage = result['usage'] if fromfile_prefix: - usage += ' [{}file]'.format(fromfile_prefix[0]) + usage += f' [{fromfile_prefix[0]}file]' items.append(nodes.literal_block(text=usage)) if len(fromfile_prefix) > 1: @@ -833,7 +833,7 @@ def run(self): for i, p in enumerate(fromfile_prefix): if i > 0: children.append(nodes.Text(' or ')) - children.append(nodes.literal(text='{}file'.format(p))) + children.append(nodes.literal(text=f'{p}file')) children.append(nodes.Text('.')) items.append(nodes.paragraph('', '', *children)) diff --git a/test/test_default_html.py b/test/test_default_html.py index 0d55a49..2beb2ff 100644 --- a/test/test_default_html.py +++ b/test/test_default_html.py @@ -72,7 +72,10 @@ ('.//h2', 'Named Arguments'), ('.//section/dl/dd/p', 'Default', False), (".//div[@class='highlight']", r'\[=file\]'), - ('.//p', 'Additional arguments will be read from files passed as =file or @file.'), + ( + './/p', + 'Additional arguments will be read from files passed as =file or @file.', + ), ], ), ], diff --git a/test/utils/xpath.py b/test/utils/xpath.py index 6c74475..1260bdc 100644 --- a/test/utils/xpath.py +++ b/test/utils/xpath.py @@ -1,4 +1,5 @@ import re + from lxml import etree as lxmletree From fb60a2d670318326a1a4c5e2f97105cba7767f44 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Sat, 14 Mar 2026 20:33:05 +0100 Subject: [PATCH 4/4] Fix formatting. Linter fixes by `ruff format`. --- test/sample-default-supressed.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/sample-default-supressed.py b/test/sample-default-supressed.py index 2721303..25faf86 100644 --- a/test/sample-default-supressed.py +++ b/test/sample-default-supressed.py @@ -3,7 +3,8 @@ def get_parser(): parser = ArgumentParser( - prog='sample-default-suppressed', description='Test suppression of version default', + prog='sample-default-suppressed', + description='Test suppression of version default', fromfile_prefix_chars='=@', ) parser.add_argument(