argparse is a standard library for parsing command line arguments. Its usage is roughly like this.
This is very mundane, and the same functionality could be written much faster if
click were used.
The difference between the two is that
argparse uniformly parses the argument value and then processes it itself, while
click can pass the argument value directly to the decorated function. The latter approach is more conducive to code decoupling and easier to maintain.
I also initially chose
click when I was working on PDM, which has a series of subcommands on the command line, and
click’s nested command groups (
click.Group) provided powerful support that helped me do this well. However, as I wrote deeper and tried to add some more complex functionality, I discovered the shortcomings of
click and was prompted to finally choose
argparse. So far it seems that the capabilities provided by
argparse do the job very well.
Inheritance and extensions
Suppose we have written a command line interface like the following with
This command line contains two subcommands
goodbye, and now I’ve released this bot library and want users to add new commands to this with
click, which is easy.
This command line now has a new
test subcommand. This is how the Flask CLI can be extended. But I’d like to build on this and provide the ability to add command options, like adding a
-verbose option to the original
greet command, which is a verbose hello if true, otherwise a concise hello. How is this done? It involves adding an argument to the original
greet function and changing the behavior of the function to read that argument. Looking at the API documentation I learn that this function is stored on the
callback property of the generated
Command object, so I can only write a new function that replaces it, then if I don’t want to copy the original function over, but just want to inherit and extend it, then I have to keep the original function in place and call it in the new function.
This whole process, to me, seemed like a Monkey patch, which in a language that supports OOP, it shouldn’t be, so I started looking for alternatives. Of course, I finally found
argparse, and here’s how I used
argparse to implement the command-line interface of PDM.
Subcommands of argparse
argparse also supports subcommands, and subcommands can have their own subcommands.
This looks much more laborious than
click and still only gets the parsed result, not processed, but this drawback also makes
argparse more flexible and we can control how it finds the corresponding processing method. Inheritance and extension, isn’t that the idea of OOP? So can I change this spaghetti-type code to OOP?
OOPification of argparse
The principle is to put each subcommand into its own class, so I’ll separate the above code.
You can see that the middle two subcommands are written in a highly consistent way with only one operation, which is
add_argument, so I put this method inside the subcommand class to be implemented and use some IoC trick to get the following code.
The following are the mounting methods in the root parser.
Here I instantiated
command instead of using
classmethod directly, to facilitate passing in some root parser-related information when instantiating. This way I have decoupled the command parsing, and arguments related to subcommands are added in
add_argument in their own class.
Handling method routing
Now we’ve just implemented adding arguments to subcommands, but we still need to choose different processing methods for different subcommands. We don’t know how to do this yet, so whatever, let’s put this method inside the
Command class first.
How do I route to the processing of this subcommand when it is parsed? You need to understand the parsing process of
argparse is to get
sys.argv and then look at it in order, if it finds an argument, assign the value of that argument in the result, if it finds a subcommand name, get the parser of that subcommand and call the parser recursively to parse the rest of the command line arguments. In other words, if the subcommand is not matched, no action related to the subcommand will be executed, and the parameters of the subcommand will not be added to the parser. Subcommands at the same level are necessarily mutually exclusive, and it is not possible to match multiple subcommands at the same time. For example,
python cli.py greet goodbye matches the
greet command, and
goodbye will be parsed in
greet’s own parser as an argument to
Then we can save the processing of this subcommand to the parser when it is matched, and we are done. All it takes is a slight modification to the subcommand mount procedure.
The value of handle is set to
set_defaults. It is used to set the value of handle to
cmd_instance.handle if there is no
handle in the result after parsing. And this behavior will only take effect when the subcommand is parsed, because it works on
Then the final processing logic is very natural.
With the power of OOP, I can come up with some less repetitive code. Notice that
goodbye both have a
-n/--name argument, of the same type. Adding the argument is done in
add_argument. IoC again.
Further, add the class attribute
arguments to the
Upgraded argparse usage
Now back to the requirement I started with. Inheritance and extension, if I want to add a new subcommand, I just need to inherit the base class
Command, implement the
handle methods, and add it to
subcommands (the added methods will be exposed).
If you want to modify the existing commands, you only need to inherit from the original command class.
The original command name
greet is used to override the original command when mounting.
We’ve taken advantage of Python’s dynamic nature and implemented the OOPization of
argparse with reasonable finesse (IoC). PDM uses this approach to implement extensible command-line parsing. The complete command class is in pdm/cli/commands, and the command parsing assembly process is in pdm/ core.py. In fact,
Django are written in a similar way on the command line, only the implementation is different.