Package: cline
📜 ClineClineis a Python package that helps you build command line applications by separating the concerns ofunderstanding the command line arguments you receiveandoperating on strongly-typed arguments, and helps you to write clean, testable tasks.Getting startedInstallationCline requiresPython 3.8 or later.Install Cline via pip:pipinstallclineUsageCreate aCliclass to parse arguments and orchestrate tasks.CreateTaskclasses to describe the work that your application can perform.On startup, invoke your CLI'sinvoke_and_exit()method. Cline will discover and invoke the most appropriateTaskfor the given arguments.The examples below go into detail.ExamplesExample 1: Summing two integersThe source code for this example is available atgithub.com/cariad/cline/blob/main/examples/example01. The tests are ingithub.com/cariad/cline/blob/main/tests/examples/example01.In this example, we'll build a command line application that sums two integers then prints the result:python-mexamples.example011341. Create a CLI class with an argument parserFor this example, we'll parse the command line arguments with Python's baked-inArgumentParser. Cline provides anArgumentParserClibase class for this.Create a class namedExampleClithat inherits fromcline.ArgumentParserCli. Overridemake_parser()to return anArgumentParserthat accepts two numbers.For example:fromargparseimportArgumentParserfromclineimportArgumentParserCliclassExampleCli(ArgumentParserCli):defmake_parser(self)->ArgumentParser:parser=ArgumentParser()parser.add_argument("a",help="first number",nargs="?",)parser.add_argument("b",help="second number",nargs="?",)returnparser2. Creating a taskA task does two things:Converts the command line arguments to strongly-typed task argumentsOperates on strongly-typed task argumentsWe'll start with the strongly-typed task arguments. Since we'll be summing two integers, let's define a dataclass that holds two integers:fromdataclassesimportdataclass@dataclassclassNumberArgs:a:intb:intNow we need a way to convert command line arguments to these task arguments. Create a class namedSumTaskand inherit fromcline.Task.Taskis generic and requires your strongly-typed arguments type.fromdataclassesimportdataclassfromclineimportCommandLineArguments,Task@dataclassclassNumberArgs:a:intb:intclassSumTask(Task[NumberArgs]):passTo convert command line arguments to strongly-typed task arguments, override themake_args()method:fromdataclassesimportdataclassfromclineimportCommandLineArguments,Task@dataclassclassNumberArgs:a:intb:intclassSumTask(Task[NumberArgs]):@classmethoddefmake_args(cls,args:CommandLineArguments)->NumberArgs:returnNumberArgs(a=args.get_integer("a"),b=args.get_integer("b"),)CommandLineArgumentswraps the parsed command line arguments with some handy functions for reading argument values.For example, since we named the two numbersaandbin the parser earlier, we can refer to them by name and callget_integer()to get their values.Note thatCommandLineArgumentswill raiseCannotMakeArgumentsifaorbaren't set or can't be parsed as integers. This is the correct way to indicate that task arguments cannot be made for a given task. You don't need to validate the arguments yourself.Now thatSumTaskknows how to create strongly-typed arguments, we can operate on them.Override theinvoke()method to:Operate onself.argsWrite output toself.outReturn the shell exit codeIn this example,invoke()sums the two integer arguments, writes the result, then returns0to indicate success:fromdataclassesimportdataclassfromclineimportCommandLineArguments,Task@dataclassclassNumberArgs:a:intb:intclassSumTask(Task[NumberArgs]):@classmethoddefmake_args(cls,args:CommandLineArguments)->NumberArgs:returnNumberArgs(a=args.get_integer("a"),b=args.get_integer("b"),)definvoke(self)->int:result=self.args.a+self.args.bself.out.write(f"{result}\n")return03. Registering the task with the CLIBack in your CLI class, overrideregister_tasks()to registerSumTask:fromargparseimportArgumentParserfromclineimportArgumentParserClifromexamples.example01.sumimportSumTaskclassExampleCli(ArgumentParserCli):defmake_parser(self)->ArgumentParser:parser=ArgumentParser()parser.add_argument("a",help="first number",nargs="?",)parser.add_argument("b",help="second number",nargs="?",)returnparserdefregister_tasks(self)->RegisteredTasks:return[SumTask,]4. Creating a CLI entry pointFinally, create a__main__.pyscript that calls your CLI'sinvoke_and_exit()method:fromexamples.example01.cliimportExampleClidefentry()->None:ExampleCli.invoke_and_exit()if__name__=="__main__":entry()And that's it! You can now sum integers on the command line:python-mexamples.example011345. Unit testingCline was designed to support easy high coverage of your work.To test your task'smake_args()function, construct your ownCommandLineArgumentsinstance with the command line arguments to test, then verify thatmake_args()returns the strongly-typed arguments that you'd expect:fromclineimportCommandLineArgumentsfromexamples.example01.sumimportSumTask,NumberArgsdeftest_make_args()->None:cli_args=CommandLineArguments({"a":"1","b":"2",})args=SumTask.make_args(cli_args)assertargs==NumberArgs(a=1,b=2)To test your task'sinvoke()function, construct your own strongly-typed arguments and an output writer then assert that the shell exit code and written response are as you expect:fromioimportStringIOfromexamples.example01.sumimportSumTask,NumberArgsdeftest_invoke()->None:out=StringIO()task=SumTask(args=NumberArgs(a=5,b=2),out=out)asserttask.invoke()==0assertout.getvalue()=="7\n"To test that the CLI will invoke your task for a given set of command line arguments, you can instantiate your CLI with the arguments to test then assert that the.taskand.task.argsproperties are as you expect:fromtypingimportList,TypefrompytestimportmarkfromclineimportAnyTaskfromexamples.example01.cliimportExampleClifromexamples.example01.tasksimportNumberArgs,SumTask@mark.parametrize("args, expect_task, expect_args",[(["1","2"],SumTask,NumberArgs(a=1,b=2)),],)deftest(args:List[str],expect_task:Type[AnyTask],expect_args:NumberArgs,)->None:cli=ExampleCli(args=args)assertisinstance(cli.task,expect_task)assertcli.task.args==expect_argsExample 2: Adding support for subtractionThe source code for this example is available atgithub.com/cariad/cline/blob/main/examples/example02. The tests are ingithub.com/cariad/cline/blob/main/tests/examples/example02.In this example, we'll build on Example 1 to allow integers to be subtracted. We'll add--sumand--subflags to describe that we want to do.For example:python-mexamples.example02--sum85python-mexamples.example02--sub8513
31. Add the new flags to the argument parserIn your CLI class, update the argument parser to support--sumand--subflags:defmake_parser(self)->ArgumentParser:parser=ArgumentParser()parser.add_argument("a",help="first number",nargs="?")parser.add_argument("b",help="second number",nargs="?")parser.add_argument("--sub",help="subtract",action="store_true",)parser.add_argument("--sum",help="sum",action="store_true",)returnparserNote that the new arguments are presented as--suband--sumhere, but their names omit the dashes. The new arguments are namedsubandsum.2. Update SumTask to require the "--sum" flagIn the previous example, we wantedSumTaskto always run if we gave it two numbers. Now we want it to run only if we give it two numbersandthe--sumflag.We can achieve this simply by callingassert_true()in the task'smake_args()function, passing the name of the flag we want to assert is truthy:@classmethoddefmake_args(cls,args:CommandLineArguments)->NumberArgs:args.assert_true("sum")returnNumberArgs(a=args.get_integer("a"),b=args.get_integer("b"),)3. Create the SubtractTask classTo perform subtraction, we'll need aSubtractTaskclass. This is almost identical toSumTask, expect:We want to assert that thesub(notsum) flag is truthyWe want to subtract (not sum) the argumentsSo, for example:classSubtractTask(Task[NumberArgs]):@classmethoddefmake_args(cls,args:CommandLineArguments)->NumberArgs:args.assert_true("sub")returnNumberArgs(a=args.get_integer("a"),b=args.get_integer("b"),)definvoke(self)->int:result=self.args.a-self.args.bself.out.write(f"{result}\n")return04. Register the SubtractTask classOpen your CLI class and registerSubtractTask:defregister_tasks(self)->RegisteredTasks:return[examples.example02.tasks.SubtractTask,examples.example02.tasks.SumTask,]...and that's it! When your application runs, Cline will try each registered task in order until one is found that accepts the given command line arguments, then executes it:python-mexamples.example02--sum85python-mexamples.example02--sub8513
3Example 3: Supporting help and versionsThe source code for this example is available atgithub.com/cariad/cline/blob/main/examples/example03. The tests are ingithub.com/cariad/cline/blob/main/tests/examples/example03.Cline has baked-in support for printing your application's help and version on the command line.Enabling support for help is easy enough: it's already enabled. If Cline cannot find any registered tasks that can handle the given command line arguments then it will print the argument parser's help.For example, if we run one of the above examples without specifying any numbers then each task'smake_args()call will fail and Cline will fall back to displaying the help:python-mexamples.example01usage: __main__.py [-h] [a] [b]

