Package: piou
PiouA CLI tool to build beautiful command-line interfaces with type validation.It is as simple asfrompiouimportCli,Optioncli=Cli(description='A CLI tool')@cli.command(cmd='foo',help='Run foo command')deffoo_main(bar:int=Option(...,help='Bar positional argument (required)'),baz:str=Option(...,'-b','--baz',help='Baz keyword argument (required)'),foo:str=Option(None,'--foo',help='Foo keyword argument'),):"""A longer description on what the function is doing.You can run it with:```bashpoetry run python -m piou.test.simple foo 1 -b baz```And you are good to go!"""passif__name__=='__main__':cli.run()The output will look like this:python -m piou.test.simple -hpython -m piou.test.simple foo -hWhy ?I could not find a library that provided:the same developer experience thanFastAPIcustomization of the interface (to build a CLI similar to the one ofPoetry)type validation / castingTyperis the closest alternative in terms of experience but lacks the possibility
to format the output is a custom way using external libraries (likeRich).Piouprovides all these possibilities and lets you define your ownFormatter.InstallYou can installpiouwith either:pip install piouconda install piou -c conda-forgeFeaturesCommandsfrompiouimportCli,Optioncli=Cli(description='A CLI tool')@cli.command(cmd='foo',help='Run foo command')deffoo_main(foo1:int=Option(...,help='Foo arguments'),foo2:str=Option(...,'-f','--foo2',help='Foo2 arguments'),foo3:str=Option(None,'-g','--foo3',help='Foo3 arguments'),):pass@cli.command(cmd='bar',help='Run bar command')defbar_main(foo1:int=Option(...,help='Foo arguments'),foo2:str=Option(...,'-f','--foo2',help='Foo2 arguments'),foo3:str=Option(None,'-g','--foo3',help='Foo3 arguments'),):passif__name__=='__main__':cli.run()In this case,foo1is a positional argument whilefoo2andfoo3are keyword arguments.You can optionally specify global options that will be passed to all commands:cli=Cli(description='A CLI tool')cli.add_option('-q','--quiet',help='Do not output any message')Thedescriptioncan also be extracted from the function docstring. Both functions here return the same description.@cli.command(cmd='bar',description='Run foo command')defbar_main():pass@cli.command(cmd='bar2')defbar_2_main():"""Run foo command"""passA command can also be asynchronous, it will be run automatically usingasyncio.run.@cli.command(cmd='bar',help='Run foo command')asyncdefbar_main():passCommand Groups / Sub-commandsYou can group commands into sub-commands:frompiouimportCli,Optioncli=Cli(description='A CLI tool')@cli.command(cmd='foo',help='Run foo command')deffoo_main():passsub_cmd=cli.add_sub_parser(cmd='sub',help='A sub command')sub_cmd.add_option('--test',help='Test mode')@sub_cmd.command(cmd='bar',help='Run bar command')defsub_bar_main(**kwargs):pass@sub_cmd.command(cmd='foo',help='Run foo command')defsub_foo_main(test:bool,foo1:int=Option(...,help='Foo argument'),foo2:str=Option(...,'-f','--foo2',help='Foo2 argument'),):passif__name__=='__main__':cli.run()So when runningpython run.py sub -hit will output the following:Options processorSometimes, you want to run a function using the global arguments before running the actual command (for instance
initialize a logger based on theverboselevel).To do so, you useset_options_processorthat will receive all the current global options of the CLI.frompiouimportClicli=Cli(description='A CLI tool')cli.add_option('--verbose',help='Increase verbosity')defprocessor(verbose:bool):print(f'Processing{verbose=}')cli.set_options_processor(processor)You can also use the decorator syntax:frompiouimportCli,Optioncli=Cli(description='A CLI tool')@cli.processor()defprocessor(verbose:bool=Option(False,'--verbose',help='Increase verbosity')):print(f'Processing{verbose=}')By default, when a processor is set, the global arguments will not be passed downstream.
If you still want them to be passed to the functions by settingcli=Cli(description='A CLI tool',propagate_options=True)or in the case of asub-commandcli.add_sub_parser(cmd='sub',help='A sub command',propagate_options=True)Derived OptionsSometimes, you want to reuse the options in multiple command and group them into a single output to pass to
the command. For instance, you might want to group a connection string parameter to connect to a database. Here is a
full example:frompiouimportCli,Option,Derived,Passwordimportpsycopg2cli=Cli(description='A CLI tool')defget_pg_conn(pg_user:str=Option('postgres','--pg-user'),pg_pwd:Password=Option('postgres','--pg-pwd'),pg_host:str=Option('localhost','--pg-host'),pg_port:int=Option(5432,'--pg-port'),pg_db:str=Option('postgres','--pg-db')):conn=psycopg2.connect(dbname=pg_db,user=pg_user,password=pg_pwd,host=pg_host,port=pg_port)returnconn@cli.command(help='Run foo command')deffoo(pg_conn=Derived(get_pg_conn)):...@cli.command(help='Run bar command')defbar(pg_conn=Derived(get_pg_conn)):...You can also pass dynamic derived functions to avoid duplicating the derived logic:importosfromtypingimportLiteralfrompiouimportCli,Option,Derivedcli=Cli(description='A CLI tool')defget_pg_url_dynamic(source:Literal['db1','db2']):_source_upper=source.upper()_host_arg=f'--host-{source}'_db_arg=f'--{source}'def_derived(# We need to specify the `arg_name` herepg_host:str=Option(os.getenv(f'PG_HOST_{_source_upper}','localhost'),_host_arg,arg_name=_host_arg),pg_db:str=Option(os.getenv(f'PG_DB_{_source_upper}',source),_db_arg,arg_name=_db_arg),):returnf'postgresql://postgres:postgres@{pg_host}:5432/{pg_db}'return_derived@cli.command(help='Run dynamic command')defdynamic(url_1:str=Derived(get_pg_url_dynamic('db1')),url_2:str=Derived(get_pg_url_dynamic('db2'))):...So that the output will look like this:On Command RunIf you want to get the command name and arguments information that are passed to it (in case of general purpose
debugging for instance), you can passon_cmd_runto the CLI.frompiouimportCli,Option,CommandMeta,Deriveddefon_cmd_run(meta:CommandMeta):passcli=Cli(description='A CLI tool',on_cmd_run=on_cmd_run)defprocessor(a:int=Option(1,'-a'),b:int=Option(2,'-b')):returna+b@cli.command()deftest(value:int=Derived(processor),bar:str=Option(None,'--bar')):passIn this case,metawill be equal to:CommandMeta(cmd_name='test',fn_args={'bar':'bar','value':5},cmd_args={'a':3,'b':2,'bar':'bar'})Help / Errors FormatterYou can customize the help and the different errors displayed by the CLI by passing a Formatter.
The default one is theRich formatterbased on theRichpackage:cmd_color: set the color of the command in the helpoption_color: set the color of the positional / keyword arguments in the helpdefault_color: set the color of the default values in the helpshow_default: show the default values if the keyword arguments (if available)You can create your own Formatter by subclassing theFormatterclass (see
theRich formatterfor example).TheRich Formattersupports thePasswordtype that will hide the default value when printing help.For instance:frompiouimportPassword,Optiondeftest(pg_pwd:Password=Option('postgres','--pg-pwd')):...Complete exampleYou can try a more complete example by runningpython -m piou.test -hMoving fromargparseIf you are migrating code fromargparsetopiouhere are some differences:1. choices:add_argument('--pick', choices=['foo', 'bar'])can be replaced with the following:pick: Literal['foo', 'bar'] = Option(None, '--pick')pick: Literal['foo'] | Literal['bar'] = Option(None, '--pick')pick: str = Option(None, '--pick', choices=['foo', 'bar'])Notes:You can disable the case sensitivity by passingOption(None, '--pick', case_sentitive=False)Specifying both aLiteraltype andchoiceswill raise an error.2. action=store_true:add_argument('--verbose', action='store_true')can be replaced withverbose: bool = Option(False, '--verbose')