diff options
| author | CoprDistGit <infra@openeuler.org> | 2023-05-10 07:30:47 +0000 |
|---|---|---|
| committer | CoprDistGit <infra@openeuler.org> | 2023-05-10 07:30:47 +0000 |
| commit | 3ed3c6d474a2d9c695cc1008305c8e81f58971a4 (patch) | |
| tree | 920f0230cb2bc681452e12b029afdba4cfea9e1e | |
| parent | 085cd7700dca280d6d869c41889af95d513974b0 (diff) | |
automatic import of python-class-cli
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | python-class-cli.spec | 1774 | ||||
| -rw-r--r-- | sources | 1 |
3 files changed, 1776 insertions, 0 deletions
@@ -0,0 +1 @@ +/class_cli-0.3.3.tar.gz diff --git a/python-class-cli.spec b/python-class-cli.spec new file mode 100644 index 0000000..3897b75 --- /dev/null +++ b/python-class-cli.spec @@ -0,0 +1,1774 @@ +%global _empty_manifest_terminate_build 0 +Name: python-class-cli +Version: 0.3.3 +Release: 1 +Summary: Converts a python class into a CLI program +License: GNU General Public License v3.0 +URL: https://github.com/yoavhayun/CLI +Source0: https://mirrors.nju.edu.cn/pypi/web/packages/2e/d4/dd7ffebebb2e064789dac4d4ff80dfcace6f1fc605b7028568637c3eaf25/class_cli-0.3.3.tar.gz +BuildArch: noarch + +Requires: python3-prompt-toolkit + +%description +# Class CLI + +A Python module for converting a Python class into a CLI program. + +Using the module encourages for well documented code while not demanding it. +It allows for separation in implementation between method execution and it's argument validations. + +## Capabilities + +* Full command and argument auto-completion and suggestions +* Basic type validation for entered arguments while typing +* Full input line validation +* Useful help messages for all methods and arguments +* Can combine CLI Class Objects together into a single Program +* Logging support +* Command history +* Execution of commands from a text file, line by line + +****************** +****************** + +## API + +The module exposes an API in the form of decorators. + +First import the CLI class + + from class_cli import CLI + +create an instance and wrap a class with decorators the it exposes + + cli = CLI() + + @cli.Program() + class MyClass: + + @cli.Operation() + def show(self): + return "I'm a CLI Program!" + +Run the main method of the CLI + + if __name__ == "__main__": + MyClass().CLI.main() + +When running the script without arguments, It will open the CLI for user input: + +> **MyClass>** show\ +> I'm a CLI Program! +> +> **MyClass>** | + +****************** + +## These are the available decorators + +### **@Program** + + Program(name=None, version=None, description=None, log=None, style=None, verbosity=logging.INFO) + + a class decorator that defines the CLI program. + Instantiation of the wrapped user class can be used as normal python code, accessing all it's attributes. + It also exposes the CLI interface with an added attribute named 'CLI' + + * Only a single Program can be declared for every CLI instance + + @name The name of the CLI program. (Default is the class name) + @version The version of the CLI program. (Default is a CLI without versioning) + @description The description of the CLI program. (Default is the class documentation) + @log A path to a log file. (Default is no log file) + @style A dictionary that overrides the styling of the CLI for the given keys (Keys: CLI.STYLE) + @verbosity Determines the verbosity level on the logger. Use None to silence STDOUT (Keys: logging module) + +### **@Operation** + + Operation() + + a method decorator that defines the execution code of an operation in the CLI + +### **@Setting** + + Setting(initial_value, updates_value=True) + + a method decorator that creates a setting value for the CLI with name equals to the method name. + It defines the execution code for setting the value into the created setting. + + @initial_value An initial value that the setting will hold after class initialization + @updates_value Whether or not calling this method automatically updates the inner setting value + +### **@Delegate** + + Delegate(reuse=True) + + a method decorator that delegates control to another CLI Object instance by exposing an access point. + The wrapped method does not accept any arguments and returns the CLI Object instance to delegate control to. + * This allows for easy integration and combination of CLI Objects into a single CLI Program + + @reuse Whether or not the returned CLI Object should be reused for later calls + +### **@Validation** + + Validation() + + A method decorator that defines a validation to be performed on an execution (Operation / Setting / Delegation) + Holds the same signature as the execution it is validating and raises an exception for invalid arguments. + * A single execution can have multiple Validations + +****************** + +## API Example + +In this example, we are wrapping a class, that holds a Setting named **value**, and exposes a method called **show** that prints it to the screen. + + from class_cli import CLI + + cli = CLI() + @cli.Program() + class MyClass: + + @cli.Operation() + def show(self): + if self.CLI.name is None: + return "I'm a CLI Program!" + else: + return "My name is {}!".format(self.CLI.name) + + @cli.Setting() + def name(self, str): + return str + + if __name__ == "__main__": + MyClass().CLI.main() + + In the main method we called **MyClass().CLI.main()** which checks for sys.argv for input and executes the commands using the instance. + +> $> **python3 MyClass.py show**\ +> I'm a CLI Program! + + If we execute the script without arguments, it will open the CLI for user input: + +> $> **python3 MyClass.py**\ +> **MyClass**\ +>     To exit, enter one of the following: **'q'** **'quit'** **'exit'**\ +>     To read commands from a file, enter one of the following: **'.r'** **'.read'**\ +>     To access the program settings, enter one of the following: **'.set'** **'.setting'**\ +> +>     **At any time, add '-h' flag to the command for help.**\ +> \ +> **MyClass>** show\ +> I'm a CLI Program! +> +> **MyClass>** .setting name John\ +> =John +> +> **MyClass>** show\ +> My name is John! +> +> **MyClass>** | + + You can also open the CLI directly by calling **run(\*args)** instead of **main** + + if __name__ == "__main__": + MyClass().CLI.run() + +Other commands: + +**run_line(line)** which executes a single line of String + +**execute(\*args)** which executes the same as **run**, but does not print out log outputs. + +## Instance API + +You can call the methods of a CLI Object directly without opening a CLI session: + + >>> instance = MyClass() + + >>> instance.show() + 'I'm a CLI Program!' + + >>> instance.name("Bob") + 'Bob' + + >> instance.show() + 'My name is Bob!' + +****************** +****************** + +## Delegation + +The library treats Class CLIs as building blocks that can integrate into a larger program. +This encourages for code separation between the components of a program. + +This is done by wrapping a class method with the @Delegate decorator. + + cli = CLI() + @cli.Program() + class Main: + @cli.Operation() + def show(self): return "Main CLI" + + @cli.Delegate() + def inner(self): + inner_cli = CLI() + @cli.Program() + class Inner: + @cli.Operation() + def show(self): return "Inner CLI" + return Inner() + + if __name__ == "__main__": + Main().CLI.main() + +You can access the inner CLI by calling the inner method: + + >>> Main() + <__main__.Main object at 0x03DBF5D0> + + >>> Main().inner() + <__main__.Main.inner.<locals>.Inner object at 0x03B6D750> + +From the CLI, you can pass commands to it by prefixing with the **inner** method, or open it directly for commands + +> **Main>** show\ +> Main CLI +> +> **Main>** inner show\ +> Inner CLI +> +> **Main>** inner +> +> **Main\\Inner>** show\ +> Inner CLI +> +> **Main\\Inner>** q +> +> **Main>** | + +****************** +****************** + +## Logging + + To log messages, the cli holds a Logger instance. to access it, use the **CLI.log** keyword + + def method(self): + self.CLI.log.info("This is an information Line") + self.CLI.log.warning("This is a Warning") + self.CLI.log.error("This is an Error") + self.CLI.log.debug("This line is shown only when Program decorator is called with 'debug=True'"") + +> **MyClass>** method\ +> This is an information Line\ +> [WARNING] This is a Warning\ +> `2019-10-10 17:51:21,807`\ +> `[ERROR] This is an Error`\ +> 2019-10-10 17:51:21,808\ +> [DEBUG][test:12] This line is shown only when Program decorator is called with 'debug=True' +> +> **MyClass>** | + +You can see documentation for the Logger object [here](https://docs.python.org/3/library/logging.html) + +****************** +****************** + +## Help Messages + +The CLI makes use of user code documentation in order to provide help messages to the user providing the **--help**/**-h** flag in the input. +Using the **--help** flag will display a message relevant to the user input. +Using it alone will display a usage message for the entire program while adding it after an Operation or Setting name will display usage information for that Operation or Setting. + + @cli.Operation() + def method(self): + """ + A description of the method + """ + pass + +> **CLI>** method -h\ +> usage: CLI method [-h] +> +> =========================\ +> A description of the method +> +> =========================\ +> optional arguments:\ +> -h, --help show this help message and exit\ +> +> **CLI>**| + +Any Validations for the method will be added to the description + + @cli.Validation() + def method(self, arg): + """ + cannot perform operation when disabled + """ + if self.disabled: + raise Exception("Cannot perform operations when disabled") + +> **CLI>** method -h\ +> usage: CLI method [-h] +> +> =========================\ +> A description of the method\ +> \* cannot perform operation when disabled\ +> \========================= +> +> optional arguments:\ +> -h, --help show this help message and exit\ +> +> **CLI>**| + +If there are arguments for the method, you can add descriptions for them inside the method documentation with a **@** prefix as demonstrated below. + + @cli.Operation() + def method(self, arg): + """ + A description of the method + + Accepts: + @arg arg description + @arg another line of the description + """ + pass + +> **CLI>** method -h\ +> usage: CLI method [-h] arg +> +> =========================\ +> A description of the method +> +> Accepts:\ +>   @arg arg description\ +>   @arg another line of the description +> +>   * cannot perform operation when disabled +> +> =========================\ +> positional arguments:\ +>  arg    =========================\ +>        arg description\ +>        another line of the description\ +>        Type: \<class 'str'>\ +>        =========================\ +> optional arguments:\ +> -h, --help show this help message and exit +> +> **CLI>**| + +****************** +****************** + +## Type Annotations + +Pythons type annotations are used by the CLI in order to use type validation and conversion. + +### Type Conversion + +Since the user input is a String, by default the arguments passed to a method are String + +**Default Behavior**: + + @cli.Operation() + def method(self, arg): + print(type(arg), ';', arg) + +> **CLI>** method value\ +> \<class 'str'> ; value +> +> **CLI>**| + +**Basic Annotation**: +Using annotations, we can provide a type to tell the CLI what is expected, and use it to convert the String into its proper type. +The CLI accepts annotations which are a Python Callable that receive a String and returns it converted to the desired type. + + @cli.Operation() + def method(self, arg:int): + print(type(arg), ';', arg) + +> **CLI>** method 17\ +> \<class 'int'> ; 17 +> +> **CLI>**| + +**Iterable Annotation**: +You can also use an Iterable as an annotation to specify a set of options. + + @cli.Operation() + def method(self, value:[1, "value", (0,0)]): + print(type(value), ';', value) + +> **CLI>** method 1\ +> \<class 'int'> ; 1 +> +> **CLI>** method value\ +> \<class 'str'> ; value +> +> **CLI>** method '(0, 0)'\ +> \<class 'tuple'> ; (0, 0) +> +> **CLI>**| + +* *Since the string representation of the items in the List are used to select the value from it, The String representations of the Items need to be unique.* + +### Type Validation + +During typing of input, The CLI will tip the user for the expected inputs, as well as block the user from entering invalid types into a command. + + @cli.Operation() + def method(self, arg1, arg2:int=0, arg3:[1, -1]=1, **extras): + pass +> **CLI>** method value number|\ +> \ +> \ +> `invalid literal for int() with base 10: 'number'`\ +> arg1 **arg2[=0]** arg3[=1] {*\*extras} : **{\<class 'int'>}** +\ +> **CLI>** method value 0 2|\ +> \ +> \ +> `'2' is not a valid option`\ +> arg1 arg2[=0] **arg3[=1]** {*\*extras} : **{\<value from: 1, -1>}** +\ +> **CLI>** method value 0 -1 arg|\ +> \ +> \ +> `'arg' must be in the format [key]=[value]`\ +> arg1 arg2[=0] arg3[=1] **{\*\*extras}** : **{\<class 'dict'> [items=0]}** +\ +> **CLI>** method value 0 -1 arg=val|\ +> \ +> \ +> \ +> arg1 arg2[=0] arg3[=1] **{\*\*extras}** : **{\<class 'dict'> [items=1]}** + +****************** + +## Auto Completion + +The CLI can auto-complete command names, file paths and argument values. + + @cli.Operation() + def method(self, arg1, arg2:int=0, arg3:[1, -1]=None, *extras): + pass + + @cli.Setting() + def name(self, arg1): + return arg1 + +Pressing 'TAB' will open up all available suggestions +> **CLI>** |\ +>    | method |\ +>    | .setting |\ +>    |  .read  | +\ +> **CLI>** .setting |\ +>       | name | + +When browsing suggestions, you can see the expected arguments at the bottom: + +> **CLI>** method|\ +>    **| method |**\ +>    |  .setting |\ +>    |  .read  |\ +> \ +> **arg1 arg2[=0] arg3[=None] {\*extras}** + +When the expected argument is an Iterable, the CLI will suggest all the Iterable items + +> **CLI>** method value 0 |\ +>           | 1 |\ +>           | -1 |\ +> \ +> arg1 arg2[=0] **arg3[=None]** {*extras} : **{1, -1}** + +If you are providing your own Callable for annotation, you can make use of the auto complete mechanism by implementing **\__complete__(self, keyword)** + +****************** + +## Implementing a Custom Annotation + +You can use any callable(str) as a type annotation for the CLI. + +### Representation + +The CLI displays the callable to the user as its String representation. To set the description, override the **\__str__(self)** method + + def __str__(self): + return "A Custom Type" + +### Callable + +The annotation must be a callable that accepts a string and returns a list of strings. +The callable returns all the suggestions relevant to the given keyword + + def __call__(self, key): + if key in self.options_dict: + return self.options_dict[key] + +### Validation + +If the given string is not a valid option, you can throw an exception inside the **\__call__** method + + def __call__(self, str): + if key in self.options_dict: + return self.options_dict[key] + throw Exception("'{}'' is not a valid option".format(str)) + +This will block the user from entering a non existing key + +### Autocomplete Suggestion + +To make use of the auto complete mechanism, implement the **\__complete__(self, keyword)** method + + def __complete__(self, str): + return [s for s in self.suggestions if s.startswith(str)] + +# Dynamic Iterable Annotation + +We've seen that you can use an iterable as an annotation for a variable + + @cli.Program() + class MyClass: + @cli.Operation() + def oper(self, value:[True, False]): + print("You have selected", value) + +In this example, we are using a list to specify that only boolean values are accepted. + +What if we want to use a list that is dynamic and can change throughout the run of the program. +To do that, we need to define the list outside the class scope in order to access it in method signature + + values = [1, 2, 3] + + @cli.Program() + class MyClass: + @cli.Operation() + def oper(self, value:values): + print("You have selected", value) + + @cli.Operation() + def add_value(self, value:int): + values.append(value) + +The **oper** operation will now accept values in [1, 2, 3]. +We can also use the **add_value** operation in order to add more options to be selected + +For better code structure, we can use the **with** statement with the CLI object + + with CLI() as cli: + values = [1, 2, 3] + + @cli.Program() + class MyClass: + @cli.Operation() + def oper(self, value:values): + print("You have selected", value) + + @cli.Operation() + def add_value(self, value:int): + values.append(value) + +This will make it clear that the **values** variable is associated with the CLI. + + + + +%package -n python3-class-cli +Summary: Converts a python class into a CLI program +Provides: python-class-cli +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: python3-pip +%description -n python3-class-cli +# Class CLI + +A Python module for converting a Python class into a CLI program. + +Using the module encourages for well documented code while not demanding it. +It allows for separation in implementation between method execution and it's argument validations. + +## Capabilities + +* Full command and argument auto-completion and suggestions +* Basic type validation for entered arguments while typing +* Full input line validation +* Useful help messages for all methods and arguments +* Can combine CLI Class Objects together into a single Program +* Logging support +* Command history +* Execution of commands from a text file, line by line + +****************** +****************** + +## API + +The module exposes an API in the form of decorators. + +First import the CLI class + + from class_cli import CLI + +create an instance and wrap a class with decorators the it exposes + + cli = CLI() + + @cli.Program() + class MyClass: + + @cli.Operation() + def show(self): + return "I'm a CLI Program!" + +Run the main method of the CLI + + if __name__ == "__main__": + MyClass().CLI.main() + +When running the script without arguments, It will open the CLI for user input: + +> **MyClass>** show\ +> I'm a CLI Program! +> +> **MyClass>** | + +****************** + +## These are the available decorators + +### **@Program** + + Program(name=None, version=None, description=None, log=None, style=None, verbosity=logging.INFO) + + a class decorator that defines the CLI program. + Instantiation of the wrapped user class can be used as normal python code, accessing all it's attributes. + It also exposes the CLI interface with an added attribute named 'CLI' + + * Only a single Program can be declared for every CLI instance + + @name The name of the CLI program. (Default is the class name) + @version The version of the CLI program. (Default is a CLI without versioning) + @description The description of the CLI program. (Default is the class documentation) + @log A path to a log file. (Default is no log file) + @style A dictionary that overrides the styling of the CLI for the given keys (Keys: CLI.STYLE) + @verbosity Determines the verbosity level on the logger. Use None to silence STDOUT (Keys: logging module) + +### **@Operation** + + Operation() + + a method decorator that defines the execution code of an operation in the CLI + +### **@Setting** + + Setting(initial_value, updates_value=True) + + a method decorator that creates a setting value for the CLI with name equals to the method name. + It defines the execution code for setting the value into the created setting. + + @initial_value An initial value that the setting will hold after class initialization + @updates_value Whether or not calling this method automatically updates the inner setting value + +### **@Delegate** + + Delegate(reuse=True) + + a method decorator that delegates control to another CLI Object instance by exposing an access point. + The wrapped method does not accept any arguments and returns the CLI Object instance to delegate control to. + * This allows for easy integration and combination of CLI Objects into a single CLI Program + + @reuse Whether or not the returned CLI Object should be reused for later calls + +### **@Validation** + + Validation() + + A method decorator that defines a validation to be performed on an execution (Operation / Setting / Delegation) + Holds the same signature as the execution it is validating and raises an exception for invalid arguments. + * A single execution can have multiple Validations + +****************** + +## API Example + +In this example, we are wrapping a class, that holds a Setting named **value**, and exposes a method called **show** that prints it to the screen. + + from class_cli import CLI + + cli = CLI() + @cli.Program() + class MyClass: + + @cli.Operation() + def show(self): + if self.CLI.name is None: + return "I'm a CLI Program!" + else: + return "My name is {}!".format(self.CLI.name) + + @cli.Setting() + def name(self, str): + return str + + if __name__ == "__main__": + MyClass().CLI.main() + + In the main method we called **MyClass().CLI.main()** which checks for sys.argv for input and executes the commands using the instance. + +> $> **python3 MyClass.py show**\ +> I'm a CLI Program! + + If we execute the script without arguments, it will open the CLI for user input: + +> $> **python3 MyClass.py**\ +> **MyClass**\ +>     To exit, enter one of the following: **'q'** **'quit'** **'exit'**\ +>     To read commands from a file, enter one of the following: **'.r'** **'.read'**\ +>     To access the program settings, enter one of the following: **'.set'** **'.setting'**\ +> +>     **At any time, add '-h' flag to the command for help.**\ +> \ +> **MyClass>** show\ +> I'm a CLI Program! +> +> **MyClass>** .setting name John\ +> =John +> +> **MyClass>** show\ +> My name is John! +> +> **MyClass>** | + + You can also open the CLI directly by calling **run(\*args)** instead of **main** + + if __name__ == "__main__": + MyClass().CLI.run() + +Other commands: + +**run_line(line)** which executes a single line of String + +**execute(\*args)** which executes the same as **run**, but does not print out log outputs. + +## Instance API + +You can call the methods of a CLI Object directly without opening a CLI session: + + >>> instance = MyClass() + + >>> instance.show() + 'I'm a CLI Program!' + + >>> instance.name("Bob") + 'Bob' + + >> instance.show() + 'My name is Bob!' + +****************** +****************** + +## Delegation + +The library treats Class CLIs as building blocks that can integrate into a larger program. +This encourages for code separation between the components of a program. + +This is done by wrapping a class method with the @Delegate decorator. + + cli = CLI() + @cli.Program() + class Main: + @cli.Operation() + def show(self): return "Main CLI" + + @cli.Delegate() + def inner(self): + inner_cli = CLI() + @cli.Program() + class Inner: + @cli.Operation() + def show(self): return "Inner CLI" + return Inner() + + if __name__ == "__main__": + Main().CLI.main() + +You can access the inner CLI by calling the inner method: + + >>> Main() + <__main__.Main object at 0x03DBF5D0> + + >>> Main().inner() + <__main__.Main.inner.<locals>.Inner object at 0x03B6D750> + +From the CLI, you can pass commands to it by prefixing with the **inner** method, or open it directly for commands + +> **Main>** show\ +> Main CLI +> +> **Main>** inner show\ +> Inner CLI +> +> **Main>** inner +> +> **Main\\Inner>** show\ +> Inner CLI +> +> **Main\\Inner>** q +> +> **Main>** | + +****************** +****************** + +## Logging + + To log messages, the cli holds a Logger instance. to access it, use the **CLI.log** keyword + + def method(self): + self.CLI.log.info("This is an information Line") + self.CLI.log.warning("This is a Warning") + self.CLI.log.error("This is an Error") + self.CLI.log.debug("This line is shown only when Program decorator is called with 'debug=True'"") + +> **MyClass>** method\ +> This is an information Line\ +> [WARNING] This is a Warning\ +> `2019-10-10 17:51:21,807`\ +> `[ERROR] This is an Error`\ +> 2019-10-10 17:51:21,808\ +> [DEBUG][test:12] This line is shown only when Program decorator is called with 'debug=True' +> +> **MyClass>** | + +You can see documentation for the Logger object [here](https://docs.python.org/3/library/logging.html) + +****************** +****************** + +## Help Messages + +The CLI makes use of user code documentation in order to provide help messages to the user providing the **--help**/**-h** flag in the input. +Using the **--help** flag will display a message relevant to the user input. +Using it alone will display a usage message for the entire program while adding it after an Operation or Setting name will display usage information for that Operation or Setting. + + @cli.Operation() + def method(self): + """ + A description of the method + """ + pass + +> **CLI>** method -h\ +> usage: CLI method [-h] +> +> =========================\ +> A description of the method +> +> =========================\ +> optional arguments:\ +> -h, --help show this help message and exit\ +> +> **CLI>**| + +Any Validations for the method will be added to the description + + @cli.Validation() + def method(self, arg): + """ + cannot perform operation when disabled + """ + if self.disabled: + raise Exception("Cannot perform operations when disabled") + +> **CLI>** method -h\ +> usage: CLI method [-h] +> +> =========================\ +> A description of the method\ +> \* cannot perform operation when disabled\ +> \========================= +> +> optional arguments:\ +> -h, --help show this help message and exit\ +> +> **CLI>**| + +If there are arguments for the method, you can add descriptions for them inside the method documentation with a **@** prefix as demonstrated below. + + @cli.Operation() + def method(self, arg): + """ + A description of the method + + Accepts: + @arg arg description + @arg another line of the description + """ + pass + +> **CLI>** method -h\ +> usage: CLI method [-h] arg +> +> =========================\ +> A description of the method +> +> Accepts:\ +>   @arg arg description\ +>   @arg another line of the description +> +>   * cannot perform operation when disabled +> +> =========================\ +> positional arguments:\ +>  arg    =========================\ +>        arg description\ +>        another line of the description\ +>        Type: \<class 'str'>\ +>        =========================\ +> optional arguments:\ +> -h, --help show this help message and exit +> +> **CLI>**| + +****************** +****************** + +## Type Annotations + +Pythons type annotations are used by the CLI in order to use type validation and conversion. + +### Type Conversion + +Since the user input is a String, by default the arguments passed to a method are String + +**Default Behavior**: + + @cli.Operation() + def method(self, arg): + print(type(arg), ';', arg) + +> **CLI>** method value\ +> \<class 'str'> ; value +> +> **CLI>**| + +**Basic Annotation**: +Using annotations, we can provide a type to tell the CLI what is expected, and use it to convert the String into its proper type. +The CLI accepts annotations which are a Python Callable that receive a String and returns it converted to the desired type. + + @cli.Operation() + def method(self, arg:int): + print(type(arg), ';', arg) + +> **CLI>** method 17\ +> \<class 'int'> ; 17 +> +> **CLI>**| + +**Iterable Annotation**: +You can also use an Iterable as an annotation to specify a set of options. + + @cli.Operation() + def method(self, value:[1, "value", (0,0)]): + print(type(value), ';', value) + +> **CLI>** method 1\ +> \<class 'int'> ; 1 +> +> **CLI>** method value\ +> \<class 'str'> ; value +> +> **CLI>** method '(0, 0)'\ +> \<class 'tuple'> ; (0, 0) +> +> **CLI>**| + +* *Since the string representation of the items in the List are used to select the value from it, The String representations of the Items need to be unique.* + +### Type Validation + +During typing of input, The CLI will tip the user for the expected inputs, as well as block the user from entering invalid types into a command. + + @cli.Operation() + def method(self, arg1, arg2:int=0, arg3:[1, -1]=1, **extras): + pass +> **CLI>** method value number|\ +> \ +> \ +> `invalid literal for int() with base 10: 'number'`\ +> arg1 **arg2[=0]** arg3[=1] {*\*extras} : **{\<class 'int'>}** +\ +> **CLI>** method value 0 2|\ +> \ +> \ +> `'2' is not a valid option`\ +> arg1 arg2[=0] **arg3[=1]** {*\*extras} : **{\<value from: 1, -1>}** +\ +> **CLI>** method value 0 -1 arg|\ +> \ +> \ +> `'arg' must be in the format [key]=[value]`\ +> arg1 arg2[=0] arg3[=1] **{\*\*extras}** : **{\<class 'dict'> [items=0]}** +\ +> **CLI>** method value 0 -1 arg=val|\ +> \ +> \ +> \ +> arg1 arg2[=0] arg3[=1] **{\*\*extras}** : **{\<class 'dict'> [items=1]}** + +****************** + +## Auto Completion + +The CLI can auto-complete command names, file paths and argument values. + + @cli.Operation() + def method(self, arg1, arg2:int=0, arg3:[1, -1]=None, *extras): + pass + + @cli.Setting() + def name(self, arg1): + return arg1 + +Pressing 'TAB' will open up all available suggestions +> **CLI>** |\ +>    | method |\ +>    | .setting |\ +>    |  .read  | +\ +> **CLI>** .setting |\ +>       | name | + +When browsing suggestions, you can see the expected arguments at the bottom: + +> **CLI>** method|\ +>    **| method |**\ +>    |  .setting |\ +>    |  .read  |\ +> \ +> **arg1 arg2[=0] arg3[=None] {\*extras}** + +When the expected argument is an Iterable, the CLI will suggest all the Iterable items + +> **CLI>** method value 0 |\ +>           | 1 |\ +>           | -1 |\ +> \ +> arg1 arg2[=0] **arg3[=None]** {*extras} : **{1, -1}** + +If you are providing your own Callable for annotation, you can make use of the auto complete mechanism by implementing **\__complete__(self, keyword)** + +****************** + +## Implementing a Custom Annotation + +You can use any callable(str) as a type annotation for the CLI. + +### Representation + +The CLI displays the callable to the user as its String representation. To set the description, override the **\__str__(self)** method + + def __str__(self): + return "A Custom Type" + +### Callable + +The annotation must be a callable that accepts a string and returns a list of strings. +The callable returns all the suggestions relevant to the given keyword + + def __call__(self, key): + if key in self.options_dict: + return self.options_dict[key] + +### Validation + +If the given string is not a valid option, you can throw an exception inside the **\__call__** method + + def __call__(self, str): + if key in self.options_dict: + return self.options_dict[key] + throw Exception("'{}'' is not a valid option".format(str)) + +This will block the user from entering a non existing key + +### Autocomplete Suggestion + +To make use of the auto complete mechanism, implement the **\__complete__(self, keyword)** method + + def __complete__(self, str): + return [s for s in self.suggestions if s.startswith(str)] + +# Dynamic Iterable Annotation + +We've seen that you can use an iterable as an annotation for a variable + + @cli.Program() + class MyClass: + @cli.Operation() + def oper(self, value:[True, False]): + print("You have selected", value) + +In this example, we are using a list to specify that only boolean values are accepted. + +What if we want to use a list that is dynamic and can change throughout the run of the program. +To do that, we need to define the list outside the class scope in order to access it in method signature + + values = [1, 2, 3] + + @cli.Program() + class MyClass: + @cli.Operation() + def oper(self, value:values): + print("You have selected", value) + + @cli.Operation() + def add_value(self, value:int): + values.append(value) + +The **oper** operation will now accept values in [1, 2, 3]. +We can also use the **add_value** operation in order to add more options to be selected + +For better code structure, we can use the **with** statement with the CLI object + + with CLI() as cli: + values = [1, 2, 3] + + @cli.Program() + class MyClass: + @cli.Operation() + def oper(self, value:values): + print("You have selected", value) + + @cli.Operation() + def add_value(self, value:int): + values.append(value) + +This will make it clear that the **values** variable is associated with the CLI. + + + + +%package help +Summary: Development documents and examples for class-cli +Provides: python3-class-cli-doc +%description help +# Class CLI + +A Python module for converting a Python class into a CLI program. + +Using the module encourages for well documented code while not demanding it. +It allows for separation in implementation between method execution and it's argument validations. + +## Capabilities + +* Full command and argument auto-completion and suggestions +* Basic type validation for entered arguments while typing +* Full input line validation +* Useful help messages for all methods and arguments +* Can combine CLI Class Objects together into a single Program +* Logging support +* Command history +* Execution of commands from a text file, line by line + +****************** +****************** + +## API + +The module exposes an API in the form of decorators. + +First import the CLI class + + from class_cli import CLI + +create an instance and wrap a class with decorators the it exposes + + cli = CLI() + + @cli.Program() + class MyClass: + + @cli.Operation() + def show(self): + return "I'm a CLI Program!" + +Run the main method of the CLI + + if __name__ == "__main__": + MyClass().CLI.main() + +When running the script without arguments, It will open the CLI for user input: + +> **MyClass>** show\ +> I'm a CLI Program! +> +> **MyClass>** | + +****************** + +## These are the available decorators + +### **@Program** + + Program(name=None, version=None, description=None, log=None, style=None, verbosity=logging.INFO) + + a class decorator that defines the CLI program. + Instantiation of the wrapped user class can be used as normal python code, accessing all it's attributes. + It also exposes the CLI interface with an added attribute named 'CLI' + + * Only a single Program can be declared for every CLI instance + + @name The name of the CLI program. (Default is the class name) + @version The version of the CLI program. (Default is a CLI without versioning) + @description The description of the CLI program. (Default is the class documentation) + @log A path to a log file. (Default is no log file) + @style A dictionary that overrides the styling of the CLI for the given keys (Keys: CLI.STYLE) + @verbosity Determines the verbosity level on the logger. Use None to silence STDOUT (Keys: logging module) + +### **@Operation** + + Operation() + + a method decorator that defines the execution code of an operation in the CLI + +### **@Setting** + + Setting(initial_value, updates_value=True) + + a method decorator that creates a setting value for the CLI with name equals to the method name. + It defines the execution code for setting the value into the created setting. + + @initial_value An initial value that the setting will hold after class initialization + @updates_value Whether or not calling this method automatically updates the inner setting value + +### **@Delegate** + + Delegate(reuse=True) + + a method decorator that delegates control to another CLI Object instance by exposing an access point. + The wrapped method does not accept any arguments and returns the CLI Object instance to delegate control to. + * This allows for easy integration and combination of CLI Objects into a single CLI Program + + @reuse Whether or not the returned CLI Object should be reused for later calls + +### **@Validation** + + Validation() + + A method decorator that defines a validation to be performed on an execution (Operation / Setting / Delegation) + Holds the same signature as the execution it is validating and raises an exception for invalid arguments. + * A single execution can have multiple Validations + +****************** + +## API Example + +In this example, we are wrapping a class, that holds a Setting named **value**, and exposes a method called **show** that prints it to the screen. + + from class_cli import CLI + + cli = CLI() + @cli.Program() + class MyClass: + + @cli.Operation() + def show(self): + if self.CLI.name is None: + return "I'm a CLI Program!" + else: + return "My name is {}!".format(self.CLI.name) + + @cli.Setting() + def name(self, str): + return str + + if __name__ == "__main__": + MyClass().CLI.main() + + In the main method we called **MyClass().CLI.main()** which checks for sys.argv for input and executes the commands using the instance. + +> $> **python3 MyClass.py show**\ +> I'm a CLI Program! + + If we execute the script without arguments, it will open the CLI for user input: + +> $> **python3 MyClass.py**\ +> **MyClass**\ +>     To exit, enter one of the following: **'q'** **'quit'** **'exit'**\ +>     To read commands from a file, enter one of the following: **'.r'** **'.read'**\ +>     To access the program settings, enter one of the following: **'.set'** **'.setting'**\ +> +>     **At any time, add '-h' flag to the command for help.**\ +> \ +> **MyClass>** show\ +> I'm a CLI Program! +> +> **MyClass>** .setting name John\ +> =John +> +> **MyClass>** show\ +> My name is John! +> +> **MyClass>** | + + You can also open the CLI directly by calling **run(\*args)** instead of **main** + + if __name__ == "__main__": + MyClass().CLI.run() + +Other commands: + +**run_line(line)** which executes a single line of String + +**execute(\*args)** which executes the same as **run**, but does not print out log outputs. + +## Instance API + +You can call the methods of a CLI Object directly without opening a CLI session: + + >>> instance = MyClass() + + >>> instance.show() + 'I'm a CLI Program!' + + >>> instance.name("Bob") + 'Bob' + + >> instance.show() + 'My name is Bob!' + +****************** +****************** + +## Delegation + +The library treats Class CLIs as building blocks that can integrate into a larger program. +This encourages for code separation between the components of a program. + +This is done by wrapping a class method with the @Delegate decorator. + + cli = CLI() + @cli.Program() + class Main: + @cli.Operation() + def show(self): return "Main CLI" + + @cli.Delegate() + def inner(self): + inner_cli = CLI() + @cli.Program() + class Inner: + @cli.Operation() + def show(self): return "Inner CLI" + return Inner() + + if __name__ == "__main__": + Main().CLI.main() + +You can access the inner CLI by calling the inner method: + + >>> Main() + <__main__.Main object at 0x03DBF5D0> + + >>> Main().inner() + <__main__.Main.inner.<locals>.Inner object at 0x03B6D750> + +From the CLI, you can pass commands to it by prefixing with the **inner** method, or open it directly for commands + +> **Main>** show\ +> Main CLI +> +> **Main>** inner show\ +> Inner CLI +> +> **Main>** inner +> +> **Main\\Inner>** show\ +> Inner CLI +> +> **Main\\Inner>** q +> +> **Main>** | + +****************** +****************** + +## Logging + + To log messages, the cli holds a Logger instance. to access it, use the **CLI.log** keyword + + def method(self): + self.CLI.log.info("This is an information Line") + self.CLI.log.warning("This is a Warning") + self.CLI.log.error("This is an Error") + self.CLI.log.debug("This line is shown only when Program decorator is called with 'debug=True'"") + +> **MyClass>** method\ +> This is an information Line\ +> [WARNING] This is a Warning\ +> `2019-10-10 17:51:21,807`\ +> `[ERROR] This is an Error`\ +> 2019-10-10 17:51:21,808\ +> [DEBUG][test:12] This line is shown only when Program decorator is called with 'debug=True' +> +> **MyClass>** | + +You can see documentation for the Logger object [here](https://docs.python.org/3/library/logging.html) + +****************** +****************** + +## Help Messages + +The CLI makes use of user code documentation in order to provide help messages to the user providing the **--help**/**-h** flag in the input. +Using the **--help** flag will display a message relevant to the user input. +Using it alone will display a usage message for the entire program while adding it after an Operation or Setting name will display usage information for that Operation or Setting. + + @cli.Operation() + def method(self): + """ + A description of the method + """ + pass + +> **CLI>** method -h\ +> usage: CLI method [-h] +> +> =========================\ +> A description of the method +> +> =========================\ +> optional arguments:\ +> -h, --help show this help message and exit\ +> +> **CLI>**| + +Any Validations for the method will be added to the description + + @cli.Validation() + def method(self, arg): + """ + cannot perform operation when disabled + """ + if self.disabled: + raise Exception("Cannot perform operations when disabled") + +> **CLI>** method -h\ +> usage: CLI method [-h] +> +> =========================\ +> A description of the method\ +> \* cannot perform operation when disabled\ +> \========================= +> +> optional arguments:\ +> -h, --help show this help message and exit\ +> +> **CLI>**| + +If there are arguments for the method, you can add descriptions for them inside the method documentation with a **@** prefix as demonstrated below. + + @cli.Operation() + def method(self, arg): + """ + A description of the method + + Accepts: + @arg arg description + @arg another line of the description + """ + pass + +> **CLI>** method -h\ +> usage: CLI method [-h] arg +> +> =========================\ +> A description of the method +> +> Accepts:\ +>   @arg arg description\ +>   @arg another line of the description +> +>   * cannot perform operation when disabled +> +> =========================\ +> positional arguments:\ +>  arg    =========================\ +>        arg description\ +>        another line of the description\ +>        Type: \<class 'str'>\ +>        =========================\ +> optional arguments:\ +> -h, --help show this help message and exit +> +> **CLI>**| + +****************** +****************** + +## Type Annotations + +Pythons type annotations are used by the CLI in order to use type validation and conversion. + +### Type Conversion + +Since the user input is a String, by default the arguments passed to a method are String + +**Default Behavior**: + + @cli.Operation() + def method(self, arg): + print(type(arg), ';', arg) + +> **CLI>** method value\ +> \<class 'str'> ; value +> +> **CLI>**| + +**Basic Annotation**: +Using annotations, we can provide a type to tell the CLI what is expected, and use it to convert the String into its proper type. +The CLI accepts annotations which are a Python Callable that receive a String and returns it converted to the desired type. + + @cli.Operation() + def method(self, arg:int): + print(type(arg), ';', arg) + +> **CLI>** method 17\ +> \<class 'int'> ; 17 +> +> **CLI>**| + +**Iterable Annotation**: +You can also use an Iterable as an annotation to specify a set of options. + + @cli.Operation() + def method(self, value:[1, "value", (0,0)]): + print(type(value), ';', value) + +> **CLI>** method 1\ +> \<class 'int'> ; 1 +> +> **CLI>** method value\ +> \<class 'str'> ; value +> +> **CLI>** method '(0, 0)'\ +> \<class 'tuple'> ; (0, 0) +> +> **CLI>**| + +* *Since the string representation of the items in the List are used to select the value from it, The String representations of the Items need to be unique.* + +### Type Validation + +During typing of input, The CLI will tip the user for the expected inputs, as well as block the user from entering invalid types into a command. + + @cli.Operation() + def method(self, arg1, arg2:int=0, arg3:[1, -1]=1, **extras): + pass +> **CLI>** method value number|\ +> \ +> \ +> `invalid literal for int() with base 10: 'number'`\ +> arg1 **arg2[=0]** arg3[=1] {*\*extras} : **{\<class 'int'>}** +\ +> **CLI>** method value 0 2|\ +> \ +> \ +> `'2' is not a valid option`\ +> arg1 arg2[=0] **arg3[=1]** {*\*extras} : **{\<value from: 1, -1>}** +\ +> **CLI>** method value 0 -1 arg|\ +> \ +> \ +> `'arg' must be in the format [key]=[value]`\ +> arg1 arg2[=0] arg3[=1] **{\*\*extras}** : **{\<class 'dict'> [items=0]}** +\ +> **CLI>** method value 0 -1 arg=val|\ +> \ +> \ +> \ +> arg1 arg2[=0] arg3[=1] **{\*\*extras}** : **{\<class 'dict'> [items=1]}** + +****************** + +## Auto Completion + +The CLI can auto-complete command names, file paths and argument values. + + @cli.Operation() + def method(self, arg1, arg2:int=0, arg3:[1, -1]=None, *extras): + pass + + @cli.Setting() + def name(self, arg1): + return arg1 + +Pressing 'TAB' will open up all available suggestions +> **CLI>** |\ +>    | method |\ +>    | .setting |\ +>    |  .read  | +\ +> **CLI>** .setting |\ +>       | name | + +When browsing suggestions, you can see the expected arguments at the bottom: + +> **CLI>** method|\ +>    **| method |**\ +>    |  .setting |\ +>    |  .read  |\ +> \ +> **arg1 arg2[=0] arg3[=None] {\*extras}** + +When the expected argument is an Iterable, the CLI will suggest all the Iterable items + +> **CLI>** method value 0 |\ +>           | 1 |\ +>           | -1 |\ +> \ +> arg1 arg2[=0] **arg3[=None]** {*extras} : **{1, -1}** + +If you are providing your own Callable for annotation, you can make use of the auto complete mechanism by implementing **\__complete__(self, keyword)** + +****************** + +## Implementing a Custom Annotation + +You can use any callable(str) as a type annotation for the CLI. + +### Representation + +The CLI displays the callable to the user as its String representation. To set the description, override the **\__str__(self)** method + + def __str__(self): + return "A Custom Type" + +### Callable + +The annotation must be a callable that accepts a string and returns a list of strings. +The callable returns all the suggestions relevant to the given keyword + + def __call__(self, key): + if key in self.options_dict: + return self.options_dict[key] + +### Validation + +If the given string is not a valid option, you can throw an exception inside the **\__call__** method + + def __call__(self, str): + if key in self.options_dict: + return self.options_dict[key] + throw Exception("'{}'' is not a valid option".format(str)) + +This will block the user from entering a non existing key + +### Autocomplete Suggestion + +To make use of the auto complete mechanism, implement the **\__complete__(self, keyword)** method + + def __complete__(self, str): + return [s for s in self.suggestions if s.startswith(str)] + +# Dynamic Iterable Annotation + +We've seen that you can use an iterable as an annotation for a variable + + @cli.Program() + class MyClass: + @cli.Operation() + def oper(self, value:[True, False]): + print("You have selected", value) + +In this example, we are using a list to specify that only boolean values are accepted. + +What if we want to use a list that is dynamic and can change throughout the run of the program. +To do that, we need to define the list outside the class scope in order to access it in method signature + + values = [1, 2, 3] + + @cli.Program() + class MyClass: + @cli.Operation() + def oper(self, value:values): + print("You have selected", value) + + @cli.Operation() + def add_value(self, value:int): + values.append(value) + +The **oper** operation will now accept values in [1, 2, 3]. +We can also use the **add_value** operation in order to add more options to be selected + +For better code structure, we can use the **with** statement with the CLI object + + with CLI() as cli: + values = [1, 2, 3] + + @cli.Program() + class MyClass: + @cli.Operation() + def oper(self, value:values): + print("You have selected", value) + + @cli.Operation() + def add_value(self, value:int): + values.append(value) + +This will make it clear that the **values** variable is associated with the CLI. + + + + +%prep +%autosetup -n class-cli-0.3.3 + +%build +%py3_build + +%install +%py3_install +install -d -m755 %{buildroot}/%{_pkgdocdir} +if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi +if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi +if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi +if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi +pushd %{buildroot} +if [ -d usr/lib ]; then + find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/lib64 ]; then + find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/bin ]; then + find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/sbin ]; then + find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst +fi +touch doclist.lst +if [ -d usr/share/man ]; then + find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst +fi +popd +mv %{buildroot}/filelist.lst . +mv %{buildroot}/doclist.lst . + +%files -n python3-class-cli -f filelist.lst +%dir %{python3_sitelib}/* + +%files help -f doclist.lst +%{_docdir}/* + +%changelog +* Wed May 10 2023 Python_Bot <Python_Bot@openeuler.org> - 0.3.3-1 +- Package Spec generated @@ -0,0 +1 @@ +9733d5851168b18c793be5c7346ec928 class_cli-0.3.3.tar.gz |
