1
\$\begingroup\$

I have a Python module that will accept many arguments. Rather than having to have a big long list, I've decided to use classes to represent argument groups and arguments

When initialised, the Parser class creates an instance of the argparse.ArgumentParser class and then adds the argument groups and arguments. The arguments can then be parsed by the parse_args() method of the Parser class.

I'm pretty new to Python and looking for some guidance or affirmation around whether or not a class method is suitable for this use case, so any comments would be appreciated. Equally, if it's totally the wrong implementation I'd be grateful for any tips.

Note that in reality there are more argument groups and arguments, but the code below works.

from argparse import ArgumentParser

class Parser:
    def __init__(self) -> None:
        self._parser = ArgumentParser()
        [cls.add_group(self._parser) for cls in ArgumentGroup.__subclasses__()]
        [cls.add_arg(self._parser) for cls in Argument.__subclasses__()]

    def parse_args(self) -> dict:
        return vars(self._parser.parse_args())


#****************
# Argument Groups
#****************

class ArgumentGroup:
    _description = None # Description is optional, so a default is required.

    @classmethod
    def add_group(cls, parser) -> None:
        instance = cls()
        parser.add_argument_group(instance._title, instance._description)
    
    @property
    def title(self) -> str:
        return self._title


class ArgumentGroup_Validation(ArgumentGroup):
    def __init__(self) -> None:
        self._title = 'validation arguments'
        self._description = 'These options are only required if the --validator option is set'


#**********
# Arguments
#**********

class Argument:
    _group = ArgumentGroup # Group is optional, so a default is required.

    @classmethod
    def add_arg(cls, parser) -> None:
        instance = cls()
        arg_groups = parser._action_groups
        iterator = (g for g in arg_groups if g.title is instance._group().title)
        parser = next(iterator, parser)
        parser.add_argument(*instance._flags, **instance._params)


class Argument_Validator(Argument):
    def __init__(self) -> None:
        self._flags = ['--validator']
        self._params = {
            'type': str,
            'choices': ['a', 'b', 'c'],
            'help': 'Validator to use.',
            'dest': 'validator'
        }
        self._group = ArgumentGroup_Validation

Code from init.py

if __name__ == '__init__':
    parser = Parser()
    args = parser.parse_args()
    validator = Validator(**args)
\$\endgroup\$
4
  • 3
    \$\begingroup\$ @DavidGard can you include an example of how you use this parser? \$\endgroup\$ Jan 31, 2022 at 11:52
  • \$\begingroup\$ @N3buchadnezzar, I've updated the question. All I'm doing is parsing the arguments, then passing them as kwargs to a custom Validator class, which will validate a configuration file. The parser arguments will control exactly how validation of the config file is completed. As noted in the question, there are in reality many argument groups and arguments, hence my desire to split them out in to their own objects rather than having a big long list of parser.add_argument() calls. \$\endgroup\$
    – David Gard
    Jan 31, 2022 at 12:05
  • 1
    \$\begingroup\$ I do not see Validator being defined? \$\endgroup\$ Jan 31, 2022 at 14:01
  • \$\begingroup\$ @N3buchadnezzar, it's not defined here, it's in a different file. It's not relevant though, it just consumes the arguments that are produced by Parser. It's the definition of Parser, specifically around adding argument groups and arguments and the use of @classmethod, for which I am seeking review. \$\endgroup\$
    – David Gard
    Jan 31, 2022 at 14:22

1 Answer 1

1
\$\begingroup\$

"Private" names

By convention, names with a single, leading underscore like _action_groups are considered private, and not part of the public API. There is nothing to stop you from using them, but the library maintainer can be rename them, delete them, or change their functionality without warning. If you are lucky such a change will cause your code to fail and not to keep running but produce incorrect results.

Classes

For what you are trying to do, classes may not be the best method to decompose the problem. It seems that trying to use classes adds a lot of complexity, which increases maintenance costs. For example, Argument subclasses need to include a reference to the ArgumentGroup to which they belong (e.g., self._group = ArgumentGroup_Validation). When an ArgumentGroup gets renamed, refactored, etc. there are now many places that need to be changed.

I prefer to use functions to decompose the task of building and argparse parser. Use a separate function for groups of related arguments (may or may not be an argument group). If desired, the functions could be methods of a class.

def make_parser():
    parser = ArgumentParser()
    
    validation_group(parser)
    debug_group(parser)
    # etc

    return parser

def validation_group(parser):
    group = parser.add_argument_group(
        title='validation arguments',
        description='These options are only required if the --validator option is set.'
    )
    group.add_argument(
        '--validator',
        type=str,
        choices'=['a', 'b', 'c'],
        help='Validator to use.',
        dest='validator'
        }
    #... add other group arguments ...


def debug_group(parser):
    group = parser.add_argument_group(
        title='debug arguments',
        description='These options are only required if the --debug option is set.'
    )
    # etc.
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for your reply. I guess I chose to use classes so that each group and argument could be it's own entity, but I agree that it has added complexity (although not necessarily renames, which are pretty easy to handle in modern IDE's). I'll consider switching though as it will avoid the need for referencing the private attribute _action_groups, which as you say could unexpectedly be renamed or removed. \$\endgroup\$
    – David Gard
    Feb 14, 2022 at 10:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.