improve the docs in preparation for 1.4.0

This commit is contained in:
Zach White 2021-05-22 23:07:30 -07:00
parent 54bb096cb2
commit fc6730fae8
19 changed files with 259 additions and 119 deletions

View file

@ -21,18 +21,20 @@ MILC follows [Semantic Versioning](https://semver.org/). You can see a list of w
Full documentation is on the web: <https://milc.clueboard.co/>
## Reporting Bugs and Requesting Features
Please let us know about any bugs and/or feature requests you have: <https://github.com/clueboard/milc/issues>
## Short Example
```python
from milc import MILC
from milc import cli
cli = MILC('My useful CLI tool.')
@cli.argument('-c', '--comma', help='comma in output', default=True, action='store_boolean')
@cli.argument('-n', '--name', help='Name to greet', default='World')
@cli.entrypoint
@cli.argument('-c', '--comma', arg_only=True, action='store_boolean', default=True, help='comma in output')
@cli.argument('-n', '--name', default='World', help='Name to greet')
@cli.entrypoint('My useful CLI tool.')
def main(cli):
comma = ',' if cli.config.general.comma else ''
comma = ',' if cli.args.comma else ''
cli.log.info('Hello%s %s!', comma, cli.config.general.name)
if __name__ == '__main__':

View file

@ -32,7 +32,9 @@ cli.echo(text)
## Available Colors
Colors prefixed with 'fg' will affect the foreground (text) color. Colors
prefixed with 'bg' will affect the background color.
prefixed with 'bg' will affect the background color. The included
`milc-color` command will show you what the colors look like in your
terminal.
| Color | Background | Extended Background | Foreground | Extended Foreground|
|-------|------------|---------------------|------------|--------------------|

View file

@ -1,6 +1,6 @@
# MILC - An Opinionated Batteries-Included Python 3 CLI Framework
MILC is a framework for writing CLI applications in Python 3. It gives you all the features users expect from a modern CLI tool out of the box:
MILC is a framework for writing CLI applications in Python 3.6+. It gives you all the features users expect from a modern CLI tool out of the box:
* CLI Argument Parsing, with or without subcommands
* Automatic tab-completion support through [argcomplete](https://github.com/kislyuk/argcomplete)
@ -10,3 +10,75 @@ MILC is a framework for writing CLI applications in Python 3. It gives you all t
* Easy method for printing to stdout with ANSI colors
* Labelling log output with colored emoji to easily distinguish message types
* Thread safety
* More than 60 built-in [spinners](https://github.com/manrajgrover/py-spinners) with the ability to add your own
## Getting Started
Read [the tutorial](tutorial.md) to learn how to use MILC.
## Reporting Bugs and Requesting Features
Please let us know about any bugs and/or feature requests you have: <https://github.com/clueboard/milc/issues>
## Short Example
```python
from milc import cli
@cli.argument('-c', '--comma', action='store_boolean', arg_only=True, default=True, help='comma in output')
@cli.argument('-n', '--name', default='World', help='Name to greet')
@cli.entrypoint('My useful CLI tool.')
def main(cli):
comma = ',' if cli.args.comma else ''
cli.log.info('Hello%s %s!', comma, cli.config.general.name)
if __name__ == '__main__':
cli.run()
```
### Output
```
$ ./hello
Hello, World!
$ ./hello --no-unicode
INFO Hello, World!
$ ./hello --no-comma
Hello World!
$ ./hello -h
usage: hello [-h] [-V] [-v] [--datetime-fmt GENERAL_DATETIME_FMT]
[--log-fmt GENERAL_LOG_FMT] [--log-file-fmt GENERAL_LOG_FILE_FMT]
[--log-file GENERAL_LOG_FILE] [--color] [--no-color]
[--config-file GENERAL_CONFIG_FILE] [--save-config]
[-n GENERAL_NAME] [-c] [--no-comma]
Greet a user.
optional arguments:
-h, --help show this help message and exit
-V, --version Display the version and exit
-v, --verbose Make the logging more verbose
--datetime-fmt GENERAL_DATETIME_FMT
Format string for datetimes
--log-fmt GENERAL_LOG_FMT
Format string for printed log output
--log-file-fmt GENERAL_LOG_FILE_FMT
Format string for log file.
--log-file GENERAL_LOG_FILE
File to write log messages to
--color Enable color in output
--no-color Disable color in output
--unicode Enable unicode loglevels
--no-unicode Disable unicode loglevels
--interactive Force interactive mode even when stdout is not a tty.
--config-file GENERAL_CONFIG_FILE
The config file to read and/or write
-n GENERAL_NAME, --name GENERAL_NAME
Name to greet
-c, --comma Enable comma in output
--no-comma Disable comma in output
```
# Breaking Changes
MILC follows [Semantic Versioning](https://semver.org/). You can see a list of why we made major or minor releases on the [Breaking Changes](https://milc.clueboard.co/#/breaking_changes) page.

View file

@ -4,15 +4,16 @@
* [Tutorial](tutorial.md)
* Features
* [ANSI Color](ANSI.md)
* [Argument Completion](argcomplete.md)
* [Argument (Tab) Completion](argcomplete.md)
* [Argument Parsing](argument_parsing.md)
* [Configuration](configuration.md)
* [Config Subcommand](subcommand_config.md)
* [Logging](logging.md)
* [Metadata](metadata.md)
* [Spinners](spinners.md)
* [Subprocesses](subprocesses.md)
* [Thread Safety](threading.md)
* [User Input](questions.md)
* [User Input](api_questions.md)
<!-- DO NOT ADD OR CHANGE ANYTHING BELOW THIS LINE -->
* API Reference
* [milc.ansi](api_ansi.md)

View file

@ -12,3 +12,11 @@ Emoji used by MILC when outputting logs
| `DEBUG` | `{fg_cyan}☐` |
| `NOTSET` | `{style_reset_all}¯\\_(o_o)_/¯` |
If you'd like to use your own icon for a level instead you can simply redefine it:
```python
from milc.emoji import EMOJI_LOGLEVELS
EMOJI_LOGLEVELS['INFO'] = {fg_green}'
```

View file

@ -1,9 +1,7 @@
<a name="questions"></a>
# questions
Functions to ask the user questions.
These functions can be used to query the user for information.
Sometimes you need to ask the user a question. MILC provides basic functions for collecting and validating user input. You can find these in the `milc.questions` module.
<a name="questions.yesno"></a>
#### yesno
@ -52,7 +50,7 @@ Securely receive a password from the user. Returns the password or None.
question(prompt, *args, *, default=None, confirm=False, answer_type=str, validate=None, **kwargs)
```
Prompt the user to answer a question with a free-form input.
Allow the user to type in a free-form string to answer.
| Argument | Description |
|----------|-------------|
@ -69,9 +67,9 @@ Prompt the user to answer a question with a free-form input.
choice(heading, options, *args, *, default=None, confirm=False, prompt='Please enter your choice: ', **kwargs)
```
Present the user with a list of options and let them pick one.
Present the user with a list of options and let them select one.
Returns the value of the item they choose.
Users can enter either the number or the text of their choice. This will return the value of the item they choose, not the numerical index.
| Argument | Description |
|----------|-------------|

View file

@ -1,10 +1,10 @@
# Argcomplete Support
# Argument (Tab) Completion Support
MILC supports argument completion out of the box using [argcomplete](). Getting tab completion to actually work can be a little fiddly, this page attempts to help you with them.
MILC supports argument completion out of the box using [argcomplete](). Getting argument completion to actually work can be a little fiddly, this page attempts to help you with that.
## Prerequisites
Before tab completion will work your program must be registered with your shell. The most direct way to do so is this:
Before argument completion will work your program must be registered with your shell. The most compatible way to do so is this:
eval "$(register-python-argcomplete my-program)"

View file

@ -3,7 +3,6 @@
<head>
<meta charset="UTF-8">
<title>MILC - An opinionated batteries-included python 3 CLI framework.</title>
<link rel="icon" type="image/png" href="gitbook/images/favicon.png">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
@ -106,12 +105,5 @@
<script src="//unpkg.com/prismjs/components/prism-cpp.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-json.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-makefile.min.js"></script>
<script>
// Register the cache worker for offline viewing mode
// https://docsify.now.sh/pwa
if (typeof navigator.serviceWorker !== 'undefined') {
navigator.serviceWorker.register('sw.js')
}
</script>
</body>
</html>

View file

@ -1,19 +1,19 @@
MILC comes with a robust logging system based on python's `logging` module. All you have to do is worry about log output, let MILC worry about presenting that output to the user in configurable ways.
MILC comes with a robust logging system based on python's `logging` module. All you have to worry about are log messages, let MILC worry about presenting those messages to the user in configurable ways.
## Writing Log Entries
A python [Logger Object](https://docs.python.org/3/library/logging.html#logger-objects) is available as `cli.log`. You can use this to write messages at various log levels:
* `log.debug()`
* `log.info()`
* `log.warning()`
* `log.error()`
* `log.critical()`
* `log.exception()`
* `cli.log.debug()`
* `cli.log.info()`
* `cli.log.warning()`
* `cli.log.error()`
* `cli.log.critical()`
* `cli.log.exception()`
As is standard for the python logging module you can use [`printf`-style format string operations](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) with these. Example:
log.info('Hello, %s!', 'World')
log.info('Hello, %s!', cli.config.general.name)
ANSI color sequences are also available. For more information see the [ANSI Color](ANSI.md) page.
@ -21,7 +21,7 @@ ANSI color sequences are also available. For more information see the [ANSI Colo
All MILC programs have `-v` and `--verbose` flags by default. When this flag is passed `DEBUG` level messages will be printed to the screen.
If you want to use this flag in your program you can check `cli.args.verbose`. It is True when `-v`/`--versbose` are passed and False otherwise.
If you want to use this flag in your program you can check `cli.config.general.verbose`. It is True when `-v`/`--verbose` is passed and False otherwise.
## Controlling Log Output
@ -36,4 +36,6 @@ Users have several CLI arguments they can pass to control the output of logs. Th
* `--log-file`, default: None
* File to write log messages to
* `--color` and `--no-color`
* Enable and disable ANSI color
* Enable or disable ANSI color
* `--unicode` and `--no-unicode`
* Enable or disable unicode icons

View file

@ -1,43 +0,0 @@
# Collecting User Input
Sometimes you need to ask the user a question. MILC provides basic functions for collecting and validating user input. You can find these in the `milc.questions` module.
## choice
Present the user with a list of options and let them select one. Users can enter either the number or the text of their choice. This will return the value of the item they choose, not the numerical index.
def choice(heading, options, *args, default=None, confirm=False, prompt='Please enter your choice: ', **kwargs)
| Argument | Description |
|----------|-------------|
| `heading` | The text to place above the list of options. |
| `options` | A sequence of items to choose from. |
| `default` | The index of the item to return when the user doesn't enter any value. Use None to prompt until they enter a value. |
| `confirm` | When True present the user with a confirmation dialog before accepting their answer. |
| `prompt` | The prompt to present to the user. Can include color and format strings like milc's `cli.echo()`. |
## question
Allow the user to type in a free-form string to answer.
question(prompt, *args, default=None, confirm=False, answer_type=str, validate=None, **kwargs)
| Argument | Description |
|----------|-------------|
| `prompt` | The prompt to present to the user. You can use [string formatting characters](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) here. |
| `default` | The value to return when the user doesn't enter any value. Use None to prompt until they enter a value. |
| `confirm` | Present the user with a confirmation dialog before accepting their answer. |
| `answer_type` | Specify a type function for the answer. Will re-prompt the user if the function raises any errors. Common choices here include `int`, `float`, and `decimal.Decimal`. |
| `validate` | This is an optional function that can be used to validate the answer. It should return True or False and have the following signature: <br><br>`def function_name(answer, *args, **kwargs)` |
## yesno
This function is useful for getting a boolean from the user. It will return True or False.
yesno(prompt, *args, default=None, **kwargs)
| Argument | Description |
|----------|-------------|
| `prompt` | This text will be shown to the left of the cursor. You can use [string formatting characters](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) here.
| `default` | The value to return when the user doesn't enter any value. Use None to prompt until they enter a value.
| `args`/`kwargs` | These are used when doing string formatting on `prompt`.

73
docs/spinners.md Normal file
View file

@ -0,0 +1,73 @@
# Spinners
Spinners let you tell the user that something is happening while you're processing. There are 3 basic ways to use a spinner:
* Instantiating a spinner and then using `.start()` and `.stop()` on your object.
* Using a context manager (`with cli.spinner(...):`)
* Decorate a function (`@cli.spinner(...)`)
For full details see the [`cli.spinner` api reference](api_milc.md#spinner).
### Adding a Spinner
If you'd like to create your own spinner animation you can do that. First you should define a dictionary with two keys, `interval` and `frames`:
```python
my_spinner = {
'interval': 100, # How many ms to display each frame
'frames': ['-', '\\', '|', '/']
}
```
You can use this in one of two ways- by passing it directly to `cli.spinner()` or by adding it to the list of available spinners using `cli.add_spinner()`.
### Example: Using a custom spinner directly
```python
my_spinner = {
'interval': 100, # How many ms to display each frame
'frames': ['-', '\\', '|', '/']
}
with cli.spinner(text='Loading', spinner=my_spinner):
time.sleep(10)
```
### Example: Adding a custom spinner
```python
my_spinner = {
'interval': 100, # How many ms to display each frame
'frames': ['-', '\\', '|', '/']
}
cli.add_spinner('my_twirl', my_spinner)
with cli.spinner(text='Loading', spinner='my_twirl'):
time.sleep(10)
```
### Example: Instantiating a Spinner
```python
spinner = cli.spinner(text='Loading', spinner='dots')
spinner.start()
# Do something here
spinner.stop()
```
### Example: Using a Context Manager
```python
with cli.spinner(text='Loading', spinner='dots'):
# Do something here
```
### Example: Decorating a Function
```python
@cli.spinner(text='Loading', spinner='dots')
def long_running_function():
# Do something here
```

View file

@ -46,7 +46,7 @@ user.arg1: None -> baz
The `config` subcommand is used to interact with the underlying configuration. When run with no argument it shows the current configuration. When arguments are supplied they are assumed to be configuration tokens, which are strings containing no spaces with the following form:
<subcommand|general|default>[.<key>][=<value>]
<subcommand|general|user>[.<key>][=<value>]
## Setting Configuration Values
@ -55,18 +55,22 @@ You can set configuration values by putting an equal sign (=) into your config k
Example:
```
$ my_cli config default.arg1=default
default.arg1: None -> default
$ my_cli config user.arg1=default
user.arg1: None -> default
Wrote configuration to '/Users/example/Library/Application Support/my_cli/my_cli.ini'
```
## Reading Configuration Values
You can read configuration values for the entire configuration, a single key, or for an entire section. You can also specify multiple keys to display more than one value.
You can read configuration values for all set options, the entire configuration, a single key, or for an entire section. You can also specify multiple keys to display more than one value.
### All Set Options Example
my_cli config
### Entire Configuration Example
my_cli config
my_cli config -a
### Whole Section Example

View file

@ -32,6 +32,13 @@ This will return a [subprocess.CompletedProcess](https://docs.python.org/3/libra
MILC's `cli.run()` differs from `subprocess.run()` in some important ways.
### Windows Support
When running inside a windows console (Powershell, DOS, Cygwin, Msys2) there are some quirks that MILC attempts to handle but which you need to be aware of:
* Commands are always run in a subshell, so that non-executable files and POSIX paths work seemlessly.
* Windows leaves stdin in a broken state after executing a subprocess. To avoid this MILC adds `stdin=DEVNULL` to the `subprocess.run()` call. If you need stdin to work in your executed process you can pass `stdin=None`.
### Building argument lists
The most important way MILC differs from `subprocess.run()` is that it only accepts commands that have already been split into sequences. A lot of bugs are caused by mistakes in building command strings that are later split into a sequence of arguments in unexpected ways.

View file

@ -24,29 +24,25 @@ if __name__ == '__main__':
cli()
```
## Entrypoints
## Quick Program Overview
MILC does the work of setting up your execution environment then it hands
off control to your entrypoint. There are two types of entrypoints in MILC-
the root entrypoint and subcommand entrypoints. When you think of subcommands
think of programs like git, where the first argument that doesn't start with
a dash indicates what mode the program is operating in.
Before we dive into the features our program is using let's take a look at the general structure of a MILC program. We start by importing the `cli` object- this is where most of MILC's functionality is exposed and where a lot of important state tracking happens.
MILC entrypoints are python callables that take a single argument- `cli`.
This is the `MILC()` object that you instaniate at the start of your program,
and for the most part is how you will interact with your user. You will also
call `cli()` to dispatch to the root or subcommand entrypoint, as determined
by the flags the user passes.
Next, we've decorated our main function with `cli.entrypoint()`. This is how we tell MILC what function to execute and set the help text for our program.
Inside our `main()` function we print a simple message to the log file, which by default is also printed to the user's screen.
Finally, we execute our `cli()` program inside the familiar `if __name__ == '__main__':` guard.
## Logging and Printing
MILC provides 2 mechanisms for outputting text to the user, and which one you
MILC provides two mechanisms for outputting text to the user, and which one you
use depends a lot on the needs of your program. Both use the same API so
switching between them should be simple.
For writing to stdout you have `cli.echo()`. This differs from python
`print()` in two important ways- It supports tokens for colorizing your text
using [ANSI](ANSI.md) and it supports format strings in the same way as
using [ANSI](ANSI.md) and it supports format strings in the same way as
[logging](https://docs.python.org/3/library/logging.html). For writing to
stderr and/or log files you have `cli.log`. You can use these to output log
messages at different levels so the CLI user can easily adjust how much
@ -61,6 +57,18 @@ More information:
* [ANSI Color](ANSI.md)
* [Logging](logging.md)
## Entrypoints
MILC does the work of setting up your execution environment then it hands
off control to your entrypoint. There are two types of entrypoints in MILC-
the root entrypoint and subcommand entrypoints. When you think of subcommands
think of programs like git, where the first argument that doesn't start with
a dash indicates what mode the program is operating in.
MILC entrypoints are python callables that take a single argument- `cli`.
When you call `cli()` at the end of your script it will determine the
correct entrypoint to call based on the arguments the user passed.
## Configuration and Argument Parsing
MILC unifies arguments and configuration files. This unified config can be
@ -163,7 +171,18 @@ Each subcommand gets its own section in the configuration. You can access a
subcommand's config with `cli.config.<subcommand>`. Options for the root
entrypoint can be found in the `cli.config.general` section of the config.
Let's finish up our program by adding some flags to hello and goodbye:
We add flags to our subcommands by decorating them with `@cli.argument`:
```python
@cli.argument('--comma', help='comma in output', action='store_boolean', default=True)
```
## User Controlled Configuration
Using the built-in `config` subcommand our user can permanently set certain
options so they don't have to type them in each time. We do this by adding a
single line to our program, `import milc.subcommand.config`. Let's take a
look at our final program:
```python
#!/usr/bin/env python3
@ -173,6 +192,8 @@ PYTHON_ARGCOMPLETE_OK
"""
from milc import cli
import milc.subcommand.config
@cli.argument('-n', '--name', help='Name to greet', default='World')
@cli.entrypoint('Greet a user.')
@ -209,12 +230,13 @@ if __name__ == '__main__':
## Example Output
Now that we've written our program let's explore how it works, starting with
running it with no arguments.
Now that we've written our program and we have a better idea what is going
on, let's explore how it works. We'll start by demonstrating it with no
arguments passed.
![Simple Output](https://i.imgur.com/Ms3G8Aw.png)
We can demonstrate entering a subcommand here:
We'll demonstrate entering a subcommand here:
![Hello Output](https://i.imgur.com/a9RjE8S.png)

11
hello
View file

@ -3,21 +3,14 @@
PYTHON_ARGCOMPLETE_OK
"""
import os
# Uncomment these to customize your config file location
#os.environ['MILC_APP_NAME'] = 'hello'
#os.environ['MILC_APP_VERSION'] = '0.0.1'
#os.environ['MILC_APP_AUTHOR'] = 'MILC'
from milc import cli
@cli.argument('-c', '--comma', help='comma in output', default=True, action='store_boolean')
@cli.argument('-c', '--comma', arg_only=True, help='comma in output', default=True, action='store_boolean')
@cli.argument('-n', '--name', help='Name to greet', default='World')
@cli.entrypoint('Greet a user.')
def main(cli):
comma = ',' if cli.config.general.comma else ''
comma = ',' if cli.args.comma else ''
cli.log.debug('You used -v you lucky person!')
cli.log.info('Hello%s %s, from cli.log.info!', comma, cli.config.general.name)
cli.echo('{fg_red}Hello%s %s, from cli.echo!', comma, cli.config.general.name)

View file

@ -10,7 +10,7 @@ colors = ('black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow')
@cli.entrypoint('Show all the colors available to us.')
def main(cli):
cli.echo('|Normal | FG | ExtFG | BG | ExtBG |')
cli.echo('|Normal | FG | ExtFG | BG | ExtBG |')
for color in colors:
cli.echo(f'|{color:8}|{{fg_{color}}}xxxxxxxx{{fg_reset}}|{{fg_light{color}_ex}}xxxxxxxx{{fg_reset}}|{{bg_{color}}}xxxxxxxx{{bg_reset}}|{{bg_light{color}_ex}}xxxxxxxx{{bg_reset}}|')

View file

@ -8,6 +8,14 @@
| `INFO` | `{fg_blue}` |
| `DEBUG` | `{fg_cyan}` |
| `NOTSET` | `{style_reset_all}¯\\_(o_o)_/¯` |
If you'd like to use your own icon for a level instead you can simply redefine it:
```python
from milc.emoji import EMOJI_LOGLEVELS
EMOJI_LOGLEVELS['INFO'] = {fg_green}'
```
"""
EMOJI_LOGLEVELS = {
'CRITICAL': '{bg_red}{fg_white}¬_¬',

View file

@ -1,6 +1,4 @@
"""Functions to ask the user questions.
These functions can be used to query the user for information.
"""Sometimes you need to ask the user a question. MILC provides basic functions for collecting and validating user input. You can find these in the `milc.questions` module.
"""
from getpass import getpass
@ -101,7 +99,7 @@ def password(prompt='Enter password:', *args, confirm=False, confirm_prompt='Con
def question(prompt, *args, default=None, confirm=False, answer_type=str, validate=None, **kwargs):
"""Prompt the user to answer a question with a free-form input.
"""Allow the user to type in a free-form string to answer.
| Argument | Description |
|----------|-------------|
@ -144,9 +142,9 @@ def question(prompt, *args, default=None, confirm=False, answer_type=str, valida
def choice(heading, options, *args, default=None, confirm=False, prompt='Please enter your choice: ', **kwargs):
"""Present the user with a list of options and let them pick one.
"""Present the user with a list of options and let them select one.
Returns the value of the item they choose.
Users can enter either the number or the text of their choice. This will return the value of the item they choose, not the numerical index.
| Argument | Description |
|----------|-------------|

View file

@ -21,6 +21,7 @@ if __name__ == "__main__":
long_description=Path('README.md').read_text(),
long_description_content_type="text/markdown",
packages=setuptools.find_packages(exclude=('tests',)),
scripts=['milc-color'],
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',