Source code for action_completer.types

# -*- encoding: utf-8 -*-
# Copyright (c) 2020 Stephen Bunn <stephen@bunn.io>
# ISC License <https://choosealicense.com/licenses/isc>

"""Contains the base data types used to power the completion and validation of actions.

Attributes:
    ActionCompletable_T (typing.Type): Defines the base data types that are typically
        used for completion. This type feels like it should be used more, but it really
        isn't necessary that often.

    ActionContext_T (typing.Type): Defines the value types to expect from the
        extraction of the context for the provided prompt document. Since we need
        various details about the current state of the prompt buffer, we explicitly
        define the types of the extracted context tuple through this type.

    ActionParamBasic_T (typing.Type): Defines the allowable types for basic completion
        and validation. Typically just a single value, in this case a string.

    ActionParamIterable_T (typing.Type): Defines the allowable types for iterable
        completion and validation. Only supports hashable iterables (tuples, lists) of
        strings.

    ActionParamCompleter_T (typing.Type): Defines the allowable types for nested
        completer completion (not validation). Currently just an alias of
        :class:`~prompt_toolkit.completion.Completer`.

    ActionParamCallable_T (typing.Type): Defines the allowable type for callable
        completions and validation.

    ActionParamSource_T (typing.Type): Defines the allowable types for action parameter
        sources that can be completed and validated. This is a union of previously
        defined action param types.

    ActionParamValidator_T (typing.Type): Defines the allowable types for validation
        callables or instances.

    LazyString_T (typing.Type): Defines the allowable types for optionally lazy
        evaluated strings. This is either just an instance of a string or a callable
        that results in a string.

    LazyText_T (typing.Type): Similar to :data:`~LazyString_T`, this defines the
        allowable types for optionally lazy evaluated strings or instances of
        :class:`~prompt_toolkit.formatted_text.FormattedText`. This is either just an
        instance or a callable that results in an instance.
"""

import re
import warnings
from typing import (
    Any,
    Callable,
    Dict,
    FrozenSet,
    Iterable,
    List,
    Optional,
    Set,
    Tuple,
    Type,
    Union,
)

import attr
from prompt_toolkit.completion import Completer
from prompt_toolkit.filters import Filter
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.validation import Validator

_decorator_staging: Dict[object, Optional[List["ActionParam"]]] = {}

ActionCompletable_T = Union["ActionGroup", "Action", "ActionParam"]
ActionContext_T = Tuple[
    Optional["ActionGroup"], Optional[str], Union["ActionGroup", "Action"], List[str]
]
ActionParamBasic_T = str
ActionParamCallable_T = Callable[["Action", "ActionParam", str], Iterable[str]]
ActionParamCompleter_T = Type[Completer]
ActionParamIterable_T = Union[List[str], Tuple[str]]
ActionParamSource_T = Union[
    ActionParamBasic_T,
    ActionParamIterable_T,
    ActionParamCompleter_T,
    ActionParamCallable_T,
]
ActionParamValidator_T = Union[
    Validator, Callable[["ActionParam", str, List[str]], Any]
]
LazyString_T = Union[str, Callable[[ActionCompletable_T, str], str]]
LazyText_T = Union[
    str, FormattedText, Callable[[ActionCompletable_T, str], Union[str, FormattedText]]
]


