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 click.
|
|
This command line contains two subcommands greet and 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.
argparse Advanced
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. 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 greet.
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 cmd_instance.handle by 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 subparser.
Then the final processing logic is very natural.
Parameter reuse
With the power of OOP, I can come up with some less repetitive code. Notice that greet and 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 Command class.
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 subcommands_add_arguments and 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.
Conclusion
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, pip and Django are written in a similar way on the command line, only the implementation is different.