python function to command translator
Project description
handofcats
https://travis-ci.org/podhmo/handofcats.svg
A tiny magically Converter that making executable command from plain python function. If the function is type annotated, it is used.
- If you want single-command,
as_command()
is helpful ✨ - If you want sub-commands,
as_subcommand()
is helpful ✨ - If you want something like create-react-app's eject, use
--expose
option ◀️
as_command()
greeting.py
from handofcats import as_command
@as_command
def greeting(message: str, is_surprised: bool = False, name: str = "foo") -> None:
"""greeting message"""
suffix = "!" if is_surprised else ""
print("{name}: {message}{suffix}".format(name=name, message=message, suffix=suffix))
🚀 Using as single-command
$ python greeting.py hello
foo: hello
$ python greeting.py --is-surprised hello
foo: hello!
$ python greeting.py --is-surprised --name=bar bye
bar: bye!
help message
$ python greeting.py -h
usage: greeting [-h] [--is-surprised] [--name NAME] [--expose] [--inplace]
[--untyped]
[--logging {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}]
message
greeting message
positional arguments:
message
optional arguments:
-h, --help show this help message and exit
--is-surprised
--name NAME (default: 'foo')
--expose dump generated code. with --inplace, eject from
handofcats dependency
--inplace overwrite file
--untyped untyped expression is dumped (default: False)
--logging {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}
( :warning: TODO: detail description )
as_subcommand()
and as_subcommand.run()
If you want sub-commands, from following code.
cli.py
from handofcats import as_subcommand
@as_subcommand
def hello(*, name: str = "world"):
print(f"hello {name}")
@as_subcommand
def byebye(name):
print(f"byebye {name}")
# :warning: don't forget this
as_subcommand.run()
🚀 Using as sub-commands
$ python cli.py hello
hello world
$ python cli.py hello --name foo
hello foo
$ python cli.py byebye foo
byebye foo
help message
$ python cli.py -h
usage: cli.py [-h] [--expose] [--inplace] [--untyped]
[--logging {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}]
{hello,byebye} ...
optional arguments:
-h, --help show this help message and exit
--expose dump generated code. with --inplace, eject from handofcats dependency (default: False)
--inplace overwrite file
--untyped untyped expression is dumped (default: False)
--logging {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}
subcommands:
{hello,byebye}
hello
byebye
$ python cli.py hello -h
usage: cli.py hello [-h] [--name NAME]
optional arguments:
-h, --help show this help message and exit
--name NAME (default: 'world')
--expose
Runing with --expose
option, generationg the code that dropping
dependencies of handofcats module.
Something like create-react-app'seject .
No Lock-In: You can “eject” to a custom setup at any time. Run a single-command, and all the configuration and build dependencies will be moved directly into your project, so you can pick up right where you left off.
If you want to eject from the code described above, --expose
is helpful, maybe.
$ python greeting.py --expose
import typing as t
def greeting(message: str, is_surprised: bool = False, name: str = "foo") -> None:
"""greeting message"""
suffix = "!" if is_surprised else ""
print("{name}: {message}{suffix}".format(name=name, message=message, suffix=suffix))
def main(argv: t.Optional[t.List[str]] = None) -> t.Any:
import argparse
parser = argparse.ArgumentParser(prog=greeting.__name__, description=greeting.__doc__, formatter_class=type('_HelpFormatter', (argparse.ArgumentDefaultsHelpFormatter, argparse.RawTextHelpFormatter), {}))
parser.print_usage = parser.print_help # type: ignore
parser.add_argument('message', help='-')
parser.add_argument('--is-surprised', action='store_true', help='-')
parser.add_argument('--name', required=False, default='foo', help='-')
args = parser.parse_args(argv)
params = vars(args).copy()
return greeting(**params)
if __name__ == '__main__':
main()
--expose
with --inplace
In addition, running with inplace
option, when --expose
, overwrite
target source code.
For handofcats, eject action is --inplace --exepose
.
handofcats
command
sum.py
def sum(x: int, y: int) -> None:
print(f"{x} + {y} = {x + y}")
It is also ok, calling the function that not decorated via handofcats command.
$ handofcats sum.py:sum 10 20
10 + 20 = 30
$ handofcats sum.py:sum -h
handofcats sum.py:sum -h
usage: sum [-h] [--expose] [--inplace] [--untyped]
[--logging {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}]
x y
positional arguments:
x
y
optional arguments:
-h, --help show this help message and exit
--expose dump generated code. with --inplace, eject from handofcats dependency (default: False)
--inplace overwrite file
--untyped untyped expression is dumped (default: False)
--logging {CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}
--expose
with handofcats command
Passed in the form <filename>.py
, it will be interpreted as a
sub-commands. Of course, the --expose
option also works.
And passed in the form <filename>.py:<function name>
, it will be
interpreted as a single-command.
So, plain python function only needed.
cli.py
def hello(*, name: str = "world"):
print(f"hello {name}")
# FIXME: default arguments (positional arguments)
def byebye(name: str):
print(f"byebye {name}")
# ignored
def _ignore(name: str):
print("ignored")
# treated as sub-commands
$ handofcats cli.py --expose
import typing as t
def hello(*, name: str = "world"):
print(f"hello {name}")
# FIXME: default arguments (positional arguments)
def byebye(name: str):
print(f"byebye {name}")
# ignored
def ignore(name: str):
print(f"ignored {name}")
def _ignore(name: str):
print("of cource, ignored")
def main(argv: t.Optional[t.List[str]] = None) -> t.Any:
import argparse
parser = argparse.ArgumentParser(formatter_class=type('_HelpFormatter', (argparse.ArgumentDefaultsHelpFormatter, argparse.RawTextHelpFormatter), {}))
subparsers = parser.add_subparsers(title='subcommands', dest='subcommand')
subparsers.required = True
fn = hello
sub_parser = subparsers.add_parser(fn.__name__, help=fn.__doc__, formatter_class=parser.formatter_class)
sub_parser.add_argument('--name', required=False, default='world', help='-')
sub_parser.set_defaults(subcommand=fn)
fn = byebye # type: ignore
sub_parser = subparsers.add_parser(fn.__name__, help=fn.__doc__, formatter_class=parser.formatter_class)
sub_parser.add_argument('name', help='-')
sub_parser.set_defaults(subcommand=fn)
args = parser.parse_args(argv)
params = vars(args).copy()
subcommand = params.pop('subcommand')
return subcommand(**params)
if __name__ == '__main__':
main()
# treated as single-command
$ handofcats cli.py:hello --expose
...
experimental
sequences
from typing import List, Optional
def psum(xs: List[int], *, ys: Optional[List[int]] = None):
# treated as
# parser.add_argument('xs', nargs='*', type=int)
# parser.add_argument('--ys', action='append', required=False, type=int)
..
choices
from typing_extensions import Literal
DumpFormat = Literal["json", "csv"] # this: (experimental)
def run(*, format: DumpFormat = "json"):
# treated as
# parser.add_argument("--format", defaul="json", choices=("json", "csv"), required=False)
...
3.0.1
- (README uploading is failed)
3.0.0
- add sub-commands support
- some refactoring
- on
--expose
, default format is typed code, changes to more mypy friendly output
2.5.0
- logging option for lazy person
- run with DEBUG=1 envvar, then logging feature is actuvated, automatically
2.4.3
- catch up magicalimport 0.8.1
2.4.2
- Injector with callback option
2.4.1
- Injector support ignore_arguments and ignore_flags
2.4.0
- some refactoring
- use magicalimport for reducing code
- use fastentrypoint for fast bootstrap time editable installed
2.3.2
- fix, with Literal Types, detect type is failed
2.3.1
- fix, choices's type is list, not dict
2.3.0
- support Literal types
- fix, error is occured, running with
from __future__ import annotations
- fix, generated code is invalid when positional arguments with "_"
- fix, unable to use the function named "main"
--typed
option, with--expose
2.2.0
--inplace
option, with--expose
2.1.0
choices
function- fix bug that it is not working, importing with physical filepath
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file handofcats-3.0.1.tar.gz
.
File metadata
- Download URL: handofcats-3.0.1.tar.gz
- Upload date:
- Size: 18.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/45.0.0 requests-toolbelt/0.9.1 tqdm/4.41.1 CPython/3.8.1
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | f8ea684e69d27b20d1fe673b6461d03c59bea595ead0ce1e7479021f7556f851 |
|
MD5 | cf87140a5aa393add778614499fff464 |
|
BLAKE2b-256 | b295497f6cdf8258f2a1662d0d778264cad95a1f7655e11bbc7ccc26cd3e54b7 |
File details
Details for the file handofcats-3.0.1-py3-none-any.whl
.
File metadata
- Download URL: handofcats-3.0.1-py3-none-any.whl
- Upload date:
- Size: 19.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/45.0.0 requests-toolbelt/0.9.1 tqdm/4.41.1 CPython/3.8.1
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | d2ece1ee09d1c2c8a7839b5c135fc6ae73e9a2e79717429c7f93bff38c8846f1 |
|
MD5 | ba90faebce4b4780d6f579d11c97df92 |
|
BLAKE2b-256 | 29d202b5c8823b4e66014ec40dbaf7ce94d6adf0befd8ecb338e2f762a6dbe8c |