Source code for repobee_plug.cli.args

"""Command line options for extension commands."""
import dataclasses
import enum
from typing import Optional, Any, Callable, Mapping, List, Tuple


[docs]class ArgumentType(enum.Enum): OPTION = "option" POSITIONAL = "positional" MUTEX_GROUP = "mutually_exclusive_group" FLAG = "flag"
@dataclasses.dataclass(frozen=True) class Option: short_name: Optional[str] = None long_name: Optional[str] = None configurable: Optional[bool] = None help: Optional[str] = None converter: Optional[Callable[[str], Any]] = None required: Optional[bool] = None default: Optional[Any] = None argument_type: ArgumentType = ArgumentType.OPTION argparse_kwargs: Optional[Mapping[str, Any]] = None @dataclasses.dataclass(frozen=True) class MutuallyExclusiveGroup: options: List[Tuple[str, Option]] required: bool = False def __post_init__(self): for name, opt in self.options: self._check_arg_type(name, opt) def _check_arg_type(self, name: str, opt: Option): allowed_types = (ArgumentType.OPTION, ArgumentType.FLAG) if opt.argument_type not in allowed_types: raise ValueError( f"{opt.argument_type.value} not allowed in mutex group" )
[docs]def is_cli_arg(obj: Any) -> bool: """Determine if an object is a CLI argument. Args: obj: An object. Returns: True if the object is an instance of a CLI argument class. """ return isinstance(obj, (Option, MutuallyExclusiveGroup))
[docs]def option( short_name: Optional[str] = None, long_name: Optional[str] = None, help: str = "", required: bool = False, default: Optional[Any] = None, configurable: bool = False, converter: Optional[Callable[[str], Any]] = None, argparse_kwargs: Optional[Mapping[str, Any]] = None, ) -> Option: """Create an option for a :py:class:`Command` or a :py:class:`CommandExtension`. Example usage: .. code-block:: python :caption: ext.py import repobee_plug as plug class Hello(plug.Plugin, plug.cli.Command): name = plug.cli.option(help="Your name.") age = plug.cli.option(converter=int, help="Your age.") def command(self): print( f"Hello, my name is {self.name} " f"and I am {self.age} years old" ) This command can then be called like so: .. code-block:: bash $ repobee -p ext.py hello --name Alice --age 22 Hello, my name is Alice and I am 22 years old Args: short_name: The short name of this option. Must start with ``-``. long_name: The long name of this option. Must start with `--`. help: A description of this option that is used in the CLI help section. required: Whether or not this option is required. default: A default value for this option. configurable: Whether or not this option is configurable. If an option is both configurable and required, having a value for the option in the configuration file makes the option non-required. converter: A converter function that takes a string and returns the argument in its proper state. Should also perform input validation and raise an error if the input is malformed. argparse_kwargs: Keyword arguments that are passed directly to :py:meth:`argparse.ArgumentParser.add_argument` Returns: A CLI argument wrapper used internally by RepoBee to create command line arguments. """ return Option( short_name=short_name, long_name=long_name, configurable=configurable, help=help, converter=converter, required=required, default=default, argument_type=ArgumentType.OPTION, argparse_kwargs=argparse_kwargs or {}, )
[docs]def positional( help: str = "", converter: Optional[Callable[[str], Any]] = None, argparse_kwargs: Optional[Mapping[str, Any]] = None, ) -> Option: """Create a positional argument for a :py:class:`Command` or a :py:class:`CommandExtension`. Example usage: .. code-block:: python :caption: ext.py import repobee_plug as plug class Hello(plug.Plugin, plug.cli.Command): name = plug.cli.positional(help="Your name.") age = plug.cli.positional(converter=int, help="Your age.") def command(self): print( f"Hello, my name is {self.name} " f"and I am {self.age} years old" ) This command can then be called like so: .. code-block:: bash $ repobee -p ext.py hello Alice 22 Hello, my name is Alice and I am 22 years old Args: help: The help section for the positional argument. converter: A converter function that takes a string and returns the argument in its proper state. Should also perform input validation and raise an error if the input is malformed. argparse_kwargs: Keyword arguments that are passed directly to :py:meth:`argparse.ArgumentParser.add_argument` Returns: A CLI argument wrapper used internally by RepoBee to create command line argument. """ return Option( help=help, converter=converter, argparse_kwargs=argparse_kwargs or {}, argument_type=ArgumentType.POSITIONAL, )
[docs]def flag( short_name: Optional[str] = None, long_name: Optional[str] = None, help: str = "", const: Any = True, default: Optional[Any] = None, ) -> Option: """Create a command line flag for a :py:class:`Command` or a :py:class`CommandExtension`. This is simply a convenience wrapper around :py:func:`option`. A flag is specified on the command line as ``--flag``, and causes a constant to be stored. If the flag is omitted, a default value is used instead. The default behavior is that specifying ``--flag`` stores the constant ``True``, and omitting it causes it to default to ``False``. It can also be used to store any other form of constant by specifying the ``const`` argument. If so, then omitting the flag will cause it to default to ``None`` instead of ``False``. Finally, the default value can also be overridden by specifying the ``default`` argument. Example: .. code-block:: python :caption: ext.py import repobee_plug as plug class Flags(plug.Plugin, plug.cli.Command): # a normal flag, which toggles between True and False is_great = plug.cli.flag() # a "reverse" flag which defaults to True instead of False not_great = plug.cli.flag(const=False, default=True) # a flag that stores a constant and defaults to None meaning = plug.cli.flag(const=42) # a flag that stores a constant and defaults to another constant approve = plug.cli.flag(const="yes", default="no") def command(self): print("is_great", self.is_great) print("not_great", self.not_great) print("meaning", self.meaning) print("approve", self.approve) We can then call this command (for example) like so: .. code-block:: bash $ repobee -p ext.py flags --meaning --not-great is_great False not_great False meaning 42 approve no Args: short_name: The short name of this option. Must start with ``-``. long_name: The long name of this option. Must start with `--`. help: A description of this option that is used in the CLI help section. const: The constant to store. default: The value to default to if the flag is omitted. Returns: A CLI argument wrapper used internally by RepoBee to create command line argument. """ resolved_default = ( not const if default is None and isinstance(const, bool) else default ) return Option( short_name=short_name, long_name=long_name, help=help, argparse_kwargs=dict( action="store_const", const=const, default=resolved_default ), argument_type=ArgumentType.FLAG, )
[docs]def mutually_exclusive_group(*, __required__: bool = False, **kwargs): """ Args: __required__: Whether or not this mutex group is required. kwargs: Keyword arguments on the form ``name=plug.cli.option()``. """ options = [(key, value) for key, value in kwargs.items()] return MutuallyExclusiveGroup(required=__required__, options=options)