[docs]@attr.s class ActionParam: """Defines a completable action parameter. Attributes: source (:data:`~ActionParamSource_T`): The completion source for the parameter cast (Optional[~typing.Type], optional): The type to cast the action parameter to during execution of the action tied to this parameter style (Optional[:data:`~LazyString_T`], optional): The style string to apply to completion results for the action parameter selected_style (Optional[:data:`~LazyString_T`], optional): The style string to apply to selected completion results for the action parameter display (Optional[:data:`~LazyText_T`], optional): The custom display to apply to completion results for the action parameter display_meta (Optional[:data:`~LazyText_T`], optional): The custom display meta (description) to apply to completion results for the action parameter validators (Optional[List[:data:`~ActionParamValidator_T`]], optional): The list of validators to run in-order against the parameter value during validation """ source: ActionParamSource_T = attr.ib() cast: Optional[Type] = attr.ib(default=None) style: Optional[LazyString_T] = attr.ib(default=None) selected_style: Optional[LazyString_T] = attr.ib(default=None) display: Optional[LazyText_T] = attr.ib(default=None) display_meta: Optional[LazyText_T] = attr.ib(default=None) validators: Optional[List[ActionParamValidator_T]] = attr.ib(default=None)
[docs]@attr.s class Action: """Defines a completable action. Attributes: action (Optional[Callable[..., Any]], optional): The callable function that should be completeable and can be called after validation of the given action and associated parameters params (Optional[List[ActionParam]], optional): The list of action parameters in the same order of positional arguments for the ``action`` callable. style (Optional[:data:`~LazyString_T`], optional): The style string to apply to completion results for the action selected_style (Optional[:data:`~LazyString_T`], optional): The style string to apply to selected completion results for the action display (Optional[:data:`~LazyText_T`], optional): The custom display to apply to completion results for the action display_meta (Optional[:data:`~LazyText_T`], optional): The custom display meta (description) to apply to completion results for the action active (Optional[~prompt_toolkit.filters.Filter], optional): A callable filter that results in a boolean to indicate if the action should be considred as active and displayed as a completion result capture_all (bool, optional): If there is the option for this action to accept more arguments than defined by the provided parameters, this flag will allow for any number of following arguments (after parameters) as additional positional arguments to the provided ``action`` callable, defaults to False """ action: Optional[Callable[..., Any]] = attr.ib(default=None) params: Optional[List[ActionParam]] = attr.ib(default=None) style: Optional[LazyString_T] = attr.ib(default=None) selected_style: Optional[LazyString_T] = attr.ib(default=None) display: Optional[LazyText_T] = attr.ib(default=None) display_meta: Optional[LazyText_T] = attr.ib(default=None) active: Optional[Filter] = attr.ib(default=None) capture_all: bool = attr.ib(default=False)
[docs]@attr.s class ActionGroup: """Defines a completable group of either nested action groups or leaf actions. .. important:: It is crucial for proper fragment extraction that no children keys contain spaces. Each non-escaped space is considered a new fragment and used to find the context for both completion and validation. Although it would technically be possible to support child key completion with spaces, it involves more complex features of prompt-toolkit than *just* completion. Therefore, I have decided to hold the opinion that no spaces should be allowable as keys in a group's children. A post-initialization validator exists to ensure this is true for any action group you define. Attributes: children (Dict[str, Union[ActionGroup, Action]]): The dictionary of completion text (keys) to a nested action group or a callable action (value) that forms the group's children style (Optional[:data:`~LazyString_T`], optional): The style string to apply to completion results for the action group selected_style (Optional[:data:`~LazyString_T`], optional): The style string to apply to selected completion results for the action group display (Optional[:data:`~LazyText_T`], optional): The custom display to apply to completion results for the action group display_meta (Optional[:data:`~LazyText_T`], optional): The custom display meta (description) to apply to completion results for the action group active (Optional[~prompt_toolkit.filters.Filter], optional): A callable filter that results in a boolean to indicate if the action group should be considred as active and displayed as a completion result """ children: Dict[str, Union["ActionGroup", Action]] = attr.ib() style: Optional[LazyString_T] = attr.ib(default=None) selected_style: Optional[LazyString_T] = attr.ib(default=None) display: Optional[LazyText_T] = attr.ib(default=None) display_meta: Optional[LazyText_T] = attr.ib(default=None) active: Optional[Filter] = attr.ib(default=None) @children.validator def _children_validator(self, attribute: attr.Attribute, value: dict): """Validate the children attribute for the group on instance creation. Args: attribute (attr.Attribute): The attribute to validate value (dict): The value of the children attribute Raises: ValueError: When any children keys contain no characters or contain spaces """ invalid_names = [ name for name in value if len(name) <= 0 or re.findall(r"\s+", name) ] if invalid_names: raise ValueError( "group children can not use names without characters or names " f"including spaces, {invalid_names!r}" )
[docs] def group( self, name: str, children: Optional[Dict[str, Union["ActionGroup", Action]]] = None, style: Optional[LazyString_T] = None, selected_style: Optional[LazyString_T] = None, display: Optional[LazyText_T] = None, display_meta: Optional[LazyText_T] = None, active: Optional[Filter] = None, ) -> "ActionGroup": """Create a new subgroup for the current group. By default the :class:`~action_completer.completer.ActionCompleter` comes with a ``root`` group to extend from. However, if you want to build a set of nested commands, you can use this function to register a new group on the completer. .. code-block:: python from prompt_toolkit.shortcuts import prompt from action_completer import ActionCompleter completer = ActionCompleter() nested_group = completer.group("hello") @nested_group.action("world") def _hello_world_action(): print("Hello, World!") completer.run_action(prompt(">>> ", completer=completer)) # available through the following prompt: # >>> hello world # Hello, World! Args: name (str): The completion text that triggers this group children (Dict[str, Union[ActionGroup, Action]]): The dictionary of completion text (keys) to a nested action group or a callable action (value) that forms the group's children style (Optional[:data:`~LazyString_T`], optional): The style string to apply to completion results for the action group selected_style (Optional[:data:`~LazyString_T`], optional): The style string to apply to selected completion results for the action group display (Optional[:data:`~LazyText_T`], optional): The custom display to apply to completion results for the action group display_meta (Optional[:data:`~LazyText_T`], optional): The custom display meta (description) to apply to completion results for the action group active (Optional[~prompt_toolkit.filters.Filter], optional): A callable filter that results in a boolean to indicate if the action group should be considred as active and displayed as a completion result Raises: ValueError: When the provided group name contains spaces ValueError: When the provided group name is already in the current group Returns: ActionGroup: The created action group """ if len(name) <= 0: raise ValueError(f"group names must contain characters, {name!r}") if re.findall(r"\s+", name): raise ValueError(f"groups can not use names including whitespace, {name!r}") if name in self.children: raise ValueError( f"name {name!r} already registered as {self.children[name]!r}" ) group = ActionGroup( children=children or {}, style=style, selected_style=selected_style, display=display, display_meta=display_meta, active=active, ) self.children[name] = group return group
[docs] def action( self, name: str, params: Optional[List[ActionParam]] = None, style: Optional[LazyString_T] = None, selected_style: Optional[LazyString_T] = None, display: Optional[LazyText_T] = None, display_meta: Optional[LazyText_T] = None, active: Optional[Filter] = None, capture_all: bool = False, ) -> Callable[..., Any]: """Decorate a callable as an action within the current group. Basic root level actions are easily defined directly off of the completer instance like follows: .. code-block:: python from prompt_toolkit.shortcuts import prompt from action_completer import ActionCompleter completer = ActionCompleter() @completer.action("hello") def _hello_action(): print("Hello, World!") completer.run_action(prompt(">>> ", completer=completer)) # available through the following prompt: # >>> hello # Hello, World! You can nest actions in sub-groups by first calling :meth:`~ActionGroup.group` to define a new group and base all actions from the ``action`` decorator provided on that new group. Args: name (str): The completion name that triggers this action params (Optional[List[ActionParam]], optional): The list of action parameters to complete and handle within the action style (Optional[:data:`~LazyString_T`], optional): The style string to apply to completion results for the action selected_style (Optional[:data:`~LazyString_T`], optional): The style string to apply to selected completion results for the action display (Optional[:data:`~LazyText_T`], optional): The custom display to apply to completion results for the action display_meta (Optional[:data:`~LazyText_T`], optional): The custom display meta (description) to apply to completion results for the action active (Optional[~prompt_toolkit.filters.Filter], optional): A callable filter that results in a boolean to indicate if the action group should be considred as active and displayed as a completion result capture_all (bool, optional): If there is the option for this action to accept more arguments than defined by the provided parameters, this flag will allow for any number of following arguments (after parameters) as additional positional arguments to the provided ``action`` callable, defaults to False Raises: ValueError: When the given action name contains spaces ValueError: When the given action name is already present in the group Returns: Callable[..., Any]: The newly wrapped action callable """ if len(name) <= 0: raise ValueError(f"action names must contain characters, {name!r}") if re.findall(r"\s+", name): raise ValueError( f"actions can not use names including whitespace, {name!r}" ) if name in self.children: raise ValueError( f"name {name!r} already registered as {self.children[name]!r}" ) def action_wrapper(func: Callable[..., Any]) -> Callable[..., Any]: action_parameters = [] if params: action_parameters.extend(params) action_parameters.extend(reversed(_decorator_staging.get(func) or [])) action = Action( action=func, params=action_parameters, style=style, selected_style=selected_style, display=display, display_meta=display_meta, active=active, capture_all=capture_all, ) # Delete the staged action parameters for a specific function if available if func in _decorator_staging: del _decorator_staging[func] else: # In case we don't detect any available parameters we are marking the # function as None indicating that no parameters were dynamically added # to the action with the staging dictionary. # This is later used in the @param decorator to determine if a parameter # is being added to an already registered action and therefore the param # SHOULD NOT (not could not) be added to the action. This None value is # used as a sentinel to indicate the action has already been registered. # Any state where action is not currently registered should be either: # 1. The function does not appear in the dictionary # 2. The dictionary value for the function key is an instance of a # list of ActionParams _decorator_staging[func] = None self.children[name] = action return func return action_wrapper
[docs]def param( source: ActionParamSource_T, cast: Optional[Type] = None, style: Optional[LazyString_T] = None, selected_style: Optional[LazyString_T] = None, display: Optional[LazyText_T] = None, display_meta: Optional[LazyText_T] = None, validators: Optional[List[ActionParamValidator_T]] = None, ) -> Callable[..., Any]: """Create a new action parameter for an action. Basic parameters can be defined right before defining a method as an action. .. code-block:: from pathlib import Path from prompt_toolkit.shortcuts import prompt from prompt_toolkit.validation import Validator from action_completer import ActionCompleter completer = ActionCompleter() @completer.action("cat") @completer.param( PathCompleter(), cast=Path, validators=[Validator.from_callable(lambda p: Path(p).is_file())] ) def _cat_action(filepath: Path): with filepath.open("r") as file_handle: print(file_handle.read()) completer.run_action(prompt(">>> ", completer=completer)) # available through the following prompt: # # $ echo "my content" > ./my-file.txt # >>> cat ./my-file.txt # my content .. important:: The application order of ``param`` decorators is very important. Since these defined action parameters are applied as positional arguments to the action, we are opinionated on the ordering that decorators should be applied. We purposefully reverse the traditional decorator application order to make the readability of actions created purely through decorators a bit easier. This means that the **last** parameter should be the **first** decorator applied to the action callable, and the **very last** decorator applied to the action callable should be the action decorator. This benefits readability by ordering the defined param decorators top-to-bottom as arguments applied left-to-right in the action callable. .. code-block:: python # valid @action("test") @param("source1") @param("source2") def _valid_action(source1: str, source2: str): ... # invalid @param("source2") @param("source1") @action("test") def _invalid_action(source1: str, source2: str): ... # also invalid @action("test") @param("source2") @param("source1") def _also_invalid_action(source1: str, source2: str): ... We have *some* checks to raise warnings in case you accidentally use an invalid spec for applying the decorators. Although I'm not 100% sure it will capture all the possible states of defining actions that you might come up with. If you don't like this design, I would recommend you instead pass :class:`~ActionParam` instances to the ``params`` keyword argument when using the :meth:`~ActionGroup.action` decorator. Args: source (:data:`~ActionParamSource_T`): The completion source for the parameter cast (Optional[~typing.Type], optional): The type to cast the action parameter to during execution of the action tied to this parameter style (Optional[:data:`~LazyString_T`], optional): The style string to apply to completion results for the action parameter selected_style (Optional[:data:`~LazyString_T`], optional): The style string to apply to selected completion results for the action parameter display (Optional[:data:`~LazyText_T`], optional): The custom display to apply to completion results for the action parameter display_meta (Optional[:data:`~LazyText_T`], optional): The custom display meta (description) to apply to completion results for the action parameter validators (Optional[List[:data:`~ActionParamValidator_T`]], optional): The list of validators to run in-order against the parameter value during validation Returns: Callable[..., Any]: The newly wrapped action with the defined parameters """ def param_wrapper(func: Callable[..., Any]) -> Callable[..., Any]: action_param = ActionParam( source=source, cast=cast, style=style, selected_style=selected_style, display=display, display_meta=display_meta, validators=validators, ) parameter_store = _decorator_staging.get(func, []) # When the parameter's function is already registered and marked with the None # sentinel, we are attempting to add a parameter to an action that has already # been registered and therefore should not have any more parameters added. if parameter_store is None: warnings.warn( f"Action parameter with source {source!r} for {func!r} could not be " "applied to the already registered action, make sure you decorate the " "action callable with the @action decorator last " "(on top of all @param decorators)", UserWarning, ) return func parameter_store.append(action_param) _decorator_staging[func] = parameter_store return func return param_wrapper