Skip to main content

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.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

handofcats-3.0.1.tar.gz (18.7 kB view details)

Uploaded Source

Built Distribution

handofcats-3.0.1-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

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

Hashes for handofcats-3.0.1.tar.gz
Algorithm Hash digest
SHA256 f8ea684e69d27b20d1fe673b6461d03c59bea595ead0ce1e7479021f7556f851
MD5 cf87140a5aa393add778614499fff464
BLAKE2b-256 b295497f6cdf8258f2a1662d0d778264cad95a1f7655e11bbc7ccc26cd3e54b7

See more details on using hashes here.

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

Hashes for handofcats-3.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d2ece1ee09d1c2c8a7839b5c135fc6ae73e9a2e79717429c7f93bff38c8846f1
MD5 ba90faebce4b4780d6f579d11c97df92
BLAKE2b-256 29d202b5c8823b4e66014ec40dbaf7ce94d6adf0befd8ecb338e2f762a6dbe8c

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page