Skip to content

Commit

Permalink
support reading Argparse instances inside classes
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
laf0rge committed Jul 31, 2024
1 parent 2a2a202 commit a0cafcd
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 16 deletions.
40 changes: 24 additions & 16 deletions sphinxarg/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,28 @@ def _open_filename(self):
# raise exception
raise FileNotFoundError(self.options['filename'])

def _get_parser(self, obj, path):
for attr in path.split('.'):
try:
if isinstance(obj, dict):
obj = obj[attr]
else:
obj = getattr(obj, attr)
except (KeyError, AttributeError) as exc:
msg = (

Check warning on line 497 in sphinxarg/ext.py

View check run for this annotation

Codecov / codecov/patch

sphinxarg/ext.py#L496-L497

Added lines #L496 - L497 were not covered by tests
f'"{obj}" has no key/attribute "{attr} (path: {path})"\n'
f'Incorrect argparse :module: or :func: values?'
)
raise self.error(msg) from exc

Check failure on line 501 in sphinxarg/ext.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (PERF203)

sphinxarg/ext.py:496:13: PERF203 `try`-`except` within a loop incurs performance overhead

Check warning on line 501 in sphinxarg/ext.py

View check run for this annotation

Codecov / codecov/patch

sphinxarg/ext.py#L501

Added line #L501 was not covered by tests
if isinstance(obj, ArgumentParser):
parser = obj
elif 'passparser' in self.options:
parser = ArgumentParser()
obj(parser)

Check warning on line 506 in sphinxarg/ext.py

View check run for this annotation

Codecov / codecov/patch

sphinxarg/ext.py#L505-L506

Added lines #L505 - L506 were not covered by tests
else:
parser = obj()
return parser

def run(self):
if 'module' in self.options and 'func' in self.options:
module_name = self.options['module']
Expand All @@ -501,7 +523,7 @@ def run(self):
exec(code, mod)
module_name = None
attr_name = self.options['func']
func = mod[attr_name]
parser = self._get_parser(mod, attr_name)
else:
msg = ':module: and :func: should be specified, or :ref:, or :filename: and :func:'
raise self.error(msg)
Expand All @@ -517,22 +539,8 @@ def run(self):
f'{sys.exc_info()[1]}'
)
raise self.error(msg) from exc
parser = self._get_parser(mod, attr_name)

Check warning on line 542 in sphinxarg/ext.py

View check run for this annotation

Codecov / codecov/patch

sphinxarg/ext.py#L542

Added line #L542 was not covered by tests

if not hasattr(mod, attr_name):
msg = (
f'Module "{module_name}" has no attribute "{attr_name}"\n'
f'Incorrect argparse :module: or :func: values?'
)
raise self.error(msg)
func = getattr(mod, attr_name)

if isinstance(func, ArgumentParser):
parser = func
elif 'passparser' in self.options:
parser = ArgumentParser()
func(parser)
else:
parser = func()
if 'path' not in self.options:
self.options['path'] = ''
path = str(self.options['path'])
Expand Down
15 changes: 15 additions & 0 deletions test/roots/test-default-html/inside-class.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Argparse inside class Foo
#########################

.. argparse::
:filename: test/sample-inside-class.py
:prog: sample-inside-class-foo
:func: Foo.parser

Argparse inside class Foo.Bar
#############################

.. argparse::
:filename: test/sample-inside-class.py
:prog: sample-inside-class-foo-bar
:func: Foo.Bar.parser
17 changes: 17 additions & 0 deletions test/sample-inside-class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import argparse

Check failure on line 1 in test/sample-inside-class.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (N999)

test/sample-inside-class.py:1:1: N999 Invalid module name: 'sample-inside-class'


desc = 'Test parsing of Argparse instances inside classes'

Check failure on line 4 in test/sample-inside-class.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

test/sample-inside-class.py:1:1: I001 Import block is un-sorted or un-formatted

class Foo:

Check failure on line 6 in test/sample-inside-class.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E302)

test/sample-inside-class.py:6:1: E302 Expected 2 blank lines, found 1
parser = argparse.ArgumentParser(prog=f'{__name__}-foo',
description=desc)
parser.add_argument('--foo-arg1', help='foo-arg1 help')
parser.add_argument('--foo-arg2', help='foo-arg2 help')


class Bar:

Check failure on line 13 in test/sample-inside-class.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E303)

test/sample-inside-class.py:13:5: E303 Too many blank lines (2)
parser = argparse.ArgumentParser(prog=f'{__name__}-foo-bar',
description=desc)
parser.add_argument('--foo-bar-arg1', help='foo-bar-arg1 help')
parser.add_argument('--foo-bar-arg2', help='foo-bar-arg2 help')
16 changes: 16 additions & 0 deletions test/test_default_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ def get_text(node):
('.//section/dl/dd/p', 'Default', False),
],
),
(
'inside-class.html',
[
(".//section[@id='argparse-inside-class-foo']/h1", 'Argparse inside class Foo'),

Check failure on line 104 in test/test_default_html.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

test/test_default_html.py:104:96: E501 Line too long (96 > 95)
(".//section[@id='argparse-inside-class-foo']//div[@class='highlight']//span", 'usage'),

Check failure on line 105 in test/test_default_html.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

test/test_default_html.py:105:96: E501 Line too long (104 > 95)
(".//section[@id='Foo.parser-named-arguments']/h2", 'Named Arguments'),
(".//section[@id='Foo.parser-named-arguments']/dl/dt[1]/kbd", '--foo-arg1'),
(".//section[@id='Foo.parser-named-arguments']/dl/dt[2]/kbd", '--foo-arg2'),
(".//section[@id='argparse-inside-class-foo-bar']/h1", 'Argparse inside class Foo.Bar'),

Check failure on line 110 in test/test_default_html.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

test/test_default_html.py:110:96: E501 Line too long (104 > 95)
(".//section[@id='argparse-inside-class-foo-bar']//div[@class='highlight']//span", 'usage'),

Check failure on line 111 in test/test_default_html.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

test/test_default_html.py:111:96: E501 Line too long (108 > 95)
(".//section[@id='Foo.Bar.parser-named-arguments']/h2", 'Named Arguments'),
(".//section[@id='Foo.Bar.parser-named-arguments']/dl/dt[1]/kbd", '--foo-bar-arg1'),

Check failure on line 113 in test/test_default_html.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

test/test_default_html.py:113:96: E501 Line too long (100 > 95)
(".//section[@id='Foo.Bar.parser-named-arguments']/dl/dt[2]/kbd", '--foo-bar-arg2'),
],
),
],
)
@pytest.mark.sphinx('html', testroot='default-html')
Expand Down

0 comments on commit a0cafcd

Please sign in to comment.