diff --git a/.ruff.toml b/.ruff.toml index cdbd5d6..fd49386 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -39,6 +39,9 @@ ignore = [ "test/sample-directive-special.py" = [ "N999", # invalid module name ] +"test/sample-inside-class.py" = [ + "N999", # invalid module name +] [format] preview = true diff --git a/sphinxarg/ext.py b/sphinxarg/ext.py index 847a187..67049be 100644 --- a/sphinxarg/ext.py +++ b/sphinxarg/ext.py @@ -719,6 +719,28 @@ def _is_suppressed(item: str | None) -> bool: item = str(item).replace('"', '').replace("'", '') return item == '==SUPPRESS==' + def _get_parser(self, obj, path): + try: + for attr in path.split('.'): + if isinstance(obj, dict): + obj = obj[attr] + else: + obj = getattr(obj, attr) + except (KeyError, AttributeError) as exc: + msg = ( + f'"{obj}" has no key/attribute "{attr} (path: {path})"\n' + f'Incorrect argparse :module: or :func: values?' + ) + raise self.error(msg) from exc + if isinstance(obj, ArgumentParser): + parser = obj + elif 'passparser' in self.options: + parser = ArgumentParser() + obj(parser) + else: + parser = obj() + return parser + def run(self): if 'module' in self.options and 'func' in self.options: module_name = self.options['module'] @@ -734,7 +756,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) @@ -750,22 +772,8 @@ def run(self): f'{sys.exc_info()[1]}' ) raise self.error(msg) from exc + parser = self._get_parser(mod, attr_name) - 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']) diff --git a/test/roots/test-default-html/inside-class.rst b/test/roots/test-default-html/inside-class.rst new file mode 100644 index 0000000..d0d4566 --- /dev/null +++ b/test/roots/test-default-html/inside-class.rst @@ -0,0 +1,15 @@ +Argparse inside class Foo +######################### + +.. argparse:: + :filename: sample-inside-class.py + :prog: sample-inside-class-foo + :func: Foo.parser + +Argparse inside class Foo.Bar +############################# + +.. argparse:: + :filename: sample-inside-class.py + :prog: sample-inside-class-foo-bar + :func: Foo.Bar.parser diff --git a/test/sample-inside-class.py b/test/sample-inside-class.py new file mode 100644 index 0000000..19eaa54 --- /dev/null +++ b/test/sample-inside-class.py @@ -0,0 +1,14 @@ +import argparse + +desc = 'Test parsing of Argparse instances inside classes' + + +class Foo: + 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: + 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') diff --git a/test/test_default_html.py b/test/test_default_html.py index 27dc351..701bcda 100644 --- a/test/test_default_html.py +++ b/test/test_default_html.py @@ -71,6 +71,51 @@ ('.//section/dl/dd/p', 'Default', False), ], ), + ( + 'inside-class.html', + [ + ( + ".//section[@id='argparse-inside-class-foo']/h1", + 'Argparse inside class Foo', + ), + ( + ".//section[@id='argparse-inside-class-foo']//div[@class='highlight']//span", + 'usage', + ), + ( + ".//section[@id='sample-inside-class-foo-named-arguments']/h2", + 'Named Arguments', + ), + ( + ".//section[@id='sample-inside-class-foo-named-arguments']/dl/dt[1]/kbd", + '--foo-arg1', + ), + ( + ".//section[@id='sample-inside-class-foo-named-arguments']/dl/dt[2]/kbd", + '--foo-arg2', + ), + ( + ".//section[@id='argparse-inside-class-foo-bar']/h1", + 'Argparse inside class Foo.Bar', + ), + ( + ".//section[@id='argparse-inside-class-foo-bar']//div[@class='highlight']//span", + 'usage', + ), + ( + ".//section[@id='sample-inside-class-foo-bar-named-arguments']/h2", + 'Named Arguments', + ), + ( + ".//section[@id='sample-inside-class-foo-bar-named-arguments']/dl/dt[1]/kbd", + '--foo-bar-arg1', + ), + ( + ".//section[@id='sample-inside-class-foo-bar-named-arguments']/dl/dt[2]/kbd", + '--foo-bar-arg2', + ), + ], + ), ], ) @pytest.mark.sphinx('html', testroot='default-html')