From b27ce82afd52fa4682f1dd79ba196dbc4bbb960f Mon Sep 17 00:00:00 2001 From: Denis Navarro Date: Mon, 18 Apr 2022 21:33:18 +0200 Subject: [PATCH] release: v1.6.1 - Add CommandArgument for Command positional and optional arguments and accept State as callback --- aiocli/__init__.py | 2 +- aiocli/commander.py | 10 +- aiocli/commander_app.py | 107 +++- aiocli/helpers.py | 10 +- docs/404.html | 18 +- docs/advanced/exception_handler/index.html | 18 +- docs/advanced/hooks/index.html | 22 +- docs/advanced/middleware/index.html | 18 +- docs/advanced/serverless_support/index.html | 18 +- docs/advanced/state/index.html | 586 ++++++++++++++++++ docs/advanced/test_support/index.html | 18 +- docs/en/advanced/exception_handler/index.html | 18 +- docs/en/advanced/hooks/index.html | 18 +- docs/en/advanced/middleware/index.html | 18 +- .../en/advanced/serverless_support/index.html | 18 +- docs/en/advanced/state/index.html | 541 ++++++++++++++++ docs/en/advanced/test_support/index.html | 18 +- docs/en/features/index.html | 18 +- docs/en/index.html | 18 +- docs/en/release-notes/index.html | 42 +- docs/es/advanced/exception_handler/index.html | 18 +- docs/es/advanced/hooks/index.html | 18 +- docs/es/advanced/middleware/index.html | 18 +- .../es/advanced/serverless_support/index.html | 18 +- docs/es/advanced/test_support/index.html | 18 +- docs/es/features/index.html | 18 +- docs/es/index.html | 18 +- docs/es/release-notes/index.html | 42 +- docs/features/index.html | 18 +- docs/index.html | 18 +- docs/release-notes/index.html | 53 +- docs/search/search_index.json | 2 +- docs/sitemap.xml | 58 +- docs/sitemap.xml.gz | Bin 349 -> 360 bytes docs_src/docs/en/advanced/state.md | 1 + docs_src/docs/en/release-notes.md | 17 +- docs_src/mkdocs.yml | 5 +- pyproject.toml | 2 +- requirements-dev.txt | 14 +- 39 files changed, 1762 insertions(+), 130 deletions(-) create mode 100644 docs/advanced/state/index.html create mode 100644 docs/en/advanced/state/index.html create mode 100644 docs_src/docs/en/advanced/state.md diff --git a/aiocli/__init__.py b/aiocli/__init__.py index 9cc6694..5ed906d 100644 --- a/aiocli/__init__.py +++ b/aiocli/__init__.py @@ -1,3 +1,3 @@ """Simple and lightweight async console runner.""" -__version__ = '1.6.0' +__version__ = '1.6.1' diff --git a/aiocli/commander.py b/aiocli/commander.py index 383b1f2..e1fbb7d 100644 --- a/aiocli/commander.py +++ b/aiocli/commander.py @@ -5,12 +5,20 @@ from asyncio.events import AbstractEventLoop from typing import Any, Awaitable, Callable, List, Optional, Set, Union, cast -from aiocli.commander_app import Application, Command, Depends, State, command +from aiocli.commander_app import ( + Application, + Command, + CommandArgument, + Depends, + State, + command, +) __all__ = ( # commander_app 'State', 'Depends', + 'CommandArgument', 'Command', 'command', 'Application', diff --git a/aiocli/commander_app.py b/aiocli/commander_app.py index 4955422..7d28fbb 100644 --- a/aiocli/commander_app.py +++ b/aiocli/commander_app.py @@ -1,12 +1,14 @@ -from argparse import ArgumentParser, RawTextHelpFormatter +from argparse import Action, ArgumentParser, RawTextHelpFormatter from inspect import signature from typing import ( Any, Awaitable, Callable, + Container, Coroutine, Dict, List, + NamedTuple, Optional, Sequence, Tuple, @@ -18,13 +20,13 @@ # commander_app 'State', 'Depends', + 'CommandArgument', 'Command', 'command', 'CommandHandler', 'Application', ) - from .helpers import resolve_function from .logger import logger @@ -35,6 +37,7 @@ class State(dict): pass +# dataclass class _Depends: def __init__(self, dependency: Callable[..., Any], cache: bool) -> None: self.dependency = dependency @@ -45,34 +48,88 @@ def Depends(dependency: Callable[..., Any], cache: bool = True) -> Any: return _Depends(dependency=dependency, cache=cache) +# https://docs.python.org/3/library/argparse.html#the-add-argument-method +class CommandArgument(NamedTuple): + name_or_flags: Union[str, List[str]] + action: Optional[Union[str, Action]] = None + nargs: Optional[Union[int, str]] = None + const: Any = None + default: Any = None + type: Union[Type[Any], Callable[[str], Any]] = str + choices: Optional[Container[Any]] = None + required: Optional[bool] = None + help: Optional[str] = None + metavar: Optional[str] = None + dest: Optional[str] = None + + +# dataclass class Command: name: str handler: CommandHandler - positionals: List[Tuple[str, Dict[str, Any]]] - optionals: List[Tuple[str, Dict[str, Any]]] + positionals: List[ + Union[ + Tuple[str, Dict[str, Any]], + CommandArgument, + ] + ] + optionals: List[ + Union[ + Tuple[str, Dict[str, Any]], + CommandArgument, + ] + ] deprecated: Optional[bool] + description: Optional[str] def __init__( self, name: str, handler: CommandHandler, - positionals: List[Tuple[str, Dict[str, Any]]], - optionals: List[Tuple[str, Dict[str, Any]]], + positionals: List[ + Union[ + Tuple[str, Dict[str, Any]], + CommandArgument, + ] + ], + optionals: List[ + Union[ + Tuple[str, Dict[str, Any]], + CommandArgument, + ] + ], deprecated: Optional[bool] = None, + description: Optional[str] = None, ) -> None: self.name = name self.handler = handler # type: ignore self.positionals = positionals self.optionals = optionals self.deprecated = deprecated + self.description = description def command( name: str, handler: CommandHandler, - positionals: Optional[List[Tuple[str, Dict[str, Any]]]] = None, - optionals: Optional[List[Tuple[str, Dict[str, Any]]]] = None, + positionals: Optional[ + List[ + Union[ + Tuple[str, Dict[str, Any]], + CommandArgument, + ] + ] + ] = None, + optionals: Optional[ + List[ + Union[ + Tuple[str, Dict[str, Any]], + CommandArgument, + ] + ] + ] = None, deprecated: Optional[bool] = None, + description: Optional[str] = None, ) -> Command: return Command( name=name, @@ -80,6 +137,7 @@ def command( positionals=positionals or [], optionals=optionals or [], deprecated=deprecated, + description=description, ) @@ -91,6 +149,12 @@ def command( CommandHook = Callable[['Application'], Union[None, Awaitable[None]]] +ArgumentState = Union[ + State, + Dict[str, Any], + Callable[[], Union[State, Dict[str, Any]]], +] + class Application: _parser: ArgumentParser @@ -122,7 +186,7 @@ def __init__( on_shutdown: Optional[Sequence[CommandHook]] = None, on_cleanup: Optional[Sequence[CommandHook]] = None, deprecated: Optional[bool] = None, - state: Optional[Union[State, Dict[str, Any]]] = None, + state: Optional[ArgumentState] = None, ) -> None: self._parser = ArgumentParser( description=description, @@ -160,7 +224,7 @@ async def self_handler() -> int: self._on_cleanup = [] if on_cleanup is None else list(on_cleanup) self._deprecated = bool(deprecated) self._dependencies_cached = {} - self._state = State(state or {}) if not isinstance(state, State) else state + self._set_state(state or State()) async def __call__(self, args: List[str]) -> int: exit_code: Optional[int] = self._exit_code @@ -219,8 +283,8 @@ def decorator(handler: CommandHandler) -> CommandHandler: Command( name=name, handler=handler, - positionals=positionals or [], - optionals=optionals or [], + positionals=positionals or [], # type: ignore + optionals=optionals or [], # type: ignore deprecated=deprecated, ) ) @@ -305,8 +369,13 @@ def _add_command(self, cmd: Command) -> None: if cmd.deprecated is None: cmd.deprecated = self._deprecated self._commands[cmd.name] = cmd - parser = ArgumentParser(prog=cmd.name) - _ = [parser.add_argument(arg[0], **arg[1]) for arg in cmd.positionals + cmd.optionals] + parser = ArgumentParser(description=cmd.description, prog=cmd.name, prefix_chars='-') + args = cmd.positionals + cmd.optionals + for arg in args: + if isinstance(arg, CommandArgument): + arg = (arg.name_or_flags, arg._asdict()) # type: ignore + del arg[1]['name_or_flags'] # type: ignore + parser.add_argument(arg[0], **arg[1]) # type: ignore self._parsers[cmd.name] = parser self._update_parser_description(cmd.name) @@ -432,6 +501,16 @@ async def _execute_command_exception_handler( ) return await resolve_function(exception_handler, err, cmd, kwargs) + def _set_state(self, state: ArgumentState) -> None: + if isinstance(state, State): + self._state = state + elif isinstance(state, dict): + self._state = State(state) + elif callable(state): + self._state = state() # type: ignore + else: + self._state = State() + def _log(self, msg: str) -> None: if self._debug: # print(msg) diff --git a/aiocli/helpers.py b/aiocli/helpers.py index cad34ee..743e40f 100644 --- a/aiocli/helpers.py +++ b/aiocli/helpers.py @@ -1,6 +1,6 @@ import asyncio -import functools -import sys +from functools import partial +from sys import version_info from typing import Any, Callable, Optional, Set @@ -9,17 +9,17 @@ def all_tasks(loop: Optional[asyncio.AbstractEventLoop] = None) -> Set["asyncio. return {t for t in tasks if not t.done()} -if sys.version_info >= (3, 7): +if version_info >= (3, 7): all_tasks = getattr(asyncio, 'all_tasks') def iscoroutinefunction(func: Callable[..., Any]) -> bool: - while isinstance(func, functools.partial): + while isinstance(func, partial): func = func.func return asyncio.iscoroutinefunction(func) -if sys.version_info >= (3, 8): +if version_info >= (3, 8): iscoroutinefunction = asyncio.iscoroutinefunction diff --git a/docs/404.html b/docs/404.html index 0ba7afc..6f40d5a 100644 --- a/docs/404.html +++ b/docs/404.html @@ -126,7 +126,7 @@
  • - es - español + es - Spanish
  • @@ -421,6 +421,20 @@ + + + + + +
  • + + State + +
  • + + + + @@ -468,7 +482,7 @@

    404 - Not found