Skip to content

Commit

Permalink
build parser with sub-commands using multiple functions
Browse files Browse the repository at this point in the history
  • Loading branch information
vanyakosmos committed Oct 28, 2019
1 parent d562842 commit 4a615ef
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 31 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ Versions follow [Semantic Versioning](https://semver.org) (`<major>.<minor>.<pat
**But**. Currently project is in alpha stage (`0.0.*`) and any changes can potentially break existing code.


## 0.0.12

### Features

- build parser based on function arguments
- build parser with sub-commands using multiple functions


## 0.0.11

### Breaking changes
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ optional arguments:
```


## Get arguments from function

```python
import argser

def foo(a, b: int, c=1.2):
return [a, b, c]

assert argser.call(foo, '1 2 -c 3.4') == ['1', 2, 3.4]
```


## Sub-commands

```python
Expand Down Expand Up @@ -157,3 +169,22 @@ class Args:

args = parse_args(Args, '-a 1 sub1 -b 2 sub2 -c 3 sub3 -d 4')
```


### Sub-commands from functions

```python
import argser
subs = argser.SubCommands()

@subs.add
def foo():
return 'foo'

@subs.add(description="foo bar") # with additional arguments for sub-parser
def bar(a, b=1):
return [a, b]

assert subs.parse('foo') == 'foo'
assert subs.parse('bar 1 -b 2') == ['1', 2]
```
3 changes: 2 additions & 1 deletion argser/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from argser.consts import FALSE_VALUES, TRUE_VALUES
from argser.display import make_table, stringify
from argser.fields import Arg, Opt
from argser.parse_func import call
from argser.parse_func import call, SubCommands
from argser.parser import parse_args, sub_command

parse = parse_args
Argument = Arg
Option = Opt
Subs = SubCommands
63 changes: 58 additions & 5 deletions argser/parse_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from types import FunctionType
from argser.fields import Arg, Opt
from argser.parser import parse_args, _get_type_and_nargs
from argser.parser import parse_args, _get_type_and_nargs, sub_command


def _get_default_args(func):
Expand All @@ -21,15 +21,18 @@ def _make_argument(name, annotations: dict, defaults: dict):
return arg


def _call(func: FunctionType, *parser_args, **parser_kwargs):
def _make_args_cls(func: FunctionType):
ann = func.__annotations__
args = func.__code__.co_varnames
args = args[:func.__code__.co_argcount] # arguments excluding *args, **kwargs and kw only args
defaults = _get_default_args(func)
Args = type('Args', (), {arg: _make_argument(arg, ann, defaults) for arg in args})
return Args

parser_kwargs.setdefault('parser_prog', func.__name__)

Args = type('Args', (), {arg: _make_argument(arg, ann, defaults) for arg in args})
def _call(func: FunctionType, *parser_args, **parser_kwargs):
Args = _make_args_cls(func)
parser_kwargs.setdefault('parser_prog', func.__name__)
args = parse_args(Args, *parser_args, **parser_kwargs)
return func(**args.__dict__)

Expand Down Expand Up @@ -62,4 +65,54 @@ def call(func=None, *args, **kwargs):
if isinstance(func, FunctionType):
return _call(func, *args, **kwargs)
args = (func,) + args
return lambda f: _call(f, *args, **kwargs)

def dec(f):
return _call(f, *args, **kwargs)

return dec


class SubCommands:
"""
Allows to create sub-commands from multiple functions.
>>> subs = SubCommands()
>>> @subs.add(description="foo bar")
... def foo():
... return 'foo'
>>> @subs.add
... def bar(a, b: int):
... return [a, b]
>>> subs.parse('foo')
'foo'
>>> subs.parse('bar 1 2')
['1', 2]
"""
def __init__(self):
self.commands = {}
self.functions = {}

def _add(self, func: FunctionType, **kwargs):
self.commands[func.__name__] = sub_command(_make_args_cls(func), **kwargs)
self.functions[func.__name__] = func

def add(self, func=None, **kwargs):
"""Use as ``@subs.add`` or ``@subs.add(...params...)``"""
if isinstance(func, FunctionType):
return self._add(func, **kwargs)

def dec(f):
return self._add(f, **kwargs)

return dec

def parse(self, *parser_args, **parser_kwargs):
Args = type('Args', (), {name: sub_cmd for name, sub_cmd in self.commands.items()})
args = parse_args(Args, *parser_args, **parser_kwargs)
for name in self.commands:
sub_args = getattr(args, name, None)
if sub_args is not None:
return self.functions[name](**sub_args.__dict__)
19 changes: 19 additions & 0 deletions docs/source/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,25 @@ Or as decorator:
In examples above ``a`` (implicit string) and ``b`` (int) are positional argument because they don't have default values.


Multiple sub-commands:

.. doctest::

>>> from argser import SubCommands
>>> subs = SubCommands()

>>> @subs.add(description="foo bar")
... def foo(): return 'foo'

>>> @subs.add
... def bar(a, b: int): return [a, b]

>>> subs.parse('foo')
'foo'
>>> subs.parse('bar 1 2')
['1', 2]


Auto completion
***************

Expand Down
44 changes: 44 additions & 0 deletions tests/test_parse_func.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import List

import argser
from argser.parse_func import SubCommands


def test_simple_case():
def foo(a, b: int, c=1.2):
return [a, b, c]

assert argser.call(foo, '1 2') == ['1', 2, 1.2]
assert argser.call(foo, '2 3 -c 4.4') == ['2', 3, 4.4]

def foo(a, b: List[int]):
return [a, b]

assert argser.call(foo, '1 2 3') == ['1', [2, 3]]


def test_decorator():
@argser.call('')
def foo(a=1, b='2'):
assert a == 1
assert b == '2'

@argser.call('-a 5 -b "foo bar"')
def foo(a=1, b='2'):
assert a == 5
assert b == 'foo bar'


def test_group():
subs = SubCommands()

@subs.add(description="foo bar")
def foo():
return 'foo'

@subs.add
def bar(a, b: int):
return [a, b]

assert subs.parse('foo') == 'foo'
assert subs.parse('bar 1 2') == ['1', 2]
25 changes: 0 additions & 25 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,28 +463,3 @@ class Args2(CommonArgs):
assert args.b == 5.5
assert args.c == 'c'
assert args.e == 'foo bar'


def test_parse_function():
def foo(a, b: int, c=1.2):
return [a, b, c]

assert argser.call(foo, '1 2') == ['1', 2, 1.2]
assert argser.call(foo, '2 3 -c 4.4') == ['2', 3, 4.4]

def foo(a, b: List[int]):
return [a, b]

assert argser.call(foo, '1 2 3') == ['1', [2, 3]]


def test_parse_function_decorator():
@argser.call('')
def foo(a=1, b='2'):
assert a == 1
assert b == '2'

@argser.call('-a 5 -b "foo bar"')
def foo(a=1, b='2'):
assert a == 5
assert b == 'foo bar'

0 comments on commit 4a615ef

Please sign in to comment.