Skip to content

Commit

Permalink
modernize a bunch of old-style kw-only arguments and add typing along…
Browse files Browse the repository at this point in the history
… the way
  • Loading branch information
mahmoud committed Nov 4, 2024
1 parent 0a1e113 commit a2769db
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 117 deletions.
94 changes: 48 additions & 46 deletions face/command.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import sys
from collections import OrderedDict
from typing import Callable, List, Optional, Union

from face.utils import unwrap_text, get_rdep_map, echo
from face.errors import ArgumentParseError, CommandLineError, UsageError
from face.parser import Parser, Flag
from face.parser import Parser, Flag, PosArgSpec
from face.helpers import HelpHandler
from face.middleware import (inject,
get_arg_names,
Expand Down Expand Up @@ -57,73 +58,74 @@ class Command(Parser):
populate it with flags and subcommands, and then call
command.run() to execute your CLI.
Note that only the first three constructor arguments are
positional, the rest are keyword-only.
Args:
func (callable): The function called when this command is
run with an argv that contains no subcommands.
name (str): The name of this command, used when this
command is included as a subcommand. (Defaults to name
of function)
doc (str): A description or message that appears in various
help outputs.
flags (list): A list of Flag instances to initialize the
Command with. Flags can always be added later with the
.add() method.
posargs (bool): Pass True if the command takes positional
arguments. Defaults to False. Can also pass a PosArgSpec
instance.
post_posargs (bool): Pass True if the command takes
additional positional arguments after a conventional '--'
specifier.
help (bool): Pass False to disable the automatically added
--help flag. Defaults to True. Also accepts a HelpHandler
instance, see those docs for more details.
middlewares (list): A list of @face_middleware decorated
callables which participate in dispatch. Also addable
via the .add() method. See Middleware docs for more
details.
func: The function called when this command is
run with an argv that contains no subcommands.
name: The name of this command, used when this
command is included as a subcommand. (Defaults to name
of function)
doc: A description or message that appears in various
help outputs.
flags: A list of Flag instances to initialize the
Command with. Flags can always be added later with the
.add() method.
posargs: Pass True if the command takes positional
arguments. Defaults to False. Can also pass a PosArgSpec
instance.
post_posargs: Pass True if the command takes
additional positional arguments after a conventional '--'
specifier.
help: Pass False to disable the automatically added
--help flag. Defaults to True. Also accepts a HelpHandler
instance.
middlewares: A list of @face_middleware decorated
callables which participate in dispatch.
"""
def __init__(self, func, name=None, doc=None, **kwargs):
def __init__(self,
func: Optional[Callable],
name: Optional[str] = None,
doc: Optional[str] = None,
*,
flags: Optional[List[Flag]] = None,
posargs: Optional[Union[bool, PosArgSpec]] = None,
post_posargs: Optional[bool] = None,
flagfile: bool = True,
help: Union[bool, HelpHandler] = DEFAULT_HELP_HANDLER,
middlewares: Optional[List[Callable]] = None) -> None:
name = name if name is not None else _get_default_name(func)

if doc is None:
doc = _docstring_to_doc(func)

# TODO: default posargs if none by inspecting func
super().__init__(name, doc,
flags=kwargs.pop('flags', None),
posargs=kwargs.pop('posargs', None),
post_posargs=kwargs.pop('post_posargs', None),
flagfile=kwargs.pop('flagfile', True))
flags=flags,
posargs=posargs,
post_posargs=post_posargs,
flagfile=flagfile)

_help = kwargs.pop('help', DEFAULT_HELP_HANDLER)
self.help_handler = _help
self.help_handler = help

# TODO: if func is callable, check that "next_" isn't taken
self._path_func_map = OrderedDict()
self._path_func_map[()] = func

middlewares = list(kwargs.pop('middlewares', None) or [])
middlewares = list(middlewares or [])
self._path_mw_map = OrderedDict()
self._path_mw_map[()] = []
self._path_wrapped_map = OrderedDict()
self._path_wrapped_map[()] = func
for mw in middlewares:
self.add_middleware(mw)

if kwargs:
raise TypeError(f'unexpected keyword arguments: {sorted(kwargs.keys())!r}')

if _help:
if _help.flag:
self.add(_help.flag)
if _help.subcmd:
self.add(_help.func, _help.subcmd) # for 'help' as a subcmd
if help:
if help is True:
help = DEFAULT_HELP_HANDLER
if help.flag:
self.add(help.flag)
if help.subcmd:
self.add(help.func, help.subcmd) # for 'help' as a subcmd

if not func and not _help:
if not func and not help:
raise ValueError('Command requires a handler function or help handler'
' to be set, not: %r' % func)

Expand Down
28 changes: 16 additions & 12 deletions face/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ def timing_middleware(next_, echo_time):
from face.parser import Flag
from face.sinter import make_chain, get_arg_names, get_fb, get_callable_labels
from face.sinter import inject # transitive import for external use
from typing import Callable, List, Optional, Union

INNER_NAME = 'next_'

Expand All @@ -171,35 +172,38 @@ def is_middleware(target):
return False


def face_middleware(func=None, **kwargs):
def face_middleware(func: Optional[Callable] = None,
*,
provides: Union[List[str], str] = [],
flags: List[Flag] = [],
optional: bool = False) -> Callable:
"""A decorator to mark a function as face middleware, which wraps
execution of a subcommand handler function. This decorator can be
called with or without arguments:
Args:
provides (list): An optional list of names, declaring which
values be provided by this middleware at execution time.
flags (list): An optional list of Flag instances, which will be
automatically added to any Command which adds this middleware.
optional (bool): Whether this middleware should be skipped if its
provides are not required by the command.
provides: An optional list of names, declaring which
values be provided by this middleware at execution time.
flags: An optional list of Flag instances, which will be
automatically added to any Command which adds this middleware.
optional: Whether this middleware should be skipped if its
provides are not required by the command.
The first argument of the decorated function must be named
"next_". This argument is a function, representing the next
function in the execution chain, the last of which is the
command's handler function.
Returns:
A decorator function that marks the decorated function as middleware.
"""
provides = kwargs.pop('provides', [])
if isinstance(provides, str):
provides = [provides]
flags = list(kwargs.pop('flags', []))
flags = list(flags)
if flags:
for flag in flags:
if not isinstance(flag, Flag):
raise TypeError(f'expected Flag object, not: {flag!r}')
optional = kwargs.pop('optional', False)
if kwargs:
raise TypeError(f'unexpected keyword arguments: {kwargs.keys()!r}')

def decorate_face_middleware(func):
check_middleware(func, provides=provides)
Expand Down
58 changes: 31 additions & 27 deletions face/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import codecs
import os.path
from collections import OrderedDict
from typing import Optional

from boltons.iterutils import split, unique
from boltons.dictutils import OrderedMultiDict as OMD
Expand Down Expand Up @@ -287,7 +288,14 @@ class FlagDisplay:
"""
# value_name -> arg_name?
def __init__(self, flag, **kw):
def __init__(self, flag, *,
label: Optional[str] = None,
post_doc: Optional[str] = None,
full_doc: Optional[str] = None,
value_name: Optional[str] = None,
group: int = 0,
hidden: bool = False,
sort_key: int = 0):
self.flag = flag

self.doc = flag.doc
Expand All @@ -297,24 +305,21 @@ def __init__(self, flag, **kw):
if _prep == 'as':
self.doc = desc

self.post_doc = kw.pop('post_doc', None)
self.full_doc = kw.pop('full_doc', None)
self.post_doc = post_doc
self.full_doc = full_doc

self.value_name = ''
if callable(flag.parse_as):
# TODO: use default when it's set and it's a basic renderable type
self.value_name = kw.pop('value_name', None) or self.flag.name.upper()
self.value_name = value_name or self.flag.name.upper()

self.group = kw.pop('group', 0) # int or str
self._hide = kw.pop('hidden', False) # bool
self.label = kw.pop('label', None) # see hidden property below for more info
self.sort_key = kw.pop('sort_key', 0) # int or str
self.group = group
self._hide = hidden
self.label = label # see hidden property below for more info
self.sort_key = sort_key
# TODO: sort_key is gonna need to be partitioned on type for py3
# TODO: maybe sort_key should be a counter so that flags sort
# in the order they are created

if kw:
raise TypeError(f'unexpected keyword arguments: {kw.keys()!r}')
return

@property
Expand Down Expand Up @@ -342,16 +347,17 @@ class PosArgDisplay:
often describes default behavior.
"""
def __init__(self, **kw):
self.name = kw.pop('name', None) or 'arg'
self.doc = kw.pop('doc', '')
self.post_doc = kw.pop('post_doc', None)
self._hide = kw.pop('hidden', False) # bool
self.label = kw.pop('label', None)

if kw:
raise TypeError(f'unexpected keyword arguments: {kw.keys()!r}')
return
def __init__(self, *,
name: Optional[str] = None,
doc: str = '',
post_doc: Optional[str] = None,
hidden: bool = False,
label: Optional[str] = None) -> None:
self.name = name or 'arg'
self.doc = doc
self.post_doc = post_doc
self._hide = hidden
self.label = label

@property
def hidden(self):
Expand Down Expand Up @@ -386,13 +392,11 @@ class PosArgSpec:
times around the application.
"""
def __init__(self, parse_as=str, min_count=None, max_count=None, display=None, provides=None, **kwargs):
def __init__(self, parse_as=str, min_count=None, max_count=None, display=None, provides=None,
*, name: Optional[str] = None, count: Optional[int] = None):
if not callable(parse_as) and parse_as is not ERROR:
raise TypeError(f'expected callable or ERROR for parse_as, not {parse_as!r}')
name = kwargs.pop('name', None)
count = kwargs.pop('count', None)
if kwargs:
raise TypeError(f'unexpected keyword arguments: {list(kwargs.keys())!r}')

self.parse_as = parse_as

# count convenience alias
Expand Down Expand Up @@ -717,7 +721,7 @@ def parse(self, argv):
ape.prs_res = cpr
raise ape
for arg in argv:
if not isinstance(arg, (str, str)):
if not isinstance(arg, str):
raise TypeError(f'parse expected all args as strings, not: {arg!r} ({type(arg).__name__})')
'''
for subprs_path, subprs in self.subprs_map.items():
Expand Down
10 changes: 0 additions & 10 deletions face/test/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ def test_flag_name():
with pytest.raises(TypeError, match='or FlagDisplay instance'):
Flag('name', display=object())

with pytest.raises(TypeError, match='unexpected keyword arguments'):
Flag('name', display={'badkw': 'val'})

with pytest.raises(ValueError, match='expected identifier.*'):
assert identifier_to_flag('--flag')

Expand Down Expand Up @@ -85,11 +82,6 @@ def test_flag_hidden():
assert 'dragon' not in [f.name for f in flags]


def test_command_misc_api():
with pytest.raises(TypeError, match='unexpected keyword'):
Command(lambda: None, name='ok', bad_kwarg=True)


def test_flag_init():
cmd = Command(lambda flag, part: None, name='cmd')

Expand Down Expand Up @@ -143,8 +135,6 @@ def test_minimal_exe():
def test_posargspec_init():
with pytest.raises(TypeError, match='expected callable or ERROR'):
PosArgSpec(parse_as=object())
with pytest.raises(TypeError, match='unexpected keyword'):
PosArgSpec(badkw='val')

with pytest.raises(ValueError, match='expected min_count >= 0'):
PosArgSpec(min_count=-1)
Expand Down
2 changes: 1 addition & 1 deletion face/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ def run(self, args, input=None, env=None, chdir=None, exit_code=0):
exc_info = None
exit_code = 0

if isinstance(args, (str, str)):
if isinstance(args, str):
args = shlex.split(args)

try:
Expand Down
Loading

0 comments on commit a2769db

Please sign in to comment.