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)
1 Answer 1
"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.
-
\$\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– David Gard2022年02月14日 10:43:20 +00:00Commented Feb 14, 2022 at 10:43
kwargs
to a customValidator
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 ofparser.add_argument()
calls. \$\endgroup\$Validator
being defined? \$\endgroup\$Parser
. It's the definition ofParser
, specifically around adding argument groups and arguments and the use of@classmethod
, for which I am seeking review. \$\endgroup\$