Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
87
README.md
87
README.md
@ -1,87 +0,0 @@
|
|||||||
# magiᴄʟɪ✨
|
|
||||||
|
|
||||||
Automatically turn functions of file into command line interface.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```
|
|
||||||
pip install magicli
|
|
||||||
```
|
|
||||||
|
|
||||||
## Get started
|
|
||||||
|
|
||||||
Basic usage example.
|
|
||||||
By default, every function except for the `main` function is callable through command line arguments.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from magicli import magicli
|
|
||||||
|
|
||||||
def main():
|
|
||||||
magicli()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Define name of CLI in `setup.py`
|
|
||||||
|
|
||||||
In order to define the name of the CLI, it needs to be defined in the `setup.py` file. The following code sets up a sample CLI with the following folder structure.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
hello/
|
|
||||||
└── setup.py
|
|
||||||
└── hello.py
|
|
||||||
```
|
|
||||||
|
|
||||||
`setup.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='hello',
|
|
||||||
version='0.1.0',
|
|
||||||
install_requires=[
|
|
||||||
'magicli'
|
|
||||||
],
|
|
||||||
entry_points={
|
|
||||||
'console_scripts':[
|
|
||||||
'hello=hello:main'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
`hello.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
from magicli import magicli
|
|
||||||
|
|
||||||
def main():
|
|
||||||
magicli()
|
|
||||||
|
|
||||||
def hello(name='World', amount=1):
|
|
||||||
for _ in range(int(amount)):
|
|
||||||
print(f'Hello {name}!')
|
|
||||||
```
|
|
||||||
|
|
||||||
The script can then be called in the following way.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
hello Name --amount 3
|
|
||||||
```
|
|
||||||
|
|
||||||
This outputs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
Hello Name!
|
|
||||||
Hello Name!
|
|
||||||
Hello Name!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Help message
|
|
||||||
|
|
||||||
By default, a help message will be created based on the available functions.
|
|
||||||
For the example above, calling `hello --help` will display this help message.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
Usage:
|
|
||||||
hello --name --amount
|
|
||||||
```
|
|
||||||
18
docs/example.py
Normal file
18
docs/example.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
Usage:
|
||||||
|
hello <name> [--amount=<int>]
|
||||||
|
|
||||||
|
-a=<int> --amount=<int> How often to greet
|
||||||
|
"""
|
||||||
|
|
||||||
|
from docopt import docopt
|
||||||
|
from magicli import magicli
|
||||||
|
|
||||||
|
|
||||||
|
def cli():
|
||||||
|
magicli(docopt(__doc__))
|
||||||
|
|
||||||
|
|
||||||
|
def main(name, amount = 1):
|
||||||
|
for _ in range(int(amount)):
|
||||||
|
print(f'Hello {name}!')
|
||||||
61
magicli.py
Normal file
61
magicli.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
# Get started
|
||||||
|
|
||||||
|
```python
|
||||||
|
from docopt import docopt
|
||||||
|
from magicli import magicli
|
||||||
|
|
||||||
|
args = magically(docopt(__doc__))
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def magicli(args, glbls=None, entry_point='main'):
|
||||||
|
"""
|
||||||
|
Calls all callable functions with all arguments.
|
||||||
|
"""
|
||||||
|
# Get the `globals()` dict of the file from where the function is called.
|
||||||
|
if not glbls:
|
||||||
|
glbls = inspect.currentframe().f_back.f_globals
|
||||||
|
|
||||||
|
cleaned_args = clean_args(args)
|
||||||
|
args = args_set_in_cli(cleaned_args)
|
||||||
|
|
||||||
|
# Add main function to possible callable functions.
|
||||||
|
# Main function will be called if it exists.
|
||||||
|
if entry_point not in args:
|
||||||
|
args[entry_point] = True
|
||||||
|
|
||||||
|
for arg in args:
|
||||||
|
if arg in glbls:
|
||||||
|
func = glbls.get(arg)
|
||||||
|
func_args = inspect.getargspec(func).args
|
||||||
|
kwargs = {arg:args[arg] for arg in args if arg in func_args}
|
||||||
|
func(**kwargs)
|
||||||
|
|
||||||
|
return cleaned_args
|
||||||
|
|
||||||
|
|
||||||
|
def clean_args(args):
|
||||||
|
"""
|
||||||
|
Creates a new dict of variables converted to correct function names.
|
||||||
|
"""
|
||||||
|
return {parse_function_name(key): args[key] for key in args}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_function_name(func):
|
||||||
|
"""
|
||||||
|
Convert variables to valid python function names.
|
||||||
|
"""
|
||||||
|
for char in ['<', '>', '-']:
|
||||||
|
func = func.strip(char)
|
||||||
|
return func.replace('-', '_')
|
||||||
|
|
||||||
|
|
||||||
|
def args_set_in_cli(args):
|
||||||
|
"""
|
||||||
|
Returns a list of all dictionary entries that are specified in cli.
|
||||||
|
"""
|
||||||
|
return {arg:args[arg] for arg in args if args[arg]}
|
||||||
@ -1,24 +0,0 @@
|
|||||||
[build-system]
|
|
||||||
requires = ["flit_core >=3.2,<4"]
|
|
||||||
build-backend = "flit_core.buildapi"
|
|
||||||
|
|
||||||
[project]
|
|
||||||
name = "magicli"
|
|
||||||
description = "Automatically turn functions of file into command line interface."
|
|
||||||
authors = [{name = "Patrick Elmer", email = "patrick@elmer.ws"}]
|
|
||||||
license = {file = "LICENSE"}
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.7"
|
|
||||||
classifiers = [
|
|
||||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)"
|
|
||||||
]
|
|
||||||
dependencies = [
|
|
||||||
"pargv>=0.2.0",
|
|
||||||
"tomli>=1.1.0; python_version < '3.11'",
|
|
||||||
]
|
|
||||||
dynamic = ["version", "description"]
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
dev = [
|
|
||||||
'pytest >= 6.1.1',
|
|
||||||
]
|
|
||||||
97
readme.md
Normal file
97
readme.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# magiᴄʟɪ✨
|
||||||
|
|
||||||
|
Automatically call args parsed by `docopt` as functions.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install git+https://git.beelm.eu/patrick/magicli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
Basic usage example.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from docopt import docopt
|
||||||
|
from magicli import magicli
|
||||||
|
def cli():
|
||||||
|
magicli(docopt(__doc__))
|
||||||
|
```
|
||||||
|
|
||||||
|
Functions that share a name with the keys or `dict` returned by `docopt` are automatically called with all required args if specified (Note that keys are converted to valid python function names, i.e. stripping the characters `<` `>` `-` and replacing `-` with `_`).
|
||||||
|
|
||||||
|
All functions that are called this way need to be imported.
|
||||||
|
|
||||||
|
`magicli` also returns a dict with converted keys, and these can be used as `args` if needed.
|
||||||
|
|
||||||
|
```python
|
||||||
|
args = magicli(docopt(__doc__))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Minimal `Hello World` example
|
||||||
|
|
||||||
|
After installing, this example can be called from the command line.
|
||||||
|
|
||||||
|
### hello.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Usage:
|
||||||
|
magicli_example <name> [--amount=<int>]
|
||||||
|
|
||||||
|
-a=<int> --amount=<int> How often to greet
|
||||||
|
"""
|
||||||
|
|
||||||
|
from docopt import docopt
|
||||||
|
from magicli import magicli
|
||||||
|
|
||||||
|
|
||||||
|
def cli():
|
||||||
|
magicli(docopt(__doc__))
|
||||||
|
|
||||||
|
|
||||||
|
def main(name, amount = 1):
|
||||||
|
for _ in range(int(amount)):
|
||||||
|
print(f'Hello {name}!')
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that `magicli` will automatically try to call the `main()` function.
|
||||||
|
|
||||||
|
This can be changed to another function through the settings `magicli(docopt(__doc__), entry_point='another_function')`.
|
||||||
|
|
||||||
|
### setup.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='magicli_example',
|
||||||
|
version='0.1.0',
|
||||||
|
install_requires=[
|
||||||
|
'docopt'
|
||||||
|
],
|
||||||
|
entry_points={
|
||||||
|
'console_scripts':[
|
||||||
|
'magicli_example=docs.example:cli'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Make sure that the entry point specified in your setup.py is not the main function, otherwise it will not work.
|
||||||
|
|
||||||
|
Calling the script:
|
||||||
|
|
||||||
|
```
|
||||||
|
magicli_example World -a 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Results in the following output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hello World!
|
||||||
|
Hello World!
|
||||||
|
Hello World!
|
||||||
|
```
|
||||||
31
setup.py
31
setup.py
@ -1,39 +1,24 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
|
|
||||||
with open('README.md') as f:
|
with open('readme.md') as f:
|
||||||
long_description = f.read()
|
long_description = f.read()
|
||||||
description = long_description.split('\n')[2]
|
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='magicli',
|
name='magicli',
|
||||||
version='0.2.0',
|
version='0.1.0',
|
||||||
description=description,
|
description='Automatically call args parsed by `docopt` as functions.',
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
|
||||||
package_dir={'': 'src'},
|
|
||||||
py_modules=[
|
|
||||||
'magicli',
|
|
||||||
],
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pargv'
|
'docopt'
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'dev':[
|
'tests':[
|
||||||
'pytest',
|
'pytest',
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
keywords=[
|
keywords=[],
|
||||||
'python',
|
classifiers=[],
|
||||||
'cli'
|
entry_points={'console_scripts':['magicli_example=docs.example:cli']}
|
||||||
],
|
|
||||||
classifiers=[
|
|
||||||
"Development Status :: 1 - Planning",
|
|
||||||
"Environment :: Console",
|
|
||||||
"Intended Audience :: Developers",
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"Operating System :: Unix",
|
|
||||||
"Operating System :: OS Independent",
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,78 +0,0 @@
|
|||||||
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_command_with_invalid_arguments(capsys):
|
|
||||||
inputs = [
|
|
||||||
'appname command_with_invalid_arguments --name=Name',
|
|
||||||
'appname command_with_invalid_arguments',
|
|
||||||
'command_with_invalid_arguments magicli 2 invalid',
|
|
||||||
]
|
|
||||||
error_message = '\x1b[91mError:\x1b[0m '
|
|
||||||
for i in inputs:
|
|
||||||
args = i.split()
|
|
||||||
with mock.patch('sys.argv', args):
|
|
||||||
with pytest.raises(TypeError):
|
|
||||||
magicli(exclude=['test_command_with_invalid_arguments'])
|
|
||||||
out, err = capsys.readouterr()
|
|
||||||
assert out.startswith(error_message)
|
|
||||||
|
|
||||||
|
|
||||||
def command_with_invalid_arguments(name, amount):
|
|
||||||
pass
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
from unittest import mock
|
|
||||||
import pytest
|
|
||||||
import sys
|
|
||||||
from magicli import magicli
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('inputs, output', (
|
|
||||||
('appname command --name=Name --amount=3',
|
|
||||||
'Hello Name!\nHello Name!\nHello Name!\n'),
|
|
||||||
('appname command Name 3',
|
|
||||||
'Hello Name!\nHello Name!\nHello Name!\n'),
|
|
||||||
('appname command',
|
|
||||||
'Hello World!\n'),
|
|
||||||
('command magicli 2',
|
|
||||||
'Hello magicli!\nHello magicli!\n'),
|
|
||||||
))
|
|
||||||
def test_valid_command_line_arguments(inputs, output, capsys):
|
|
||||||
args = inputs.split()
|
|
||||||
with mock.patch('sys.argv', args):
|
|
||||||
magicli(exclude=['test_valid_command_line_arguments'])
|
|
||||||
out, err = capsys.readouterr()
|
|
||||||
assert out == output
|
|
||||||
|
|
||||||
|
|
||||||
def command(name='World', amount=1):
|
|
||||||
for _ in range(int(amount)):
|
|
||||||
print(f'Hello {name}!')
|
|
||||||
@ -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
|
|
||||||
34
tests/test_magicli.py
Normal file
34
tests/test_magicli.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from magicli import clean_args
|
||||||
|
from magicli import parse_function_name
|
||||||
|
from magicli import args_set_in_cli
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_args():
|
||||||
|
args = {
|
||||||
|
"<name-1>": True,
|
||||||
|
"--name-2": False
|
||||||
|
}
|
||||||
|
assert clean_args(args) == {
|
||||||
|
"name_1": True,
|
||||||
|
"name_2": False
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_function_name():
|
||||||
|
assert parse_function_name('<name-1>') == 'name_1'
|
||||||
|
assert parse_function_name('--name-2') == 'name_2'
|
||||||
|
|
||||||
|
|
||||||
|
def test_args_set_in_cli():
|
||||||
|
args = {
|
||||||
|
"a": True,
|
||||||
|
"b": False,
|
||||||
|
"c": None,
|
||||||
|
"d": 1,
|
||||||
|
"e": "a"
|
||||||
|
}
|
||||||
|
assert args_set_in_cli(args) == {
|
||||||
|
"a": True,
|
||||||
|
"d": 1,
|
||||||
|
"e": "a"
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user