I want to build a cli interface for my application which has nested functionality. Example:
├── ...
├── setup.py
└── package-name
├──__init__.py
├──command1.py
└──command2.py
package-name command1 --arg .. ..
package-name command2 --arg ..
AND
python -m package-name command1 --arg ..
The thing to note here is that command1
and command2
are independent modules that accept different command-line args. So linking them together in a __main__.py
might also be another challenge.
I came across similar questions that use entry_points
in setup.py
to create similar cli functionality but its not exactly what I'm looking for. I found this similar question.
How to create a CLI in Python that can be installed with PIP?
CodePudding user response:
If you want access multiple sub-cli's in one entry command, you can implement a sub-command manager at __main__.py
, which can parse sub-command from sys.argv and then dispatch to target module.
1️⃣First, i recommend the google fire, which can satify you in most scenarios without extra code.
Here is a example, you can replace the add/multiply function to your sub-command function by using from command1 import xx
,and use entry points to expose the main function.
import fire
def add(x, y):
return x y
def multiply(x, y):
return x * y
def main():
fire.Fire({
'add': add,
'multiply': multiply,
})
if __name__ == '__main__':
main()
We can debug this in the same way as below:
$ python example.py add 10 20
30
$ python example.py multiply 10 20
200
2️⃣Second, if you need implements by your self for some purpose, such as using argparse to define options for each command. A typical practice is the Django command, the official demo: Writing custom django-admin commands
The core steps is :
- define a BaseCommand
- implements BaseCommand in sub commands.py, and named it as
Command
. __main__.py
implements find command and call
# base_command.py
class BaseCommand(object):
def create_parser(self, prog_name, subcommand, **kwargs):
"""
Create and return the ``ArgumentParser`` which will be used to
parse the arguments to this command.
"""
# TODO: create your ArgumentParser
return CommandParser(**kwargs)
def add_arguments(self, parser):
"""
Entry point for subclassed commands to add custom arguments.
"""
pass
def run_from_argv(self, argv):
"""
Entry point for commands to be run from the command line.
"""
parser = self.create_parser(argv[0], argv[1])
options = parser.parse_args(argv[2:])
cmd_options = vars(options)
args = cmd_options.pop('args', ())
self.handle(*args, **cmd_options)
def handle(self, *args, **options):
"""
The actual logic of the command. Subclasses must implement
this method.
"""
raise NotImplementedError('subclasses of BaseCommand must provide a handle() method')
# command1.py
class Command(BaseCommand):
def handle(self, *args, **options):
print("Hello, it is command1!")
# command2.py
class Command(BaseCommand):
def handle(self, *args, **options):
print("Hello, it is command2!")
# __main__.py
def main():
# sub command name is the second argument
command_name = sys.argv[1]
# build sub command module name, and import
# package-name is the module name you mentioned
cmd_package_name = f'package-name.{command_name}'
instance = importlib.import_module(cmd_package_name)
# create instance of sub command, the Command must exist
command = instance.Command()
command.run_from_argv(sys.argv)
if __name__ == "__main__":
main()