PK!0 CHANGES.md# AutoClick Changes ## 0.6.0 (2019.06.07) * Add additional types * Add version option * EnumChoice: don't try to convert if the value is already an enum ## 0.5.1 (2019.04.11) * Fix bug in get_conversion in which wrong type was being used to look up automatic conversion function ## 0.5.0 (2019.04.10) * Breaking change: Major refactoring to split up core.py and make import graph acyclic * Add defaults for validations.Defined * Breaking change: Rename validations.ge_defined to validations.defined_ge ## 0.4.0 (2019.04.09) * Add additional types (DelimitedList) and validations (SequenceLength) * Add decorator for conversions that can be automatically applied based on the parameter type ## 0.3.0 (2019.03.29) * Handle tuple types for which the desired default value is `None` (rather than a sequence of `None`s) * Make ValidationError inherit from UsageError * Fix Defined validations * By default do not create an instance of a composite types if none of its parameters are defined ## 0.2.3 (2019.01.15) * Add generic GLOBAL_OPTIONS * Fix handling of collection types * Add new Mutex validation * Add ability to pass function/class to be decorted to most decorators * Fix argument parsing when using prefixes for composite types ## 0.2.2 (2018.12.30) * Fix pass_context * Fix AutoClickGroup parse_args() ## 0.2.1 (2018.12.18) * Add pass_context to command() and group() decorators ## 0.2.0 (2018.12.18) * Code reorg * Name change to autoclick * Better support for composites ## 0.1.0 (2018.12.04) * Initial releasePK!Lautoclick/__init__.pyimport pkg_resources from autoclick.utils import LOG, set_global from autoclick.commands import command, group from autoclick.core import SignatureError, ParameterCollisionError from autoclick.composites import ( TypeCollisionError, create_composite, composite_type, composite_factory ) from autoclick.composites.library import * from autoclick.types import conversion from autoclick.types.library import * from autoclick.validations import ValidationError, validation from autoclick.validations.library import * # Load modules for validation and composite plugins for entry_point in pkg_resources.iter_entry_points(group='autoclick'): LOG.debug("Loading plugin entry-point %s", str(entry_point)) entry_point.load() PK!tZPPautoclick/commands.pyfrom abc import ABCMeta, abstractmethod from typing import Any, Callable, Dict, Optional, Sequence, Set, Type, Union, cast import click from autoclick.composites import Composite, get_composite from autoclick.core import DEC, BaseDecorator, ParameterInfo, apply_to_parsed_args from autoclick.utils import EMPTY, LOG, get_global class CommandMixin: """ Mixin class that overrides :func:`parse_args` to apply validations and conditionals, and to resolve composite types. """ def __init__( self, *args, conditionals: Dict[Sequence[str], Sequence[Callable]], validations: Dict[Sequence[str], Sequence[Callable]], composite_callbacks: Sequence[Callable[[dict], None]], used_short_names: Set[str], **kwargs ): super().__init__(*args, **kwargs) self._conditionals = conditionals or {} self._validations = validations or {} self._composite_callbacks = composite_callbacks or {} self._used_short_names = used_short_names or {} def parse_args(self, ctx, args): click.Command.parse_args(cast(click.Command, self), ctx, args) apply_to_parsed_args(self._conditionals, ctx.params, update=True) apply_to_parsed_args(self._validations, ctx.params, update=False) for callback in self._composite_callbacks: callback(ctx) return args class AutoClickCommand(CommandMixin, click.Command): """ Subclass of :class:`click.Command` that also inherits :class:`CommandMixin`. """ pass class AutoClickGroup(CommandMixin, click.Group): """ Subclass of :class:`click.Group` that also inherits :class:`CommandMixin`. Args: match_prefix: Whether to look for a command that starts with the specified name if the command name cannot be matched exactly. """ def __init__(self, *args, match_prefix: bool = False, **kwargs): super().__init__(*args, **kwargs) self._match_prefix = match_prefix def command( self, name: Optional[str] = None, decorated: Optional[Callable] = None, **kwargs ): """A shortcut decorator for declaring and attaching a command to the group. This takes the same arguments as :func:`command` but immediately registers the created command with this instance by calling into :meth:`add_command`. """ def decorator(f): cmd = command( name=name, used_short_names=self._used_short_names, **kwargs ) click_command = cmd(f) self.add_command(click_command) return click_command if decorated: return decorator(decorated) else: return decorator def group( self, name: Optional[str] = None, decorated: Optional[Callable] = None, **kwargs ): """A shortcut decorator for declaring and attaching a group to the group. This takes the same arguments as :func:`group` but immediately registers the created command with this instance by calling into :meth:`add_command`. """ def decorator(f): grp = group( name=name, used_short_names=self._used_short_names, **kwargs ) click_group = grp(f) self.add_command(click_group) return click_group if decorated: return decorator(decorated) else: return decorator def get_command(self, ctx, cmd_name): cmd = click.Group.get_command(self, ctx, cmd_name) if cmd is not None: return cmd matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)] if not matches: return None elif len(matches) == 1: return click.Group.get_command(self, ctx, matches[0]) else: ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") def parse_args(self, ctx, args): if not args and self.no_args_is_help and not ctx.resilient_parsing: click.echo(ctx.get_help(), color=ctx.color) ctx.exit() rest = CommandMixin.parse_args(self, ctx, args) if self.chain: ctx.protected_args = rest ctx.args = [] elif rest: ctx.protected_args, ctx.args = rest[:1], rest[1:] return ctx.args class DefaultAutoClickGroup(AutoClickGroup): """ """ def __init__( self, *args, invoke_without_command: bool = None, no_args_is_help: bool = None, default: Optional[str] = None, default_if_no_args: bool = False, **kwargs ): if default_if_no_args: if invoke_without_command is False or no_args_is_help is True: raise ValueError( "One one of 'no_args_is_help', 'default_if_no_args' may be True." ) invoke_without_command = True no_args_is_help = False super().__init__( *args, invoke_without_command=invoke_without_command, no_args_is_help=no_args_is_help, **kwargs ) self._default_cmd_name = default self._default_if_no_args = default_if_no_args def set_default_command(self, cmd): """Sets a command function as the default command. """ self._default_cmd_name = cmd.name self.add_command(cmd) def parse_args(self, ctx, args): if not args and self._default_if_no_args: args.insert(0, self._default_cmd_name) return super().parse_args(ctx, args) def get_command(self, ctx, cmd_name): if cmd_name not in self.commands: # No command name matched. ctx.arg0 = cmd_name cmd_name = self._default_cmd_name return super().get_command(ctx, cmd_name) def resolve_command(self, ctx, args): base = super() cmd_name, cmd, args = base.resolve_command(ctx, args) if hasattr(ctx, 'arg0'): args.insert(0, ctx.arg0) return cmd_name, cmd, args def format_commands(self, ctx, formatter): formatter = DefaultCommandFormatter(self, formatter, mark='*') return super().format_commands(ctx, formatter) class DefaultCommandFormatter: """Wraps a formatter to mark a default command. """ def __init__(self, group_, formatter, mark='*'): self._group = group_ self._formatter = formatter self._mark = mark def __getattr__(self, attr): return getattr(self.formatter, attr) def write_dl(self, rows, *args, **kwargs): rows_ = [] for cmd_name, help_str in rows: if cmd_name == self._group.default_cmd_name: rows_.insert(0, (cmd_name + self._mark, help_str)) else: rows_.append((cmd_name, help)) return self._formatter.write_dl(rows_, *args, **kwargs) class BaseCommandDecorator(BaseDecorator[DEC], metaclass=ABCMeta): """ Base class for decorators that wrap command functions. """ def __init__( self, name: Optional[str] = None, composite_types: Optional[Dict[str, Composite]] = None, add_composite_prefixes: bool = True, command_help: Optional[str] = None, option_class: Type[click.Option] = click.Option, argument_class: Type[click.Argument] = click.Argument, extra_click_kwargs: Optional[dict] = None, used_short_names: Optional[Set[str]] = None, default_values: Optional[Dict[str, Any]] = None, version: Optional[Union[str, bool]] = None, pass_context: Optional[bool] = None, **kwargs ): super().__init__(**kwargs) self._name = name self._composite_types: Dict[str, Composite] = composite_types or {} self._add_composite_prefixes = get_global( "add_composite_prefixes", add_composite_prefixes ) self._command_help = command_help self._option_class = option_class self._argument_class = argument_class self._extra_click_kwargs = extra_click_kwargs or {} self._used_short_names = set() if used_short_names: self._used_short_names.update(used_short_names) self._default_values = default_values or {} self._pass_context = get_global("pass_context", pass_context) self._allow_extra_arguments = False self._allow_extra_kwargs = False self._add_version_option = version @property def name(self) -> str: """The command name.""" return self._name or self._decorated.__name__.lower().replace('_', '-') def _handle_parameter_info(self, param: ParameterInfo) -> bool: if param.extra_arguments: self._allow_extra_arguments = True return False elif param.extra_kwargs: self._allow_extra_kwargs = True return False return super()._handle_parameter_info(param) def _create_decorator(self) -> DEC: parameter_infos = self._get_parameter_info() command_params = [] composite_callbacks = [] # TODO # if self._add_version_option: # command_params.append() if self._pass_context: ctx_param = list(parameter_infos.keys())[0] if parameter_infos[ctx_param].anno_type in {click.Context, EMPTY, None}: parameter_infos.pop(ctx_param) if ctx_param in self._option_order: self._option_order.remove(ctx_param) else: LOG.warning( "pass_context set to True, but first parameter of function %s " "does not appear to be of type click.Context", self.name ) for param_name in self._option_order: param = parameter_infos[param_name] composite: Composite = ( self._composite_types[param_name] if param_name in self._composite_types else get_composite(param.match_type) ) if composite: click_parameters, callback = composite.create_click_parameters( param=param, used_short_names=self._used_short_names, default_values=self._default_values, add_prefixes=self._add_composite_prefixes, hidden=param.name in self._hidden, option_class=self._option_class, argument_class=self._argument_class, help_text=self._get_help(param.name), force_positionals_as_options=self._positionals_as_options ) command_params.extend(click_parameters) composite_callbacks.append(callback) else: command_params.append(self._create_click_parameter( param=param, used_short_names=self._used_short_names, default_values=self._default_values, option_class=self._option_class, argument_class=self._argument_class )) desc = None if self._docs and self._docs.description: desc = str(self._docs.description) callback = self._decorated if self._pass_context: callback = click.pass_context(callback) # TODO: pass `no_args_is_help=True` unless there are no required parameters click_command = self._create_click_command( name=self.name, callback=callback, help=desc, conditionals=self._conditionals, validations=self._validations, composite_callbacks=composite_callbacks, **self._extra_click_kwargs ) click_command.params = command_params if self._allow_extra_arguments: click_command.allow_extra_arguments = True if self._allow_extra_kwargs: click_command.ignore_unknown_options = False return click_command @abstractmethod def _create_click_command(self, **kwargs) -> DEC: pass # noinspection PyPep8Naming class command(BaseCommandDecorator[click.Command]): """ Decorator that creates a click.Command based on type annotations of the annotated function. Args: command_class: Class to use when creating the :class:`click.Command`. This must inherit from :class:`CommandMixin`. name: The command name. If not specified, it is taken from the name of the annotated function. composite_types: Dict mapping parameter names to :class:`CompositeParameter` objects. add_composite_prefixes: By default, the parameter name is added as a prefix when deriving the option names for composite parameters. If set to false, each composite type may only be used for at most one parameter, and the user must ensure that no composite parameter names conflict with each other or with other parameter names in the annotated function. default_values: Specify default values for parameters. The primary usage is to specify default values for hidden parameters of composite types. Otherwise, it is better to specify default values in the signature of the command function. command_help: Command description. By default, this is extracted from the funciton docstring. option_class: Class to use when creating :class:`click.Option`s. argument_class: Class to use when creating :class:`click.Argument`s. extra_click_kwargs: Dict of extra arguments to pass to the :class:`click.Command` constructor. keep_underscores: Whether underscores should be retained in option names (True) or converted to hyphens (False). short_names: Dictionary mapping parameter names to short names. If not specified, usage of short names depends on `infer_short_names`. Set a value to `None` to disable short name usage for a paramter. infer_short_names: Whether to infer short names from parameter names. See Details on the algorithm used to select the short name. If a parameter has a short name specified in `short_names` it overrides the inferred short name. option_order: Specify an order of option processing that is different from the order in the signature of the annotated function. types: Dict mapping parameter names to functions that perform type conversion. By default, the type of a parameter is inferred from its annotation. positionals_as_options: Whether to treat positional arguments as required options. conditionals: Dict mapping paramter names or tuples of parameter names to conditional functions or lists of conditinal functions. validations: Dict mapping paramter names or tuples of parameter names to validation functions or lists of validation functions. required: Sequence of required options. If not specified, only paramters without default values are required. show_defaults: Whether to show defaults in the help text. hidden: Sequence of hidden options. These options are not displayed in the help text. param_help: Dict mapping parameters to help strings. By default, these are extracted from the function docstring. pass_context: Whether to pass in the click.Context as the first argument to the function. """ def __init__( self, name: str = None, command_class: Type[CommandMixin] = AutoClickCommand, **kwargs ): super().__init__(name=name, **kwargs) self._command_class = command_class def _create_click_command(self, **kwargs) -> click.Command: return cast(click.Command, self._command_class( used_short_names=self._used_short_names, **kwargs )) # noinspection PyPep8Naming class group(BaseCommandDecorator[click.Group]): """ Decorator that creates a :class:`click.Group` based on type annotations of the annotated function. Args: group_class: Class to use when creating the :class:`click.Group`. This must inherit from :class:`CommandMixin`. name: The command name. If not specified, it is taken from the name of the annotated function. composite_types: Dict mapping parameter names to :class:`CompositeParameter` objects. add_composite_prefixes: By default, the parameter name is added as a prefix when deriving the option names for composite parameters. If set to false, each composite type may only be used for at most one parameter, and the user must ensure that no composite parameter names conflict with each other or with other parameter names in the annotated function. default_values: Specify default values for parameters. The primary usage is to specify default values for hidden parameters of composite types. Otherwise, it is better to specify default values in the signature of the command function. command_help: Command description. By default, this is extracted from the funciton docstring. option_class: Class to use when creating :class:`click.Option`s. argument_class: Class to use when creating :class:`click.Argument`s. extra_click_kwargs: Dict of extra arguments to pass to the :class:`click.Command` constructor. keep_underscores: Whether underscores should be retained in option names (True) or converted to hyphens (False). short_names: Dictionary mapping parameter names to short names. If not specified, usage of short names depends on `infer_short_names`. Set a value to `None` to disable short name usage for a paramter. infer_short_names: Whether to infer short names from parameter names. See Details on the algorithm used to select the short name. If a parameter has a short name specified in `short_names` it overrides the inferred short name. option_order: Specify an order of option processing that is different from the order in the signature of the annotated function. types: Dict mapping parameter names to functions that perform type conversion. By default, the type of a parameter is inferred from its annotation. positionals_as_options: Whether to treat positional arguments as required options. conditionals: Dict mapping paramter names or tuples of parameter names to conditional functions or lists of conditinal functions. validations: Dict mapping paramter names or tuples of parameter names to validation functions or lists of validation functions. required: Sequence of required options. If not specified, only paramters without default values are required. show_defaults: Whether to show defaults in the help text. hidden: Sequence of hidden options. These options are not displayed in the help text. param_help: Dict mapping parameters to help strings. By default, these are extracted from the function docstring. pass_context: Whether to pass in the click.Context as the first argument to the function. """ def __init__( self, name: str = None, group_class: Type[CommandMixin] = AutoClickGroup, commands: Optional[Dict[str, click.Command]] = None, **kwargs ): super().__init__(name=name, **kwargs) self._group_class = group_class self._extra_click_kwargs["commands"] = commands or {} def _create_click_command(self, **kwargs) -> click.Group: return cast(click.Group, self._group_class( used_short_names=self._used_short_names, **kwargs )) PK!'7'7 autoclick/composites/__init__.pyfrom abc import ABCMeta, abstractmethod import functools import inspect from typing import ( Any, Callable, Dict, Optional, Sequence, Set, Tuple, Type, Union, cast ) import click from autoclick.core import ( DEC, BaseDecorator, ParameterInfo, SignatureError, apply_to_parsed_args ) from autoclick.utils import get_dest_type class TypeCollisionError(Exception): """Raised when a decorator is defined for a type for which a decorator of the same kind has already been defined. """ class Composite(BaseDecorator[DEC], metaclass=ABCMeta): """ Represents a complex type that requires values from multiple parameters. A composite parameter is defined by annotating a class using the `composite_type` decorator, or by annotating a function with the `composite_factory` decorator. The parameters of the composite type's construtor (exluding `self`) or of the composite factory function are added to the command prior to argument parsing, and then they are replaced by an instance of the annotated class after parsing. Note that composite parameters cannot be nested, i.e. a parameter cannot be a list of composite types, and a composite type cannot itself have composite type parameters - either of these will raise a :class:`SignatureError`. Args: parameters_as_args: Whether to treat all parameters as Arguments regardless of whether they are optional or required. force_create: Always create an instance of the composite type, even if all the parameter values are `None`. kwargs: Keyword arguments passed to :class:`BaseDecorator` constructor. Kwargs: to_wrap: The function/class to wrap. keep_underscores: Whether underscores should be retained in option names (True) or converted to hyphens (False). short_names: Dictionary mapping parameter names to short names. If not specified, usage of short names depends on `infer_short_names`. Set a value to `None` to disable short name usage for a paramter. infer_short_names: Whether to infer short names from parameter names. See Details on the algorithm used to select the short name. If a parameter has a short name specified in `short_names` it overrides the inferred short name. option_order: Specify an order of option processing that is different from the order in the signature of the annotated function. types: Dict mapping parameter names to functions that perform type conversion. By default, the type of a parameter is inferred from its annotation. positionals_as_options: Whether to treat positional arguments as required options. conditionals: Dict mapping paramter names or tuples of parameter names to conditional functions or lists of conditinal functions. validations: Dict mapping paramter names or tuples of parameter names to validation functions or lists of validation functions. required: Sequence of required options. If not specified, only paramters without default values are required. show_defaults: Whether to show defaults in the help text. hidden: Sequence of hidden options. These options are not displayed in the help text. param_help: Dict mapping parameters to help strings. By default, these are extracted from the function docstring. """ def __init__( self, parameters_as_args: bool = False, force_create: bool = False, **kwargs ): self._parameters_as_args = parameters_as_args self.force_create = force_create self._parameters = None super().__init__(**kwargs) @property @abstractmethod def _match_type(self) -> Callable: """The """ pass def _handle_parameter_info(self, param: ParameterInfo) -> bool: if param.extra_arguments or param.extra_kwargs: raise SignatureError( "CompositeType cannot have *args or **kwargs" ) return super()._handle_parameter_info(param) def _create_decorator(self) -> DEC: self._parameters = self._get_parameter_info() if has_composite(self._match_type): raise TypeCollisionError( f"A composite for type {self._match_type} is already defined." ) register_composite(self._match_type, self) return self._decorated def create_click_parameters( self, param: ParameterInfo, used_short_names: Set[str], add_prefixes: bool, hidden: bool, default_values: Dict[str, Any], help_text: str, option_class: Type[click.Option], argument_class: Type[click.Argument], force_positionals_as_options: bool = False ) -> Tuple[Sequence[click.Parameter], Callable[[dict], None]]: """ Create the Click parameters for this composite's signature. Args: param: used_short_names: add_prefixes: hidden: default_values: help_text: option_class: argument_class: force_positionals_as_options: Returns: A tuple (click_parameters, callback), where click_parameters is a list of :class:`click.Option` or :class:`click.Argument` instances, and the callback is the function to be called with the actual parameter values after the command line is parsed. If `self.parameters_as_args` is True, a single :class:`click.Tuple` instance. """ if self._parameters_as_args: param_decls = ["--{}".format(self._get_long_name(param.name))] short_name = self._get_short_name(param.name, used_short_names) if short_name: used_short_names.add(short_name) param_decls.append(f"-{short_name}") types = [] default = [] for param_name in self._option_order: composite_param = self._parameters[param_name] types.append(composite_param.click_type) default.append(default_values.get(param_name, composite_param.default)) click_parameters = [ option_class( param_decls, type=click.Tuple(types), required=not param.optional, default=default, show_default=self._show_defaults, nargs=len(types), hidden=hidden, is_flag=False, multiple=False, help=help_text ) ] else: prefix = param.name if add_prefixes else None click_parameters = [ self._create_click_parameter( param=self._parameters[opt], used_short_names=used_short_names, option_class=option_class, argument_class=argument_class, long_name_prefix=prefix, hidden=hidden, default_values=default_values, force_positionals_as_options=force_positionals_as_options ) for opt in self._option_order ] callback = cast( Callable[[dict], None], functools.partial(self.handle_args, param=param, add_prefixes=add_prefixes) ) return click_parameters, callback def handle_args(self, ctx: click.Context, param: ParameterInfo, add_prefixes: bool): if self._parameters_as_args: kwargs = dict(zip(self._option_order, ctx.params.pop(param.name, ()))) apply_to_parsed_args(self._conditionals, kwargs, update=True) apply_to_parsed_args(self._validations, kwargs, update=False) else: kwargs = {} for composite_param_name in self._parameters.keys(): if add_prefixes: arg_name = f"{param.name}_{composite_param_name}" else: arg_name = composite_param_name kwargs[composite_param_name] = ctx.params.pop(arg_name, None) if ( self.force_create or not param.optional or tuple(filter(None, kwargs.values())) ): ctx.params[param.name] = self._decorated(**kwargs) else: ctx.params[param.name] = None COMPOSITES: Dict[Type, Composite] = {} def has_composite(type_: Type): return type_ in COMPOSITES def register_composite(type_: Type, composite: Composite): # Todo: warn about overwriting existing composite COMPOSITES[type_] = composite def get_composite(type_: Type) -> Optional[Composite]: return COMPOSITES.get(type_, None) # noinspection PyPep8Naming class composite_type(Composite[type]): """ """ @property def _match_type(self): return self._decorated # noinspection PyPep8Naming class composite_factory(Composite[Callable]): """ Annotates a function that returns an instance of a composite type. Args: dest_type: The composite type, i.e. the type that will be recognized in the signature of the command function and matched with this factory function. If not specified, it is inferred from the return type. keep_underscores: Whether underscores should be retained in option names (True) or converted to hyphens (False). short_names: Dictionary mapping parameter names to short names. If not specified, usage of short names depends on `infer_short_names`. Set a value to `None` to disable short name usage for a paramter. infer_short_names: Whether to infer short names from parameter names. See Details on the algorithm used to select the short name. If a parameter has a short name specified in `short_names` it overrides the inferred short name. option_order: Specify an order of option processing that is different from the order in the signature of the annotated function. types: Dict mapping parameter names to functions that perform type conversion. By default, the type of a parameter is inferred from its annotation. positionals_as_options: Whether to treat positional arguments as required options. conditionals: Dict mapping paramter names or tuples of parameter names to conditional functions or lists of conditinal functions. validations: Dict mapping paramter names or tuples of parameter names to validation functions or lists of validation functions. required: Sequence of required options. If not specified, only paramters without default values are required. show_defaults: Whether to show defaults in the help text. hidden: Sequence of hidden options. These options are not displayed in the help text. param_help: Dict mapping parameters to help strings. By default, these are extracted from the function docstring. """ def __init__( self, dest_type: Optional[Type] = None, **kwargs ): super().__init__(**kwargs) self._target = dest_type @property def _match_type(self): return self._target def _create_decorator(self) -> Callable: if self._target is None: self._target = get_dest_type(self._decorated) return super()._create_decorator() def create_composite(to_wrap: Union[Callable, Type], **kwargs) -> Composite: """Creates a :class:`Composite` for use in the `composites` paramter to a `command` or `group` decorator. Kwargs: to_wrap: The function/class to wrap. keep_underscores: Whether underscores should be retained in option names (True) or converted to hyphens (False). short_names: Dictionary mapping parameter names to short names. If not specified, usage of short names depends on `infer_short_names`. Set a value to `None` to disable short name usage for a paramter. infer_short_names: Whether to infer short names from parameter names. See Details on the algorithm used to select the short name. If a parameter has a short name specified in `short_names` it overrides the inferred short name. option_order: Specify an order of option processing that is different from the order in the signature of the annotated function. types: Dict mapping parameter names to functions that perform type conversion. By default, the type of a parameter is inferred from its annotation. positionals_as_options: Whether to treat positional arguments as required options. conditionals: Dict mapping paramter names or tuples of parameter names to conditional functions or lists of conditinal functions. validations: Dict mapping paramter names or tuples of parameter names to validation functions or lists of validation functions. required: Sequence of required options. If not specified, only paramters without default values are required. show_defaults: Whether to show defaults in the help text. hidden: Sequence of hidden options. These options are not displayed in the help text. param_help: Dict mapping parameters to help strings. By default, these are extracted from the function docstring. """ if inspect.isclass(to_wrap): comp = composite_type(**kwargs) else: comp = composite_factory(**kwargs) comp(to_wrap) return comp PK!Azooautoclick/composites/library.pyimport logging from pathlib import Path import sys from typing import Optional from autoclick.composites import composite_factory @composite_factory(hidden=["log_name"]) def log( log_name: str = "DEFAULT", log_level: str = "WARN", log_file: Optional[Path] = None ) -> logging.Logger: """ Args: log_name: log_level: log_file: Returns: """ logger = logging.getLogger(log_name) logger.setLevel(log_level) if log_file: logger.addHandler(logging.FileHandler(log_file)) else: logger.addHandler(logging.StreamHandler(sys.stderr)) return logger PK!b::autoclick/core.pyfrom abc import ABCMeta, abstractmethod import inspect import re from typing import ( Any, Callable, Collection, Dict, Generic, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union, cast ) import click import docparse from autoclick.types import get_conversion from autoclick.types.library import OptionalTuple from autoclick.utils import EMPTY, EMPTY_OR_NONE, LOG, get_global from autoclick.validations import get_validations UNDERSCORES = re.compile("_") ALPHA_CHARS = set(chr(i) for i in tuple(range(97, 123)) + tuple(range(65, 91))) DEC = TypeVar("DEC") class ParameterCollisionError(Exception): """Raised when a composite paramter has the same name as one in the parent function. """ class SignatureError(Exception): """Raised when the signature of the decorated method is not supported. """ class ParameterInfo: """Extracts and contains the necessary information from a :class:`inspect.Parameter`. Args: name: The parameter name. param: A :class:`inspect.Parameter`. click_type: The conversion function, if specified explicitly. required: Whether this is explicitly specified to be a required parameter. """ def __init__( self, name: str, param: inspect.Parameter, click_type: Optional[type] = None, required: bool = False ): self.name = name self.anno_type = param.annotation self.click_type = click_type self.optional = not (required or param.default is EMPTY) self.default = None if param.default is EMPTY else param.default self.nargs = 1 self.multiple = False self.extra_arguments = (param.kind is inspect.Parameter.VAR_POSITIONAL) self.extra_kwargs = (param.kind is inspect.Parameter.VAR_KEYWORD) if self.anno_type in EMPTY_OR_NONE: if not self.optional: LOG.debug( f"No type annotation or default value for parameter " f"{name}; using " ) self.anno_type = str else: self.anno_type = type(self.default) LOG.debug( f"Inferring type {self.anno_type} from paramter {name} " f"default value {self.default}" ) elif isinstance(self.anno_type, str): if self.anno_type in globals(): self.anno_type = globals()[self.anno_type] else: raise SignatureError( f"Could not resolve type {self.anno_type} of paramter {name}" ) # Resolve Union attributes # The only time a Union type is allowed is when it has two args and # one is None (i.e. an Optional) if ( hasattr(self.anno_type, "__origin__") and self.anno_type.__origin__ is Union ): filtered_args = set(self.anno_type.__args__) if type(None) in filtered_args: filtered_args.remove(type(None)) if len(filtered_args) == 1: self.anno_type = filtered_args.pop() self.optional = True self.default = None else: raise SignatureError( f"Union type not supported for parameter {name}" ) self.match_type = self.anno_type def resolve_new_type(t): return t.__supertype__ if ( inspect.isfunction(t) and hasattr(t, "__supertype__") ) else t self.anno_type = resolve_new_type(self.anno_type) # Resolve meta-types if hasattr(self.anno_type, "__origin__"): origin = self.anno_type.__origin__ if hasattr(self.anno_type, "__args__"): if origin == Tuple: # Resolve Tuples with specified arguments if self.click_type is None: self.click_type = click.Tuple([ resolve_new_type(a) for a in self.anno_type.__args__ ]) elif len(self.anno_type.__args__) == 1: self.match_type = self.anno_type.__args__[0] self.anno_type = origin # Unwrap complex types while hasattr(self.anno_type, "__extra__"): self.anno_type = self.anno_type.__extra__ # Set nargs when type is a click.Tuple if isinstance(self.click_type, click.Tuple): self.nargs = len(cast(click.Tuple, self.click_type).types) if self.default is None: # Substitute a subclass of click.Tuple that will convert a sequence # of all None's to a None self.default = (None,) * self.nargs self.click_type = OptionalTuple(self.click_type.types) elif not isinstance(self.default, Sequence): raise SignatureError( f"Default value of paramter {self.name} of type Tuple must be a " f"sequence." ) else: arrity = len(tuple(self.default)) if arrity != self.nargs: raise SignatureError( f"Default value of paramter {self.name} of type Tuple must be " f"a collection having the same arrity; {arrity} != {self.nargs}" ) # Collection types are treated as parameters that can be specified # multiple times if ( self.nargs == 1 and self.anno_type != str and issubclass(self.anno_type, Collection) ): self.multiple = True if self.match_type is None: self.match_type = self.anno_type if self.click_type is None: self.click_type = get_conversion(self.match_type, self.anno_type) self.is_flag = ( self.click_type == bool or isinstance(self.click_type, click.types.BoolParamType) ) class BaseDecorator(Generic[DEC], metaclass=ABCMeta): """ Base class for decorators of groups, commands, and composites. """ def __init__( self, keep_underscores: bool = False, short_names: Optional[Dict[str, str]] = None, infer_short_names: bool = True, option_order: Optional[Sequence[str]] = None, types: Optional[Dict[str, Callable]] = None, positionals_as_options: bool = False, conditionals: Dict[ Union[str, Tuple[str, ...]], Union[Callable, List[Callable]]] = None, validations: Dict[ Union[str, Tuple[str, ...]], Union[Callable, List[Callable]]] = None, required: Optional[Sequence[str]] = None, hidden: Optional[Sequence[str]] = None, show_defaults: bool = False, param_help: Optional[Dict[str, str]] = None, decorated: Optional[Callable] = None ): self._keep_underscores = get_global("keep_underscores", keep_underscores) self._short_names = short_names or {} self._infer_short_names = get_global("infer_short_names", infer_short_names) self._option_order = option_order or [] self._positionals_as_options = positionals_as_options self._types = types or {} self._required = required or set() self._hidden = hidden or set() self._show_defaults = show_defaults self._param_help = param_help or {} self._decorated = None self._docs = None def _as_many_to_many(d): if d is None: return {} else: return dict( ( k if isinstance(k, tuple) else (k,), [v] if v and not isinstance(v, list) else v ) for k, v in d.items() ) self._conditionals = _as_many_to_many(conditionals) self._validations = _as_many_to_many(validations) if decorated: self(decorated=decorated) def __call__(self, decorated: Callable) -> DEC: self._decorated = decorated # TODO: support other docstring styles self._docs = docparse.parse_docs(decorated, docparse.DocStyle.GOOGLE) return self._create_decorator() @abstractmethod def _create_decorator(self) -> DEC: pass def _get_parameter_info(self) -> Dict[str, ParameterInfo]: if inspect.isclass(self._decorated): signature_parameters = dict( inspect.signature(cast(type, self._decorated).__init__).parameters ) signature_parameters.pop("self") else: signature_parameters = dict( inspect.signature(cast(Callable, self._decorated)).parameters ) parameter_infos = {} for name, sig_param in signature_parameters.items(): param = ParameterInfo( name, sig_param, self._types.get(name, None), name in self._required ) if self._handle_parameter_info(param): parameter_infos[name] = param return parameter_infos def _handle_parameter_info(self, param: ParameterInfo) -> bool: """ Register parameter. Subclasses can override this method to filter out some paramters. Args: param: A :class:`ParameterInfo`. Returns: True if this parameter should be added to the parser. """ if param.name not in self._option_order: self._option_order.append(param.name) vals = get_validations(param.match_type) if vals: if param.name not in self._validations: self._validations[(param.name,)] = [] self._validations[(param.name,)].extend(vals) return True def _create_click_parameter( self, param: ParameterInfo, used_short_names: Set[str], default_values: Dict[str, Any], option_class: Type[click.Option], argument_class: Type[click.Argument], long_name_prefix: Optional[str] = None, hidden: bool = False, force_positionals_as_options: bool = False ) -> click.Parameter: """Create a click.Parameter instance (either Option or Argument). Args: param: A :class:`ParameterInfo`. used_short_names: A set of short names that have been used by other parameters and thus should not be re-used. default_values: option_class: Class to instantiate for option parameters. argument_class: Class to instantiate for argument parameters. long_name_prefix: Prefix to add to long option names. hidden: Whether to not show the parameter in help text. force_positionals_as_options: Whether to force positional arguments to be treated as options. Returns: A :class:`click.Parameter`. """ param_name = param.name long_name = self._get_long_name(param_name, long_name_prefix) if ( param.optional or force_positionals_as_options or self._positionals_as_options ): if not param.is_flag: long_name_decl = f"--{long_name}" elif long_name.startswith("no-"): long_name_decl = f"--{long_name[3:]}/--{long_name}" else: long_name_decl = f"--{long_name}/--no-{long_name}" param_decls = [long_name_decl] short_name = self._get_short_name(param_name, used_short_names) if short_name: used_short_names.add(short_name) param_decls.append(f"-{short_name}") return option_class( param_decls, type=None if param.is_flag else param.click_type, required=not param.optional, default=default_values.get(param_name, param.default), show_default=self._show_defaults, nargs=param.nargs, hidden=hidden or param_name in self._hidden, multiple=param.multiple, help=self._get_help(param_name) ) else: # TODO: where to show argument help? return argument_class( [long_name], type=param.click_type, default=default_values.get(param_name, param.default), nargs=-1 if param.nargs == 1 and param.multiple else param.nargs ) def _get_short_name(self, name: str, used_short_names: Set[str]): short_name = self._short_names.get(name, None) if short_name and short_name in used_short_names: raise ParameterCollisionError( f"Short name {short_name} defined for two different parameters" ) elif not short_name and self._infer_short_names: for char in name: if char.isalpha(): if char.lower() not in used_short_names: short_name = char.lower() elif char.upper() not in used_short_names: short_name = char.upper() else: continue break else: # try to select one randomly remaining = ALPHA_CHARS - used_short_names if len(remaining) == 0: raise click.BadParameter( f"Could not infer short name for parameter {name}" ) # TODO: this may not be deterministic short_name = remaining.pop() return short_name def _get_long_name(self, name: str, prefix: Optional[str] = None): long_name = name if prefix: long_name = f"{prefix}_{long_name}" if not self._keep_underscores: long_name = UNDERSCORES.sub("-", long_name) return long_name def _get_help(self, name: str): if name in self._param_help: return self._param_help[name] elif self._docs and self._docs.parameters and name in self._docs.parameters: return str(self._docs.parameters[name].description) def apply_to_parsed_args(d, values: dict, update=False): for params, fns in d.items(): fn_kwargs = dict( (param, values.get(param, None)) for param in params ) for fn in fns: result = fn(**fn_kwargs) if result and update: for param, value in result.items(): values[param] = value PK!Ndm autoclick/parameters.pyfrom pathlib import Path import sys from typing import Optional, Type import click def version_option( version: Optional[str] = None, module: Optional[str] = None, *param_decls, prog_name: Optional[str] = None, message: str = "{prog}, version {ver}", option_class: Type[click.Option] = click.Option, **kwargs ) -> click.Option: """Adds a ``--version`` option which immediately ends the program printing out the version number. This is implemented as an eager option that prints the version and exits the program in the callback. Args: version: the version number to show. If not provided, attempts an auto discovery via setuptools. module: prog_name: the name of the program (defaults to autodetection) message: custom message to show instead of the default (``"{prog}, version {ver}"``) option_class: kwargs: everything else is forwarded to :func:`option`. """ def callback(ctx, param, value): if not value or ctx.resilient_parsing: return ver = version or get_version(module or get_module()) prog = prog_name or ctx.find_root().info_name click.echo(message.format(prog, ver), color=ctx.color) ctx.exit() kwargs.setdefault("is_flag", True) kwargs.setdefault("expose_value", False) kwargs.setdefault("is_eager", True) kwargs.setdefault("help", "Show the version and exit.") kwargs["callback"] = callback return option_class(*(param_decls or ("--version",)), **kwargs) def get_module(): if hasattr(sys, "_getframe"): return sys._getframe(1).f_globals.get("__name__") else: raise RuntimeError("Could not determine module") def get_version(module): ver = None # Try pkg_resources try: import pkg_resources except ImportError: pass else: try: # Try get_distribution ver = pkg_resources.get_distribution(module).version except: # Fall back to looking for entry point for dist in pkg_resources.working_set: scripts = dist.get_entry_map().get("console_scripts") or {} for script_name, entry_point in scripts.items(): if entry_point.module_name == module: ver = dist.version break # Try pyproject.toml try: toml_mod = get_toml_parser() except ImportError: pass else: path = Path.cwd() while path: pyproj = path / "pyproject.toml" if pyproj.exists(): with open(pyproj, "rt") as inp: ver = toml_mod.parse(inp.read())["tool"]["poetry"]["version"] else: path = path.parent if ver is None: raise RuntimeError("Could not determine version") return ver def get_toml_parser(): try: import tomlkit return tomlkit except ImportError: pass import toml return toml PK!bn11autoclick/types/__init__.pyfrom typing import Callable, Collection, Dict, Optional, Tuple, Type import click from autoclick.utils import get_dest_type CONVERSIONS: Dict[Type, click.ParamType] = {} AUTOCONVERSIONS = [] DEFAULT_METAVAR = "ARG" class ParamTypeAdapter(click.ParamType): """ Adapts a conversion function to a :class:`click.ParamType`. Args: name: fn: """ def __init__(self, name, fn): self.name = name self.fn = fn def convert(self, value, param, ctx): return self.fn(value, param, ctx) def conversion( dest_type: Optional[Type] = None, depends: Optional[Tuple[Callable, ...]] = None, decorated: Optional[Callable] = None ) -> Callable: """Annotates a conversion function. Args: dest_type: Destination type for this conversion. If None, it is inferred from the return type of the annotated function. depends: Functions on which this conversion depends. They are called in order, with the output from each function being passed as the input to the next. The type of the parameter to the conversion function must be the return type of the last dependency. decorated: The function to decorate. Returns: A decorator function. """ def decorator(f: Callable) -> Callable: _dest_type = dest_type if _dest_type is None: _dest_type = get_dest_type(f) if depends: def composite_conversion(value): for dep in depends: value = dep(value) return f(value) target = composite_conversion else: target = f click_type = ParamTypeAdapter(_dest_type.__name__, target) register_conversion(_dest_type, click_type) return target if decorated: return decorator(decorated) else: return decorator def register_conversion(type_: Type, click_type: click.ParamType): """ Args: type_: click_type: """ CONVERSIONS[type_] = click_type def has_conversion(type_: Type) -> bool: """ Args: type_: Returns: """ return type_ in CONVERSIONS def get_conversion(match_type: Type, true_type: Optional[Type] = None) -> Callable: """ Gets a conversion function for the given type, if one is available. Args: match_type: Type to match against. true_type: Type to auto-convert, if `match_type` does not match any registered conversions. Returns: A Callable - generally either a click.ParamType (if a conversion was found) or `true_type`. """ if match_type in CONVERSIONS: return CONVERSIONS[match_type] if true_type is None: true_type = match_type for filter_fn, conversion_fn in AUTOCONVERSIONS: if filter_fn(true_type): return conversion_fn(true_type) if issubclass(true_type, Collection): return match_type return true_type def autoconversion( filter_fn: Callable[[Type], bool], conversion_fn: Optional[Callable[[Type], click.ParamType]] = None ): """ Decorator Args: filter_fn: Function that returns a boolean indicating whether or not the autoconversion applies to a given type. conversion_fn: Function that returns a `click.ParamType` for a given type. Returns: An object that is a subclass of `click.ParamType`. """ def decorator(fn): AUTOCONVERSIONS.append((filter_fn, conversion_fn or fn)) return fn return decorator PK!kautoclick/types/library.pyfrom abc import ABCMeta, abstractmethod from enum import Enum, EnumMeta from numbers import Number import pathlib import re from typing import ( Callable, Dict, Generic, Iterable, Match, NewType, Optional, Pattern, Sequence, Tuple, Type, TypeVar, Union, cast ) import click from autoclick.types import DEFAULT_METAVAR, autoconversion T = TypeVar("T") ReadablePath = NewType("ReadablePath", pathlib.Path) ReadableFile = NewType("ReadableFile", pathlib.Path) ReadableDir = NewType("ReadableDir", pathlib.Path) WritablePath = NewType("WritablePath", pathlib.Path) WritableFile = NewType("WritableFile", pathlib.Path) WritableDir = NewType("WritableDir", pathlib.Path) class BaseType(click.ParamType): def __init__(self, metavar: str = None): self.metavar = metavar def get_metavar(self, param): return self.metavar or DEFAULT_METAVAR class Directory(BaseType): name = "dir" def __init__( self, *args, create: bool = False, metavar: str = "DIR", **kwargs ): super().__init__(metavar) kwargs.update( file_okay=False, dir_okay=True, readable=True, writable=True ) self._path_type = click.types.Path(*args, **kwargs) self._create = create def convert(self, value, param, ctx) -> pathlib.Path: path = pathlib.Path(self._path_type(value, param, ctx)) if self._create and not path.exists(): path.mkdir(parents=True) return path class OptionalTuple(click.types.Tuple): def __init__(self, types): super().__init__([]) self.types = types def __call__(self, value, param=None, ctx=None): if value is None or all(v is None for v in value): return None return super().__call__(value, param, ctx) class DelimitedList(BaseType): name = "list" def __init__( self, item_type: Callable[[str], T] = str, delimiter=",", strip_whitespace=True, choices: Optional[Iterable[T]] = None, metavar: str = "LIST" ): super().__init__(metavar) self.item_type = item_type self.delimiter = delimiter self.strip_whitespace = strip_whitespace self.choice = click.Choice(list(choices)) if choices else None def convert(self, value, param, ctx) -> Tuple[T]: if not value: return cast(Tuple[T], ()) items = value.split(self.delimiter) if self.strip_whitespace: items = (item.strip() for item in items) items = tuple(self.item_type(item) for item in items) if self.choice: items = tuple(self.choice.convert(i, param, ctx) for i in items) return items class RegExp(BaseType, metaclass=ABCMeta): name = "regexp" def __init__( self, pattern: Union[str, Pattern], exact: bool = True, metavar: str = "PATTERN" ): super().__init__(metavar) if isinstance(pattern, str): self.pattern = re.compile(pattern) else: self.pattern = cast(Pattern, pattern) self.exact = exact def convert(self, value, param, ctx): if self.exact: match = self.pattern.match(value) else: match = self.pattern.search(value) return self._handle_match(match, value, param, ctx) @abstractmethod def _handle_match(self, match: Match, value, param, ctx): pass class Matches(RegExp): """Returns True if the regular expression matches the value. """ def _handle_match(self, match: Match, value, param, ctx): return match is not None class Parse(RegExp): """Uses regular expression to parse a value and returns the capture groups as a tuple. Raises: BadParameter if the regular expression does not match the value. """ def _handle_match(self, match, value, param, ctx): if match is None: self.fail("Pattern {} does not match value {}".format( self.pattern, value), ctx=ctx, param=param) return match.groups() E = TypeVar("E", bound=EnumMeta) @autoconversion(lambda type_: issubclass(type_, Enum)) class EnumChoice(Generic[E], BaseType): """Translates string values into enum instances. Args: enum_class: Callable (typically a subclass of Enum) that returns enum instances. xform: How to transform string values before passing to callable; upper = convert to upper case; lower = convert to lower case; None = don't convert. (default = upper) """ name = "choice" def __init__( self, enum_class: Sequence[E], xform="upper", metavar: Optional[str] = None ): super().__init__(metavar) self.choice = click.Choice(list(e.name for e in list(enum_class))) self.metavar = metavar or self.choice.get_metavar(None) self.enum_class = enum_class if xform == "upper": self.xform = str.upper elif xform == "lower": self.xform = str.lower else: self.xform = lambda s: s def convert(self, value, param, ctx) -> E: if isinstance(value, str): return self.enum_class[self.choice.convert(self.xform(value), param, ctx)] else: return cast(E, value) def get_missing_message(self, param): return self.choice.get_missing_message(param) def __repr__(self): return str(self.choice) K = TypeVar("K") V = TypeVar("V") @autoconversion(lambda type_: issubclass(type_, dict)) class Mapping(BaseType): """ Todo: support conversion of key and value types. Need to expose core.CONVERSIONS. """ name = "mapping" def __init__( self, key_type: Type[K], value_type: Type[V], metavar: Optional[str] = None ): super().__init__(metavar) def convert(self, value, param, ctx) -> Dict[K, V]: return dict(item.split("=") for item in value) N = TypeVar('N', bound=Number) class BaseNumericType(Generic[N], BaseType): def __init__( self, datatype: Callable[..., Optional[N]], name: str = None, metavar: str = None ): self.datatype = datatype type_name = datatype.__name__.upper() self.name = name or self._get_default_name(type_name) super().__init__(metavar=metavar or type_name) def _get_default_name(self, type_name: str) -> str: pass def convert(self, value, param, ctx) -> Optional[N]: try: return self.datatype(value) except (ValueError, UnicodeError): self.fail(f'{value} is not a valid {self.datatype}', param, ctx) class Positive(Generic[N], BaseNumericType[N]): def _get_default_name(self, type_name: str) -> str: return f'POSITIVE_{type_name}' def convert(self, value, param, ctx) -> Optional[N]: value = super().convert(value, param, ctx) if value is not None and value < self.datatype(0): self.fail(f'{self.datatype} value must be >= 0', param, ctx) return value class Range(Generic[N], BaseNumericType[N]): def __init__( self, datatype: Callable[..., Optional[N]], min_value=None, max_value=None, **kwargs ): self.min_value = min_value self.max_value = max_value if datatype is None: datatype = type(min_value) super().__init__(datatype, **kwargs) def _get_default_name(self, type_name: str) -> str: return f'RANGE_{self.datatype}' def convert(self, value, param, ctx) -> Optional[N]: value = super().convert(value, param, ctx) failures = [] if self.min_value is not None and value < self.min_value: failures.append(f'must be >= {self.min_value}') if self.max_value is not None and value > self.max_value: failures.append(f'must be <= {self.max_value}') if failures: self.fail(f'{value}' + ' and '.join(failures)) return value PK!'mmautoclick/utils.pyimport inspect import logging from typing import Callable, Optional, Type, TypeVar LOG = logging.getLogger("AutoClick") EMPTY = inspect.Signature.empty EMPTY_OR_NONE = {EMPTY, None} GLOBAL_CONFIG = {} T = TypeVar("T") def set_global(name: str, value: T) -> Optional[T]: """ Configure global AutoClick settings: * "infer_short_names": (bool) whether to always/never infer paramter short names. (defaut=True) * "keep_underscores": (bool) whether to retain underscores in paramter long names or covert them to dashes. (defaut=False) * "pass_context": (bool) whether to always/never pass the context to command functions, so that it is not required to specify pass_context=True to every command/group decorator. (defaut=False) Args: name: The global parameter name. value: The paramter value. Returns: The previous value of the setting. """ prev = GLOBAL_CONFIG.get(name, None) if prev != value: GLOBAL_CONFIG[name] = value return prev def get_global(name: str, default: T) -> T: return GLOBAL_CONFIG.get(name, default) def get_match_type(f: Callable) -> Type: params = inspect.signature(f).parameters if len(params) == 0: raise ValueError(f"Function {f} must have at least one parameter") params = list(params.values()) if len(params) > 1: for p in params[1:]: if p.default == EMPTY: raise ValueError( f"All but the first parameter must have default values in " f"the signature of function {f}." ) return params[0].annotation def get_dest_type(f: Callable) -> Type: dest_type = inspect.signature(f).return_annotation if dest_type in EMPTY_OR_NONE: raise ValueError(f"Function {f} must have a non-None return annotation") return dest_type PK!T; !autoclick/validations/__init__.pyimport re from typing import List, Sequence from autoclick.types import * from autoclick.utils import get_match_type VALIDATIONS: Dict[Type, List[Callable]] = {} UNDERSCORES = re.compile("_") ALPHA_CHARS = set(chr(i) for i in tuple(range(97, 123)) + tuple(range(65, 91))) class ValidationError(click.UsageError): """Raised by a validation function when an input violates a constraint. """ def register_validation(type_: Type, validation_fn: Callable): """ Args: type_: validation_fn: """ if type_ not in VALIDATIONS: VALIDATIONS[type_] = [] VALIDATIONS[type_].append(validation_fn) def has_validations(type_: Type) -> bool: """ Args: type_: Returns: """ return type_ in VALIDATIONS def get_validations(type_: Type) -> Optional[Sequence[Callable]]: """ Args: type_: Returns: """ return VALIDATIONS.get(type_, None) def validation( match_type: Optional[Type] = None, depends: Optional[Tuple[Callable, ...]] = None, decorated: Optional[Callable] = None ): """Annotates a single-parameter validation. Args: match_type: The type that will match this validation. If None, is inferred from the type of the first parameter in the signature of the annotated function. depends: Other validations that are pre-requisite for this one. decorated: The function to decorate. Returns: A decorator function. """ def decorator(f: Callable) -> Callable: _match_type = match_type if _match_type is None: _match_type = get_match_type(f) if depends: def composite_validation(**kwargs): for dep in depends: dep(**kwargs) f(**kwargs) target = composite_validation else: target = f # Annotated validation functions can only ever validate a single parameter # so we can explicitly specify the param name and value as kwargs to the # decorated function. def call_target(**kwargs): if len(kwargs) == 2 and set(kwargs.keys()) == {"param_name", "value"}: pass elif len(kwargs) != 1: print(kwargs) raise ValueError( "A @validation decorator may only validate a single parameter." ) else: kwargs = dict(zip(("param_name", "value"), list(kwargs.items())[0])) if kwargs["value"] is not None: target(**kwargs) register_validation(match_type, call_target) return call_target if decorated: return decorator(decorated) else: return decorator PK!F autoclick/validations/library.pyfrom enum import Enum import operator import os import pathlib from typing import Callable, Sequence, Tuple, Union from autoclick.types.library import ( ReadablePath, ReadableFile, ReadableDir, WritablePath, WritableFile, WritableDir ) from autoclick.validations import ValidationError, validation class Comparison(Enum): LT = ("<", operator.lt) LE = ("<=", operator.le) GT = (">", operator.gt) GE = (">=", operator.ge) EQ = ("=", operator.eq) NE = ("!=", operator.ne) def __init__(self, symbol: str, fn: Callable): self.symbol = symbol self.fn = fn class Defined: """ Validate that some number of paramters are defined. Args: n: Number to compare against the number of defined parameters. cmp: Comparison. """ def __init__(self, n: int = 1, cmp: Comparison = Comparison.GE): self.n = n self.cmp = cmp def __call__(self, **kwargs): defined = len(tuple(filter(None, kwargs.values()))) if not self.cmp.fn(defined, self.n): raise ValidationError( f"Of the following parameters, the number defined must be " f"{self.cmp.symbol} {self.n}: {', '.join(kwargs.keys())}" ) def defined_ge(n: int): return Defined(n, Comparison.GE) class Mutex: def __init__(self, *groups: Union[int, Tuple[int, ...]], max_defined: int = 1): self.groups = groups self.max_defined = max_defined def __call__(self, **kwargs): args = list(kwargs.items()) groups = self.groups or range(len(args)) defined = [] for group in groups: if isinstance(group, int): group = [group] for i in group: if args[i][1] is not None: defined.append(group) break if len(defined) > self.max_defined: group_str = ",".join(f"({','.join(str(i) for i in g)})" for g in defined) raise ValidationError( f"Values specified for > {self.max_defined} mutually exclusive groups: " f"{group_str}" ) class SequenceLength: """ Validate that the length of a sequence is within certain bounds. Args: *lengths: either a single value, which specifies the exact length a sequence must have, or a min and max value. """ def __init__(self, *lengths): if len(lengths) == 1: self.minlen = self.maxlen = lengths[0] else: self.minlen, self.maxlen = lengths if self.minlen > self.maxlen: raise ValueError("'minlen' must be <= 'maxlen'") def __call__(self, param_name: str, value: Sequence): if len(value) < self.minlen: raise ValidationError( f"Parameter {param_name} must have at least {self.minlen} elements." ) if len(value) < self.maxlen: raise ValidationError( f"Parameter {param_name} can have at most {self.maxlen} elements." ) @validation(ReadablePath) def readable_path(param_name: str, value: pathlib.Path): if not value.exists(): raise ValidationError( f"Parameter {param_name} value {value} does not exist." ) @validation(ReadableFile, depends=(readable_path,)) def readable_file(param_name: str, value: pathlib.Path): if not value.is_file(): raise ValidationError( f"Parameter {param_name} value {value} is not a file." ) @validation(ReadableDir, depends=(readable_path,)) def readable_dir(param_name: str, value: pathlib.Path): if not value.is_dir(): raise ValidationError( f"Parameter {param_name} value {value} is not a directory." ) @validation(WritablePath) def writable_path(param_name: str, value: pathlib.Path): existing = value while not existing.exists(): if existing.parent: existing = existing.parent else: break if not os.access(existing, os.W_OK): raise ValidationError( f"Parameter {param_name} value {value} is not writable." ) @validation(WritableFile, depends=(writable_path,)) def writable_file(param_name: str, value: pathlib.Path): if value.exists() and not value.is_file(): raise ValidationError( f"Parameter {param_name} value {value} exists and is not a file." ) @validation(WritableDir, depends=(writable_path,)) def writable_dir(param_name: str, value: pathlib.Path): if value.exists() and not value.is_dir(): raise ValidationError( f"Parameter {param_name} value {value} exists and is not a directory." ) PK!H2IS/O*autoclick-0.6.0.dist-info/entry_points.txtN,-OLΎJ-/,I-!s2SK2 rqPK!u22!autoclick-0.6.0.dist-info/LICENSECopyright (c) 2017 John Didion Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.PK!HnHTUautoclick-0.6.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!Hj)+"autoclick-0.6.0.dist-info/METADATAZoFŬ KDP"gSi$n (r$MMqҲRy!:\]Q41AVIT'Yxߏ^'+9I]4WeLދE*)c+F Y2xFkGL<{unļ+VK]<v-ERJ*e;uU˪ZBUzzuK2;mxRY"K]zYԍ*QXسcBV;ZҌ2Mcןgyb+3+Jxrk#cWg߼?OR_1S;[ޖzQ&*UR,jH[跇t}U+Tc2{L#8V߽wrtJ1&ҤZ 颂Guu ú;l^lTQ*R̾Ն7b)Qu.MURy{(&u)`EXI,p%[N*)8өqP ªxp.ޢ L>0U9!~#u?&~5HdBM&L&:r2ٽ|e!ew眞l1-e~4~Ûo^(>|^|7ICB_ꍨ@zh3!UեHL$im*RXaCq)]f")5iRkX8]i.d?PR 2*9GEް4Xx췡?e3:EY^AhkPջ*{9zxe"#+hO(@wAW6Dya=|h=֐B &YuͶp2˥=ot)x0,%$8x/͑yX)(!%Rs]`d`+1ݸH/;$Kwzɕ.پX׻:L =ɢ4ݏp&'޼.Rɡq-9qjeGRgӍ,҄UỲ"O,e$S l,Z2fLJ4KtKi)i†qwOfɚ"kZJPL*K&T׈deKLYW}I:% vˇWڨYKRUqwXv7à58C5BUI|PC!7  ~nKVo؋n}w!{S[}G},)/=T,٘w uB uB̐D%,qCNRBBg7Mdÿk!܀Eu 8D~C !ylJ&?'$ fN<>JYg}ȝB4$  K/ɍz^URU?0'q 0aKOu%&ŖLģTO-:3  Cy5~lcAʴ79>ÖyR>ݑ1 <aymrgXv8\Tuh|!CEmnoao&mlYT,],3 u+%~3t>>&o@ɷZYR~|:X'w?𧃆X Rk: S~0YkƸ:0B+'oTrJJWI^K$nKHKA\a L!S%8zdVb@EO`bqu]ʵ,Vo'0.9GHbj򚺤Pڸ}hʹ&2JrޔQ ?d_8w-\lv =Ӆ)laF2q'Æ'DQt,ۤQ+ G$-4*&8ܫKݻ`A]{_t`_)]gZjnhDIjcֺsP=Ň>ɕDleKJ+TVSՕ<-P`MYh̀wq}׮wed̈@ `o%mPAv#D9ޞHW uD%]JT|A!E6ނLP.ڂ}E[2L8pwPT2ZxQ]4#`FLw)ߪ EtM:·ot%FOݼ%bl-L*7d7 }.ٵVŗ.hNc^Y&\J{؞?slAq t h dȨc} QŕJXHQ(iTv~ak|Y~js4Q5: lU$'ss)b ߺnlfZ i6W,dk1pG <;NVs"mExSN'&DuZcW9( )ܤ#7޺^d\< OT-/p nRN<"kc3XkqEDs6` rf?oܩ4r5+r:NU">3EIF6]%s>Ļ :+̹cmΡ?@ *\ze)>t *E#b%rnn$EMNɒnO~͔K&+~1uzuurf>'Ac=O]హg6Ky]ȸ.[k/^rdm^GĽy@q%A{K.5ۦ|ܼCmhKjk]kzvvo.?ZyRk&?RD ڃM3}kl >Mx ao}'6zlFR&N쵔>a N_Gyk |-ӻc‚Jz]D,rJ9/s ylȅ]>D%8 &DdPQj 5Ksę]C̓=fmEx}Np}qm &벟b1oD YY-' JX]@=wr= rd97dk4t. ek]A. &3>w)f0jG`T,ޏ/VO# 'mo3 q]8GtzĞ"VPQ?<;'3 ܓB6$mQeS-h,:`ҳ%:`r$1ן86o*d ѯ[/w?ɞnT[ԅQIX5~ʝ}>ˍ>pc_Oyы^ͬ'tx v~nE3|9Qź߲'K6Cpm%"e]O$ΔEyLֹLrzt4m%$AN }vdٵݖ2!g2i)WH$8at} Pn` _`( !"z 3i65iHT&+5ž)S8q?BsW|Go^%"߾W:OqBzxdW\ o׬M/.LX$| L-kr k/,[T`_`adFn /PK!H퍈U!` autoclick-0.6.0.dist-info/RECORD}ɒ@} â)(3THd(MuXQ'߽)8Tw& /[l^ӥ5y9_S3ž|@g_8Y 8qmS2M$ܓ>";^TcnjRMem$(/X-Kom5MJ]g@r  ay'\b81gy/hޔXFuE 9kI69Mxla'l1 1Hp,zQ|/;_l]2:)/Cǒ}~O(%}Cѿy3bN5Z0]'Z-9rWZ$axXGK:ڰt׈vje`N2'H2uEDوV`_ <\`]Ax/F:mr(y!֯/$R7j\- `9r)i+^i{y2}u{ Y~C,y#nj`DY5x4wCWqDKoa&IU {-* LR+aEr]_⮅hmҚ