updating latest from mac
This commit is contained in:
16
weather-cli/__dev/complex/README
Normal file
16
weather-cli/__dev/complex/README
Normal file
@@ -0,0 +1,16 @@
|
||||
$ complex_
|
||||
|
||||
complex is an example of building very complex cli
|
||||
applications that load subcommands dynamically from
|
||||
a plugin folder and other things.
|
||||
|
||||
All the commands are implemented as plugins in the
|
||||
`complex.commands` package. If a python module is
|
||||
placed named "cmd_foo" it will show up as "foo"
|
||||
command and the `cli` object within it will be
|
||||
loaded as nested Click command.
|
||||
|
||||
Usage:
|
||||
|
||||
$ pip install --editable .
|
||||
$ complex --help
|
||||
0
weather-cli/__dev/complex/complex/__init__.py
Normal file
0
weather-cli/__dev/complex/complex/__init__.py
Normal file
68
weather-cli/__dev/complex/complex/cli.py
Normal file
68
weather-cli/__dev/complex/complex/cli.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import os
|
||||
import sys
|
||||
import click
|
||||
|
||||
CONTEXT_SETTINGS = dict(auto_envvar_prefix='COMPLEX')
|
||||
|
||||
|
||||
class Environment(object):
|
||||
|
||||
def __init__(self):
|
||||
self.verbose = False
|
||||
self.home = os.getcwd()
|
||||
|
||||
def log(self, msg, *args):
|
||||
"""Logs a message to stderr."""
|
||||
if args:
|
||||
msg %= args
|
||||
click.echo(msg, file=sys.stderr)
|
||||
|
||||
def vlog(self, msg, *args):
|
||||
"""Logs a message to stderr only if verbose is enabled."""
|
||||
if self.verbose:
|
||||
self.log(msg, *args)
|
||||
|
||||
|
||||
pass_environment = click.make_pass_decorator(Environment, ensure=True)
|
||||
cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'commands'))
|
||||
|
||||
|
||||
class ComplexCLI(click.MultiCommand):
|
||||
|
||||
def list_commands(self, ctx):
|
||||
rv = []
|
||||
for filename in os.listdir(cmd_folder):
|
||||
if filename.endswith('.py') and \
|
||||
filename.startswith('cmd_'):
|
||||
rv.append(filename[4:-3])
|
||||
rv.sort()
|
||||
return rv
|
||||
|
||||
def get_command(self, ctx, name):
|
||||
try:
|
||||
if sys.version_info[0] == 2:
|
||||
name = name.encode('ascii', 'replace')
|
||||
mod = __import__('complex.commands.cmd_' + name,
|
||||
None, None, ['cli'])
|
||||
except ImportError:
|
||||
return
|
||||
return mod.cli
|
||||
|
||||
|
||||
@click.command(cls=ComplexCLI, context_settings=CONTEXT_SETTINGS)
|
||||
@click.option('--home', type=click.Path(exists=True, file_okay=False,
|
||||
resolve_path=True),
|
||||
help='Changes the folder to operate on.')
|
||||
@click.option('-v', '--verbose', is_flag=True,
|
||||
help='Enables verbose mode.')
|
||||
@pass_environment
|
||||
def cli(ctx, verbose, home):
|
||||
"""A complex command line interface."""
|
||||
ctx.verbose = verbose
|
||||
if home is not None:
|
||||
ctx.home = home
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
15
weather-cli/__dev/complex/complex/commands/cmd_init.py
Normal file
15
weather-cli/__dev/complex/complex/commands/cmd_init.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import click
|
||||
from complex.cli import pass_environment
|
||||
|
||||
|
||||
@click.command('init', short_help='Initializes a repo.')
|
||||
@click.argument('path', required=False, type=click.Path(resolve_path=True))
|
||||
@pass_environment
|
||||
def cli(ctx, path):
|
||||
"""Initializes a repository."""
|
||||
print(f'{ctx=}')
|
||||
print(f'{dir(ctx)=}')
|
||||
if path is None:
|
||||
path = ctx.home
|
||||
ctx.log('Initialized the repository in %s',
|
||||
click.format_filename(path))
|
||||
10
weather-cli/__dev/complex/complex/commands/cmd_status.py
Normal file
10
weather-cli/__dev/complex/complex/commands/cmd_status.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import click
|
||||
from complex.cli import pass_environment
|
||||
|
||||
|
||||
@click.command('status', short_help='Shows file changes.')
|
||||
@pass_environment
|
||||
def cli(ctx):
|
||||
"""Shows file changes in the current working directory."""
|
||||
ctx.log('Changed files: none')
|
||||
ctx.vlog('bla bla bla, debug info')
|
||||
16
weather-cli/__dev/complex/complex/text
Normal file
16
weather-cli/__dev/complex/complex/text
Normal file
@@ -0,0 +1,16 @@
|
||||
.
|
||||
├── __init__.py
|
||||
├── __pycache__
|
||||
│ ├── __init__.cpython-38.pyc
|
||||
│ └── cli.cpython-38.pyc
|
||||
├── cli.py
|
||||
├── commands
|
||||
│ ├── __init__.py
|
||||
│ ├── __pycache__
|
||||
│ │ ├── __init__.cpython-38.pyc
|
||||
│ │ ├── cmd_init.cpython-38.pyc
|
||||
│ │ └── cmd_status.cpython-38.pyc
|
||||
│ ├── cmd_init.py
|
||||
│ ├── cmd_status.py
|
||||
│ └── test
|
||||
└── text
|
||||
15
weather-cli/__dev/complex/setup.py
Normal file
15
weather-cli/__dev/complex/setup.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='click-example-complex',
|
||||
version='1.0',
|
||||
packages=['complex', 'complex.commands'],
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
'click',
|
||||
],
|
||||
entry_points='''
|
||||
[console_scripts]
|
||||
complex=complex.cli:cli
|
||||
''',
|
||||
)
|
||||
81
weather-cli/__dev/documentation_todo
Normal file
81
weather-cli/__dev/documentation_todo
Normal file
@@ -0,0 +1,81 @@
|
||||
|
||||
https://click.palletsprojects.com/en/7.x/#documentation
|
||||
https://dbader.org/blog/mastering-click-advanced-python-command-line-apps
|
||||
|
||||
Use click but with the poetry cleo style of importing and structuring the commands
|
||||
|
||||
@click.command() - creates a command out of the function
|
||||
|
||||
@click.argument('name') - creates an argument for the command
|
||||
(e.g poetry install, install is the argument)
|
||||
|
||||
@click.option - creates options for the command
|
||||
pass the option flags as strings, '--option', '-o'
|
||||
pass a help string , help='help string'
|
||||
specify to use an os env , envvar="API_KEY"
|
||||
|
||||
|
||||
|
||||
https://click.palletsprojects.com/en/5.x/options/
|
||||
|
||||
- use / in an option to set a flag as true of false
|
||||
|
||||
setting arguments: https://click.palletsprojects.com/en/5.x/arguments/#arguments
|
||||
|
||||
|
||||
https://github.com/pallets/click/tree/master/examples/complex/complex
|
||||
for a good example on structure using click in a module
|
||||
|
||||
|
||||
Callbacks and eager options to implement flags such as a version
|
||||
https://click.palletsprojects.com/en/5.x/options/#callbacks-and-eager-options
|
||||
|
||||
Can use
|
||||
@click.version_option(version=version)
|
||||
to implement a version
|
||||
|
||||
|
||||
|
||||
Make pass decorator explained:
|
||||
https://stackoverflow.com/questions/49511933/better-usage-of-make-pass-decorator-in-python-click
|
||||
|
||||
Show how to run command line programs from within a script from the example above ^^
|
||||
|
||||
|
||||
For complex example:
|
||||
|
||||
We use the class Environment to store any base arguments passed (e.g a path)
|
||||
This is optional, but is useful if we need an argument initially and want to do something to use it later in another subcommand.
|
||||
|
||||
This gets passed as a decorator to each command we create
|
||||
|
||||
We can then use this class to retrieve the inputs in each command in each module
|
||||
|
||||
We use this class to also handle logging, using click.echo() to print things if needed.
|
||||
|
||||
We avoid using the group functionality in click, by using a custom class that inherits from click.MultiCommand.
|
||||
|
||||
We can use this class to override the list_commands() and get_command() methods to list and do the commands we write.
|
||||
|
||||
Each one of those commands can go in their own folder, be decorated with the Environment class and have their commands passed
|
||||
|
||||
ctx refers to the instance of the class we passed in with the decorator when using click.make_pass_decorator()
|
||||
|
||||
when using this decorator, the first argument to the function will refer to the instanced class we passed in
|
||||
|
||||
|
||||
we can also import defaults which we can store in a class
|
||||
|
||||
say we have a class that reads default values from a config file locally. it also sets default values without a config file. we store all of this in a class using properties.
|
||||
|
||||
when creating an option we can use this class to fall back on if the option isnt passed to the command. to do this we create a subclass of click.Option, and use 2 closures.
|
||||
|
||||
2 closures are needed because we pass in 2 things: the settings instance class and the value of the option itself.
|
||||
|
||||
this allows to use the class attributes when creating options, and also allows us access to attributes when writing the commands, without needing two seperate instances
|
||||
|
||||
we could avoid doing this, by following the complex example. here there is one class which is passed in, but the default aren't set in click. instead the defauts are set in a class and passed in with the decorator.
|
||||
|
||||
we can have a class for each command that needs it, and we set the defaults by putting a if option is None in the command/subcommand itself, and setting the value to the class value if it's not provided.
|
||||
|
||||
if we do it this way, we would need a class for each command that needs it, and pass it in. or we could have one class, and use the method above to do it.
|
||||
87
weather-cli/__dev/testingclass.py
Normal file
87
weather-cli/__dev/testingclass.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import click
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def build_settings_option_class(settings_instance):
|
||||
def set_default(default_name):
|
||||
class Cls(click.Option):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['default'] = getattr(settings_instance, default_name)
|
||||
super(Cls, self).__init__(*args, **kwargs)
|
||||
|
||||
def handle_parse_result(self, ctx, opts, args):
|
||||
obj = ctx.find_object(type(settings_instance))
|
||||
if obj is None:
|
||||
ctx.obj = settings_instance
|
||||
|
||||
return super(Cls, self).handle_parse_result(ctx, opts, args)
|
||||
|
||||
return Cls
|
||||
|
||||
return set_default
|
||||
|
||||
|
||||
class Settings(object):
|
||||
def __init__(self):
|
||||
self.instance_disk_size = 100
|
||||
self.instance_disk_type = 'pd-ssd'
|
||||
|
||||
|
||||
# import pudb; pudb.set_trace()
|
||||
settings = Settings()
|
||||
settings_option_cls = build_settings_option_class(settings)
|
||||
pass_settings = click.make_pass_decorator(Settings)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.help_option('-h', '--help')
|
||||
@click.option(
|
||||
'-s',
|
||||
'--disk-size',
|
||||
cls=settings_option_cls('instance_disk_size'),
|
||||
help="Disk size",
|
||||
show_default=True,
|
||||
type=int,
|
||||
)
|
||||
@click.option(
|
||||
'-t',
|
||||
'--disk-type',
|
||||
cls=settings_option_cls('instance_disk_type'),
|
||||
help="Disk type",
|
||||
show_default=True,
|
||||
type=click.Choice(['pd-standard', 'pd-ssd']),
|
||||
)
|
||||
@pass_settings
|
||||
def create(settings_test, disk_size, disk_type):
|
||||
print(f'{settings_test.instance_disk_type=}')
|
||||
# print(f'{dir(settings_test)=}')
|
||||
print(disk_size)
|
||||
print(disk_type)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
commands = (
|
||||
'-t pd-standard -s 200',
|
||||
'-t pd-standard',
|
||||
'-s 200',
|
||||
'',
|
||||
'--help',
|
||||
)
|
||||
|
||||
time.sleep(1)
|
||||
print('Click Version: {}'.format(click.__version__))
|
||||
print('Python Version: {}'.format(sys.version))
|
||||
for cmd in commands:
|
||||
try:
|
||||
time.sleep(0.1)
|
||||
print('-----------')
|
||||
print('> ' + cmd)
|
||||
time.sleep(0.1)
|
||||
create(cmd.split())
|
||||
|
||||
except BaseException as exc:
|
||||
if str(exc) != '0' and not isinstance(
|
||||
exc, (click.ClickException, SystemExit)
|
||||
):
|
||||
raise
|
||||
Reference in New Issue
Block a user