Skip to content

Commit a0cafcd

Browse files
committed
support reading Argparse instances inside classes
So far, shphinxarg only has support for reading/importing Argparse instances from global variables/attributes within a module. However, there are use cases where Argparse is used inside classes of a module, not as a global variable. This is particularly the case for uses of 'argparse' in the context of CLI / REPL style user interfaces, such as for example those built using the 'cmd2' module. This change introduces the ability to specify a path in ':func:' using the '.'-notation (e.g. 'PysimApp.bulk_script_parser'). Initial patch by: Harald Welte <laforge@osmocom.org> Co-authored by: Vadim Yanitskiy <fixeria@osmocom.org>
1 parent 2a2a202 commit a0cafcd

File tree

4 files changed

+72
-16
lines changed

4 files changed

+72
-16
lines changed

sphinxarg/ext.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,28 @@ def _open_filename(self):
486486
# raise exception
487487
raise FileNotFoundError(self.options['filename'])
488488

489+
def _get_parser(self, obj, path):
490+
for attr in path.split('.'):
491+
try:
492+
if isinstance(obj, dict):
493+
obj = obj[attr]
494+
else:
495+
obj = getattr(obj, attr)
496+
except (KeyError, AttributeError) as exc:
497+
msg = (
498+
f'"{obj}" has no key/attribute "{attr} (path: {path})"\n'
499+
f'Incorrect argparse :module: or :func: values?'
500+
)
501+
raise self.error(msg) from exc
502+
if isinstance(obj, ArgumentParser):
503+
parser = obj
504+
elif 'passparser' in self.options:
505+
parser = ArgumentParser()
506+
obj(parser)
507+
else:
508+
parser = obj()
509+
return parser
510+
489511
def run(self):
490512
if 'module' in self.options and 'func' in self.options:
491513
module_name = self.options['module']
@@ -501,7 +523,7 @@ def run(self):
501523
exec(code, mod)
502524
module_name = None
503525
attr_name = self.options['func']
504-
func = mod[attr_name]
526+
parser = self._get_parser(mod, attr_name)
505527
else:
506528
msg = ':module: and :func: should be specified, or :ref:, or :filename: and :func:'
507529
raise self.error(msg)
@@ -517,22 +539,8 @@ def run(self):
517539
f'{sys.exc_info()[1]}'
518540
)
519541
raise self.error(msg) from exc
542+
parser = self._get_parser(mod, attr_name)
520543

521-
if not hasattr(mod, attr_name):
522-
msg = (
523-
f'Module "{module_name}" has no attribute "{attr_name}"\n'
524-
f'Incorrect argparse :module: or :func: values?'
525-
)
526-
raise self.error(msg)
527-
func = getattr(mod, attr_name)
528-
529-
if isinstance(func, ArgumentParser):
530-
parser = func
531-
elif 'passparser' in self.options:
532-
parser = ArgumentParser()
533-
func(parser)
534-
else:
535-
parser = func()
536544
if 'path' not in self.options:
537545
self.options['path'] = ''
538546
path = str(self.options['path'])
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Argparse inside class Foo
2+
#########################
3+
4+
.. argparse::
5+
:filename: test/sample-inside-class.py
6+
:prog: sample-inside-class-foo
7+
:func: Foo.parser
8+
9+
Argparse inside class Foo.Bar
10+
#############################
11+
12+
.. argparse::
13+
:filename: test/sample-inside-class.py
14+
:prog: sample-inside-class-foo-bar
15+
:func: Foo.Bar.parser

test/sample-inside-class.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import argparse
2+
3+
4+
desc = 'Test parsing of Argparse instances inside classes'
5+
6+
class Foo:
7+
parser = argparse.ArgumentParser(prog=f'{__name__}-foo',
8+
description=desc)
9+
parser.add_argument('--foo-arg1', help='foo-arg1 help')
10+
parser.add_argument('--foo-arg2', help='foo-arg2 help')
11+
12+
13+
class Bar:
14+
parser = argparse.ArgumentParser(prog=f'{__name__}-foo-bar',
15+
description=desc)
16+
parser.add_argument('--foo-bar-arg1', help='foo-bar-arg1 help')
17+
parser.add_argument('--foo-bar-arg2', help='foo-bar-arg2 help')

test/test_default_html.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,22 @@ def get_text(node):
9898
('.//section/dl/dd/p', 'Default', False),
9999
],
100100
),
101+
(
102+
'inside-class.html',
103+
[
104+
(".//section[@id='argparse-inside-class-foo']/h1", 'Argparse inside class Foo'),
105+
(".//section[@id='argparse-inside-class-foo']//div[@class='highlight']//span", 'usage'),
106+
(".//section[@id='Foo.parser-named-arguments']/h2", 'Named Arguments'),
107+
(".//section[@id='Foo.parser-named-arguments']/dl/dt[1]/kbd", '--foo-arg1'),
108+
(".//section[@id='Foo.parser-named-arguments']/dl/dt[2]/kbd", '--foo-arg2'),
109+
110+
(".//section[@id='argparse-inside-class-foo-bar']/h1", 'Argparse inside class Foo.Bar'),
111+
(".//section[@id='argparse-inside-class-foo-bar']//div[@class='highlight']//span", 'usage'),
112+
(".//section[@id='Foo.Bar.parser-named-arguments']/h2", 'Named Arguments'),
113+
(".//section[@id='Foo.Bar.parser-named-arguments']/dl/dt[1]/kbd", '--foo-bar-arg1'),
114+
(".//section[@id='Foo.Bar.parser-named-arguments']/dl/dt[2]/kbd", '--foo-bar-arg2'),
115+
],
116+
),
101117
],
102118
)
103119
@pytest.mark.sphinx('html', testroot='default-html')

0 commit comments

Comments
 (0)