positional arguments:
  a           first number
  b           second number

options:
  -h, --help  show this help message and exitAdding support for version printing is a two-step process. First, add a--versionflag to your argument parser:defmake_parser(self)->ArgumentParser:parser=ArgumentParser()parser.add_argument("a",help="first number",nargs="?")parser.add_argument("b",help="second number",nargs="?")parser.add_argument("--sub",help="subtracts numbers",action="store_true",)parser.add_argument("--sum",help="sums numbers",action="store_true",)parser.add_argument("--version",help="show version",action="store_true",)returnparserThen in__main__.py, pass your application's version intoinvoke_and_exit():fromexamples.fullimport__version__fromexamples.full.cliimportExampleClidefentry()->None:ExampleCli.invoke_and_exit(app_version=__version__)if__name__=="__main__":entry()Now you can pass--versionon the command line and get your application's version:python-mexamples.example03--version1.2.3Note that this code depends on your own implementation of versioning. I use__version__but you don't have to. If your application's version is gettable via a property other than__version__then pass that instead.Example 4: Making your package executable after installingThis isn't Cline functionality, but I'll preempt the question by answering it now.In yoursetup.pyscript, pass anentry_pointsvalue intosetup():setup(# ...entry_points={"console_scripts":["foo=bar.__main__:entry",],},# ...)Note that:foois the name of the command you want your users to run (i.e.foo --help)baris your package nameentryis the name of the function to run inside__main__.pyProjectContributingTo contribute a bug report, enhancement or feature request, please raise an issue atgithub.com/cariad/cline/issues.If you want to contribute a code change, please raise an issue first so we can chat about the direction you want to take.LicenceEdition is released atgithub.com/cariad/clineunder the MIT Licence.SeeLICENSEfor more information.AuthorHello! 👋 I'mCariad Ecclestonand I'm a freelance DevOps and backend engineer. My contact details are available on my personal wiki atcariad.earth.Please consider supporting my open source projects bysponsoring me on GitHub.AcknowledgementsThis documentation was pressed withEdition.