Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86e0a36a92 |
28
README.md
28
README.md
@ -10,32 +10,14 @@ pip install magicli
|
||||
|
||||
## Get started
|
||||
|
||||
Hello world example.
|
||||
|
||||
Docstrings can be added to variables by using `typing.Annotated`.
|
||||
A short option can be added as a third argument to `Annotated`.
|
||||
|
||||
By default the first function is called automatically. All following functions in the file are treated as commands. Functions starting with an underscore are ignored.
|
||||
Basic usage example.
|
||||
By default, every function except for the `main` function is callable through command line arguments.
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from magicli import magicli
|
||||
|
||||
def hello(
|
||||
name: Annotated[str, 'Person to greet.'],
|
||||
amount: Annotated[int, 'How often to greet.', 'a']=1,
|
||||
):
|
||||
for _ in range(amount):
|
||||
print(f"Hello {name}!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
import magicli
|
||||
```
|
||||
|
||||
```bash
|
||||
$ hello world 3
|
||||
Hello world!
|
||||
Hello world!
|
||||
Hello world!
|
||||
def main():
|
||||
magicli()
|
||||
```
|
||||
|
||||
### Define name of CLI in `setup.py`
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
def hi(
|
||||
flag=False,
|
||||
true=True,
|
||||
false=False,
|
||||
none=None,
|
||||
empty='',
|
||||
string='string',
|
||||
lst=[],
|
||||
dct={},
|
||||
zero=0,
|
||||
one=1,
|
||||
):
|
||||
# TODO:
|
||||
# - `--flag=0` still leads to True value
|
||||
if flag:
|
||||
print('Flag is set')
|
||||
print(flag)
|
||||
# print('Hi', name)
|
||||
...
|
||||
|
||||
|
||||
import magicli
|
||||
@ -1 +0,0 @@
|
||||
__version__ = '0.3.0'
|
||||
@ -1,412 +0,0 @@
|
||||
import inspect
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from pargv import parse_args
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
import tomli_w
|
||||
|
||||
|
||||
def magicli(exclude=['main'], glbls=None, argv=None):
|
||||
"""
|
||||
Get all functions from calling file and interprets them as CLI commands.
|
||||
Parses command line arguments for function to call
|
||||
and calls it with all specified arguments.
|
||||
Displays a help message and exits if the --help flag is set
|
||||
or if no callable function is found.
|
||||
Errors out with a TypeError if the specified arguments are invalid.
|
||||
"""
|
||||
|
||||
glbls = glbls if glbls else inspect.currentframe().f_back.f_globals
|
||||
|
||||
settings = {
|
||||
'indent': 2,
|
||||
'gap': 2,
|
||||
'max_width': 60,
|
||||
'min_column_width': 13,
|
||||
'max_column_width': 25,
|
||||
'display_arguments_more_than': 3,
|
||||
}
|
||||
if 'magicli' in glbls:
|
||||
settings.update(glbls['magicli'])
|
||||
|
||||
argv = argv if argv else sys.argv
|
||||
name, args, kwargs = format_args(argv)
|
||||
|
||||
if 'version' in kwargs or 'v' in kwargs:
|
||||
print(glbls['__version__']) if '__version__' in glbls else print('Unknown version.')
|
||||
exit()
|
||||
|
||||
function_to_call, *commands = [f for f in filter_functions(glbls) if f.__name__ not in exclude and not f.__name__.startswith('_')]
|
||||
command_names = [f.__name__ for f in commands]
|
||||
|
||||
# Call command if specified
|
||||
if args and args[0] in command_names:
|
||||
function_to_call = glbls.get(args[0])
|
||||
name += f" {args[0]}"
|
||||
commands = []
|
||||
args = args[1:]
|
||||
|
||||
config = get_config(function_to_call, commands)
|
||||
config['name'] = name
|
||||
config['settings'] = settings
|
||||
short_options = {v['short_option']: k for k, v in config['options'].items() if 'short_option' in v}
|
||||
kwargs = replace_short_options(kwargs, short_options)
|
||||
|
||||
if 'help' in kwargs or 'h' in kwargs:
|
||||
print(help_message(config))
|
||||
exit()
|
||||
|
||||
if not len(args) == len(config['arguments']):
|
||||
print(help_message(config))
|
||||
exit()
|
||||
|
||||
args, kwargs = cast_types(args, kwargs, config)
|
||||
|
||||
try:
|
||||
function_to_call(*args, **kwargs)
|
||||
except TypeError as e:
|
||||
if re.search('got an unexpected keyword argument', e.args[0]):
|
||||
print(help_message(config))
|
||||
exit()
|
||||
else:
|
||||
raise
|
||||
exit()
|
||||
|
||||
|
||||
def cast_types(args, kwargs, config):
|
||||
for i, values in enumerate(config['arguments'].values()):
|
||||
try:
|
||||
if 'type' in values:
|
||||
args[i] = values['type'](args[i])
|
||||
except:
|
||||
print(help_message(config))
|
||||
exit()
|
||||
|
||||
for k, v in kwargs.items():
|
||||
try:
|
||||
kwargs[k] = config['options'][k]['type'](v)
|
||||
except:
|
||||
print(help_message(config))
|
||||
exit()
|
||||
|
||||
return (args, kwargs)
|
||||
|
||||
|
||||
def format_args(argv):
|
||||
(name, *args), kwargs = parse_args(argv)
|
||||
return name, args, kwargs
|
||||
|
||||
|
||||
def filter_functions(glbls):
|
||||
"""
|
||||
Gets list of functions from the globals variable of a specific module.
|
||||
"""
|
||||
return [v for v in glbls.values() if inspect.isfunction(v) and v.__module__ == glbls['__name__']]
|
||||
|
||||
|
||||
def replace_short_options(kwargs, short_options):
|
||||
return dict((short_options[k], v) if k in short_options else (k, v) for k, v in kwargs.items())
|
||||
|
||||
|
||||
def help_message(config):
|
||||
"""
|
||||
Automatically create a help message.
|
||||
"""
|
||||
usage = [config['name']]
|
||||
help_text = []
|
||||
|
||||
if config['function']['docstring']:
|
||||
help_text.append({
|
||||
'lines': [['', truncate_docstring(config['function']['docstring'])]],
|
||||
'max_width': config['settings']['max_width']+config['settings']['max_column_width'],
|
||||
'min_column_width': 0,
|
||||
'gap': 0,
|
||||
})
|
||||
|
||||
if config['arguments']:
|
||||
if len(config['arguments']) > config['settings']['display_arguments_more_than']:
|
||||
usage.append('arguments')
|
||||
else:
|
||||
usage.append(' '.join(list(config['arguments'].keys())))
|
||||
if len(config['arguments']) > config['settings']['display_arguments_more_than'] or 'docstring' in config['arguments']:
|
||||
help_text.append({'heading': 'Arguments:', 'lines': make_lines(config['arguments'])})
|
||||
|
||||
if config['options']:
|
||||
usage.append('[options]')
|
||||
help_text.append({'heading': 'Options:', 'lines': make_lines(config['options'])})
|
||||
|
||||
usage = [' '.join(usage)]
|
||||
|
||||
if config['commands']:
|
||||
usage.append(f"{config['name']} command [...]")
|
||||
help_text.append({'heading': 'Commands:', 'lines': make_lines(config['commands'])})
|
||||
|
||||
help_text = [config['settings'] | h for h in [{'heading': 'Usage:', 'lines': usage}] + help_text]
|
||||
return format_help_message(help_text)
|
||||
|
||||
|
||||
def make_lines(config):
|
||||
result = []
|
||||
for key, values in config.items():
|
||||
left_side = key
|
||||
right_side = ''
|
||||
if 'docstring' in values:
|
||||
right_side += truncate_docstring(values['docstring'])
|
||||
if 'default' in values:
|
||||
if values['type'] not in (bool, type(None)):
|
||||
if hasattr(values['default'], '__len__') and not len(values['default']):
|
||||
...
|
||||
else:
|
||||
right_side += f" (default: {values['default']})"
|
||||
left_side = f"--{left_side}"
|
||||
if 'short_option' in values:
|
||||
left_side = f"-{values['short_option']}, {left_side}"
|
||||
right_side = right_side.lstrip()
|
||||
result.append([left_side, right_side])
|
||||
return result
|
||||
|
||||
|
||||
def truncate_docstring(docstring):
|
||||
if not docstring:
|
||||
return ''
|
||||
truncated_docstring = []
|
||||
for line in docstring.split('\n'):
|
||||
if not line.lstrip():
|
||||
break
|
||||
truncated_docstring.append(line.lstrip())
|
||||
return '\n'.join(truncated_docstring)
|
||||
|
||||
|
||||
def format_help_message(list_of_dicts):
|
||||
"""Usage example:
|
||||
|
||||
format_help_message([
|
||||
{'lines': ['-, config-all', 'All files.'], 'heading': 'Options:'},
|
||||
{'lines': ['run', 'Run script.'], 'heading': 'Commands:'},
|
||||
{'lines': 'Copyright 2023'},
|
||||
])
|
||||
"""
|
||||
return '\n\n'.join(format_block(**kwargs) for kwargs in list_of_dicts)
|
||||
|
||||
|
||||
def format_block(lines, heading='',
|
||||
indent=2,
|
||||
gap=2,
|
||||
max_width=60,
|
||||
min_column_width=13,
|
||||
max_column_width=25,
|
||||
**_ # Ignore kwargs not specified above
|
||||
):
|
||||
args_lengths = [l[0] for l in lines if len(l[0]) <= max_column_width]
|
||||
max_arg_length = len(max(args_lengths, key=len)) if args_lengths else 0
|
||||
column_width = min(max(max_arg_length, min_column_width), max_column_width-indent)
|
||||
full_width = min(max_width, os.get_terminal_size()[0])
|
||||
|
||||
if isinstance(lines[0], list):
|
||||
original_lines = lines.copy()
|
||||
lines = []
|
||||
for line in original_lines:
|
||||
if line[1]:
|
||||
line[1] = line[1].replace('\n', '\n'+indent*' ')
|
||||
lines_right = break_lines(line[1], full_width)
|
||||
|
||||
if len(line[0]) > column_width:
|
||||
lines.append(line[0])
|
||||
else:
|
||||
lines.append(f"{line[0]:{column_width}}{gap*' '}{lines_right.pop(0) if lines_right else ''}")
|
||||
|
||||
for line in lines_right:
|
||||
lines.append(f"{(column_width+gap)*' '}{line}")
|
||||
|
||||
elif isinstance(lines, str):
|
||||
lines = [lines]
|
||||
|
||||
if heading:
|
||||
return '\n'.join([heading]+['\n'.join([indent*' '+l for l in lines])])
|
||||
return '\n'.join(['\n'.join([indent*' '+l for l in lines])])
|
||||
|
||||
|
||||
def get_config(function_to_call, commands):
|
||||
specs = inspect.getfullargspec(function_to_call)
|
||||
|
||||
config = {
|
||||
'arguments': {},
|
||||
'options': {},
|
||||
'commands': {},
|
||||
'function': {'docstring': function_to_call.__doc__},
|
||||
}
|
||||
|
||||
default_index = len(specs.args)-len(specs.defaults) if specs.defaults else len(specs.args)
|
||||
|
||||
for i, arg in enumerate(specs.args):
|
||||
key = arg.replace('_', '-')
|
||||
|
||||
# default
|
||||
category = 'arguments'
|
||||
if i >= default_index:
|
||||
category = 'options'
|
||||
config[category][key] = {'default': specs.defaults[i-default_index]}
|
||||
else:
|
||||
config[category][key] = {}
|
||||
# type
|
||||
try:
|
||||
config[category][key]['type'] = specs.annotations[arg].__args__[0]
|
||||
except:
|
||||
try:
|
||||
if isinstance(specs.annotations[arg], type):
|
||||
config[category][key]['type'] = specs.annotations[arg]
|
||||
else:
|
||||
config[category][key]['type'] = type(specs.annotations[arg])
|
||||
except:
|
||||
if category == 'options':
|
||||
config[category][key]['type'] = type(config[category][key]['default'])
|
||||
# docstring
|
||||
try:
|
||||
config[category][key]['docstring'] = specs.annotations[arg].__metadata__[0]
|
||||
except: ...
|
||||
# short_option
|
||||
try:
|
||||
config[category][key]['short_option'] = specs.annotations[arg].__metadata__[1][0]
|
||||
except: ...
|
||||
|
||||
config['options']['-v, --version'] = {'docstring': 'Show version information.'}
|
||||
config['options']['-h, --help'] = {'docstring': 'Show help message.'}
|
||||
|
||||
for command in commands:
|
||||
config['commands'][command.__name__] = {'docstring': command.__doc__}
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def break_lines(lines, width):
|
||||
"""
|
||||
Break lines so that they don't exceed a certain width.
|
||||
Returns the lines as a list.
|
||||
"""
|
||||
if not lines:
|
||||
return []
|
||||
result = []
|
||||
while len(lines) > width:
|
||||
cut = lines[:width].rfind(' ')
|
||||
if cut == -1:
|
||||
cut = min(len(lines), width)
|
||||
result.append(lines[:cut])
|
||||
lines = lines[cut:].lstrip()
|
||||
result.append(lines)
|
||||
return result
|
||||
|
||||
|
||||
def load_pyproject_toml(dirname, maxdepth=3, filename='pyproject.toml'):
|
||||
"""Find and load pyproject.toml of current project."""
|
||||
|
||||
for _ in range(maxdepth):
|
||||
if filename in os.listdir(dirname):
|
||||
with open(os.path.join(dirname, filename), 'rb') as f:
|
||||
config = tomllib.load(f)
|
||||
return config.get('tool', {}).get('magicli', {})
|
||||
|
||||
# Go to parent directory
|
||||
dirname = os.path.abspath(os.path.join(dirname, os.pardir))
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
# def write_pyproject_toml(path):
|
||||
|
||||
# name = prompt('Module name')
|
||||
# author = prompt('Author')
|
||||
# email = prompt('Author email')
|
||||
# cli_name = prompt('CLI name', default=name)
|
||||
# cli_path = prompt('CLI path')
|
||||
# authors = '[{' + f'name = "{author}", email = "{email}"' + '}]'
|
||||
|
||||
# with open(path, 'w', encoding='utf-8') as f:
|
||||
# f.write(TEMPLATE.format(
|
||||
# name=name,
|
||||
# authors=authors,
|
||||
# cli_name=cli_name,
|
||||
# cli_path=cli_path,
|
||||
# ))
|
||||
|
||||
|
||||
# def prompt(question, default=''):
|
||||
# if response := input(f"{question} [{default}]: "):
|
||||
# return response
|
||||
# else:
|
||||
# return default
|
||||
|
||||
|
||||
# TEMPLATE = """\
|
||||
# [build-system]
|
||||
# requires = ["flit_core >=3.2,<4"]
|
||||
# build-backend = "flit_core.buildapi"
|
||||
|
||||
# [project]
|
||||
# name = "{name}"
|
||||
# authors = [{authors}]
|
||||
# dynamic = ["version", "description"]
|
||||
# dependencies = ["magicli"]
|
||||
|
||||
# [project.scripts]
|
||||
# "{cli_name}" = "{cli_path}"
|
||||
|
||||
# # Uncomment to add user configuration
|
||||
# #[tool.magicli]
|
||||
# #test = "test"
|
||||
# """
|
||||
|
||||
|
||||
def write_pyproject_toml(path):
|
||||
if os.path.exists(path):
|
||||
with open(path, "rb") as f:
|
||||
config = tomli.load(f)
|
||||
else:
|
||||
config = {
|
||||
"build-system": {
|
||||
"requires": ["flit_core >=3.2,<4"],
|
||||
"build-backend": "flit_core.buildapi",
|
||||
},
|
||||
"project": {
|
||||
"name": "",
|
||||
"authors": [],
|
||||
"dynamic": ["version", "description"],
|
||||
"dependencies": ["magicli"],
|
||||
},
|
||||
}
|
||||
|
||||
if not 'name' in config['project'] or not config['project']['name']:
|
||||
config['project']['name'] = input('Module name: ')
|
||||
|
||||
if not 'authors' in config['project'] or not config['project']['authors']:
|
||||
author = input('Author: ')
|
||||
email = input('Author email: ')
|
||||
config['project']['authors'] = [{"author": author, "email": email}]
|
||||
|
||||
if not 'scripts' in config['project']:
|
||||
config['project']['scripts'] = {}
|
||||
|
||||
if not config['project']['scripts']:
|
||||
cli_name = input('CLI name: ')
|
||||
cli_path = input('CLI path: ')
|
||||
config['project']['scripts'][cli_name] = cli_path
|
||||
|
||||
with open(path, 'wb') as f:
|
||||
tomli_w.dump(config, f)
|
||||
|
||||
|
||||
def first_calling_frame():
|
||||
for s in reversed(inspect.stack()):
|
||||
if s.code_context == None:
|
||||
continue
|
||||
if s.code_context[0].lstrip().startswith('import magicli'):
|
||||
return s.frame
|
||||
return None
|
||||
|
||||
|
||||
frame = first_calling_frame()
|
||||
if frame != None:
|
||||
magicli(glbls=frame.f_globals, exclude=[])
|
||||
@ -1,10 +1,3 @@
|
||||
[tool.magicli]
|
||||
indent = 2
|
||||
max_width = 60
|
||||
min_column_width = 13
|
||||
max_column_width = 25
|
||||
display_arguments_more_than = 3
|
||||
|
||||
[build-system]
|
||||
requires = ["flit_core >=3.2,<4"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
@ -22,9 +15,8 @@ classifiers = [
|
||||
dependencies = [
|
||||
"pargv>=0.2.0",
|
||||
"tomli>=1.1.0; python_version < '3.11'",
|
||||
"tomli_w",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dynamic = ["version", "description"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
|
||||
@ -8,17 +8,16 @@ with open('README.md') as f:
|
||||
|
||||
setup(
|
||||
name='magicli',
|
||||
version='0.3.0',
|
||||
version='0.2.0',
|
||||
description=description,
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
package_dir={'': 'magicli'},
|
||||
package_dir={'': 'src'},
|
||||
py_modules=[
|
||||
'magicli',
|
||||
],
|
||||
install_requires=[
|
||||
'pargv>=0.2.0',
|
||||
'tomli_w',
|
||||
'pargv'
|
||||
],
|
||||
extras_require={
|
||||
'dev':[
|
||||
78
src/magicli.py
Normal file
78
src/magicli.py
Normal file
@ -0,0 +1,78 @@
|
||||
import inspect
|
||||
import sys
|
||||
import os
|
||||
from pargv import parse_args
|
||||
|
||||
|
||||
def magicli(exclude=['main'], help_message=True, glbls=None, argv=None):
|
||||
"""
|
||||
Get all functions from calling file and interprets them as CLI commands.
|
||||
Parses command line arguments for function to call
|
||||
and calls it with all specified arguments.
|
||||
Displays a help message and exits if the --help flag is set
|
||||
or if no callable function is found.
|
||||
Errors out with a TypeError if the specified arguments are invalid.
|
||||
"""
|
||||
glbls = glbls if glbls else inspect.currentframe().f_back.f_globals
|
||||
argv = argv if argv else sys.argv
|
||||
app_name, args, kwargs = format_args(argv)
|
||||
|
||||
functions = [f for f in filter_functions(glbls) if f.__name__ not in exclude]
|
||||
|
||||
if help_message and 'help' in kwargs:
|
||||
print_help_and_exit(app_name, functions)
|
||||
|
||||
possible_commands = [f.__name__ for f in functions]
|
||||
function_name = None
|
||||
|
||||
if len(args) and args[0] in possible_commands:
|
||||
function_name = args[0]
|
||||
args = args[1:]
|
||||
elif app_name in possible_commands:
|
||||
function_name = app_name
|
||||
else:
|
||||
print_help_and_exit(app_name, functions)
|
||||
|
||||
function_to_call = glbls.get(function_name)
|
||||
|
||||
try:
|
||||
function_to_call(*args, **kwargs)
|
||||
except TypeError as e:
|
||||
print_error(e)
|
||||
raise
|
||||
|
||||
|
||||
def print_error(e):
|
||||
print('\x1b[91mError:\x1b[0m ', end='')
|
||||
print(e)
|
||||
|
||||
|
||||
def format_args(argv):
|
||||
args, kwargs = parse_args(argv)
|
||||
app_name = os.path.basename(args[0])
|
||||
args = args[1:]
|
||||
return app_name, args, kwargs
|
||||
|
||||
|
||||
def filter_functions(args):
|
||||
"""
|
||||
Gets list of functions from the globals variable of a specific module (default: __main__).
|
||||
"""
|
||||
return [v for k, v in args.items() if inspect.isfunction(v) and v.__module__ == args['__name__']]
|
||||
|
||||
|
||||
def print_help_and_exit(app_name, functions):
|
||||
"""
|
||||
Print the help message based on functions contained in the calling file.
|
||||
Exits after displaying the help message.
|
||||
"""
|
||||
print('Usage:')
|
||||
for f in functions:
|
||||
words = []
|
||||
if app_name != f.__name__:
|
||||
words.append(app_name)
|
||||
words.append(f.__name__)
|
||||
specs = inspect.getfullargspec(f)
|
||||
words += ['--' + arg for arg in specs.args]
|
||||
print(' '*4 + ' '.join(words))
|
||||
exit()
|
||||
@ -1,23 +0,0 @@
|
||||
from unittest import mock
|
||||
import pytest
|
||||
from magicli import magicli
|
||||
|
||||
|
||||
def test_help_message(capsys):
|
||||
inputs = [
|
||||
'appname --help',
|
||||
'appname command --help',
|
||||
'appname command --name=Name --amount=3 --help',
|
||||
]
|
||||
help_message='Usage:\n appname command_with_arguments --positional --optional\n'
|
||||
for i in inputs:
|
||||
args = i.split()
|
||||
with mock.patch('sys.argv', args):
|
||||
with pytest.raises(SystemExit):
|
||||
magicli(exclude=['test_help_message'])
|
||||
out, err = capsys.readouterr()
|
||||
assert out.startswith(help_message)
|
||||
|
||||
|
||||
def command_with_arguments(positional, optional=True):
|
||||
pass
|
||||
@ -1,11 +0,0 @@
|
||||
from magicli.magicli import break_lines
|
||||
|
||||
|
||||
def test_no_input():
|
||||
assert break_lines(None, 0) == []
|
||||
|
||||
def test_short_line():
|
||||
assert break_lines('This is a line.', 8) == [
|
||||
'This is',
|
||||
'a line.',
|
||||
]
|
||||
@ -1,23 +0,0 @@
|
||||
from magicli.magicli import cast_types
|
||||
|
||||
|
||||
def test_cast_string_in_args_to_int():
|
||||
args = ['2']
|
||||
config = {
|
||||
'arguments': {'amount': {'type': int}},
|
||||
'options': {},
|
||||
}
|
||||
assert cast_types(args, {}, config) == ([2], {})
|
||||
|
||||
def test_not_cast_string_in_args_to_int():
|
||||
args = ['2']
|
||||
config = {'arguments': {}, 'options': {}}
|
||||
assert cast_types(args, {}, config) == (['2'], {})
|
||||
|
||||
def test_cast_string_in_kwargs_to_int():
|
||||
kwargs = {'bbb': '2'}
|
||||
config = {
|
||||
'arguments': {},
|
||||
'options': {'bbb': {'type': int}},
|
||||
}
|
||||
assert cast_types([], kwargs, config) == ([], {'bbb': 2})
|
||||
@ -1,6 +0,0 @@
|
||||
from magicli.magicli import filter_functions
|
||||
|
||||
|
||||
def test_one_function():
|
||||
functions = filter_functions(globals())
|
||||
assert functions[0] == test_one_function
|
||||
@ -1,5 +0,0 @@
|
||||
from magicli.magicli import first_calling_frame
|
||||
|
||||
|
||||
def test_no_calling_frame():
|
||||
assert first_calling_frame() == None
|
||||
@ -1,8 +0,0 @@
|
||||
from magicli.magicli import format_args
|
||||
|
||||
|
||||
def test_empty_argv():
|
||||
name, args, kwargs = format_args([])
|
||||
assert isinstance(name, str)
|
||||
assert isinstance(args, list)
|
||||
assert isinstance(kwargs, dict)
|
||||
@ -1,35 +0,0 @@
|
||||
from magicli.magicli import format_block
|
||||
from unittest import mock
|
||||
|
||||
|
||||
def get_terminal_size():
|
||||
return (80, 20)
|
||||
|
||||
def test_empty_lines():
|
||||
with mock.patch('os.get_terminal_size', get_terminal_size):
|
||||
min_column_width=5
|
||||
indent=2
|
||||
gap=2
|
||||
assert format_block([['', '']],
|
||||
min_column_width=min_column_width,
|
||||
indent=indent,
|
||||
gap=gap
|
||||
) == ' '*(min_column_width+indent+gap)
|
||||
|
||||
def test_two_lines():
|
||||
with mock.patch('os.get_terminal_size', get_terminal_size):
|
||||
assert format_block([
|
||||
['--help', 'Show help message.'],
|
||||
['-v, --version', 'Show version information.'],
|
||||
], min_column_width=0) == \
|
||||
' --help Show help message.\n' +\
|
||||
' -v, --version Show version information.'
|
||||
|
||||
def test_two_lines_with_minimum_width():
|
||||
with mock.patch('os.get_terminal_size', get_terminal_size):
|
||||
assert format_block([
|
||||
['--help', 'Show help message.'],
|
||||
['-v, --version', 'Show version information.'],
|
||||
], min_column_width=20) == \
|
||||
' --help Show help message.\n' +\
|
||||
' -v, --version Show version information.'
|
||||
@ -1,10 +0,0 @@
|
||||
from magicli.magicli import format_help_message
|
||||
from unittest import mock
|
||||
|
||||
|
||||
def get_terminal_size():
|
||||
return (80, 20)
|
||||
|
||||
def test_():
|
||||
with mock.patch('os.get_terminal_size', get_terminal_size):
|
||||
assert '\n\n' in format_help_message([{'lines': [['', '']]}, {'lines': [['', '']]}])
|
||||
@ -1,12 +0,0 @@
|
||||
from magicli.magicli import get_config
|
||||
|
||||
|
||||
def function_to_call(name, amount=1): ...
|
||||
|
||||
def command(): ...
|
||||
|
||||
def test_basic_config():
|
||||
config = get_config(function_to_call, [command])
|
||||
assert config['arguments'] == {'name': {}}
|
||||
assert config['options']['amount'] == {'default': 1, 'type': int}
|
||||
assert config['commands']['command'] == {'docstring': None}
|
||||
@ -1,20 +1,23 @@
|
||||
from magicli.magicli import help_message
|
||||
from unittest import mock
|
||||
import pytest
|
||||
from magicli import magicli
|
||||
|
||||
|
||||
def get_terminal_size():
|
||||
return (80, 20)
|
||||
def test_help_message(capsys):
|
||||
inputs = [
|
||||
'appname --help',
|
||||
'appname command --help',
|
||||
'appname command --name=Name --amount=3 --help',
|
||||
]
|
||||
help_message='Usage:\n appname command_with_arguments --positional --optional\n'
|
||||
for i in inputs:
|
||||
args = i.split()
|
||||
with mock.patch('sys.argv', args):
|
||||
with pytest.raises(SystemExit):
|
||||
magicli(exclude=['test_help_message'])
|
||||
out, err = capsys.readouterr()
|
||||
assert out.startswith(help_message)
|
||||
|
||||
def test_minimal_example():
|
||||
config = {
|
||||
'name': '',
|
||||
'function': {'docstring': ''},
|
||||
'arguments': {'argument': []},
|
||||
'options': {},
|
||||
'commands': {},
|
||||
'settings': {'display_arguments_more_than': 0},
|
||||
}
|
||||
with mock.patch('os.get_terminal_size', get_terminal_size):
|
||||
assert isinstance(help_message(config), str)
|
||||
assert help_message(config).startswith('Usage:')
|
||||
assert help_message(config).rstrip().endswith('argument')
|
||||
|
||||
def command_with_arguments(positional, optional=True):
|
||||
pass
|
||||
@ -1,6 +0,0 @@
|
||||
from magicli.magicli import make_lines
|
||||
|
||||
|
||||
def test_minimal_example():
|
||||
config = {'': {}}
|
||||
assert make_lines(config) == [['', '']]
|
||||
@ -1,9 +0,0 @@
|
||||
from magicli.magicli import replace_short_options
|
||||
|
||||
|
||||
def test_short_argument():
|
||||
kwargs = {'h': True}
|
||||
short_options = {'h': 'help'}
|
||||
assert replace_short_options(kwargs, short_options) == {
|
||||
'help': True
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
from magicli.magicli import truncate_docstring
|
||||
|
||||
|
||||
def test_no_docstring():
|
||||
assert truncate_docstring(None) == ''
|
||||
|
||||
def test_padded_multiline_string():
|
||||
assert truncate_docstring("""This
|
||||
is
|
||||
a docstring.""") == "This\nis\na docstring."
|
||||
|
||||
def test_remove_bottom():
|
||||
assert truncate_docstring("""First line\n\nsecond line.""") == "First line"
|
||||
assert truncate_docstring("""First line\n \nsecond line.""") == "First line"
|
||||
Loading…
x
Reference in New Issue
Block a user