pax_global_header00006660000000000000000000000064136677657430014542gustar00rootroot0000000000000052 comment=1432ffc333dd1afc209b4115475359729e2a477a clikit-0.6.2/000077500000000000000000000000001366776574300130265ustar00rootroot00000000000000clikit-0.6.2/.github/000077500000000000000000000000001366776574300143665ustar00rootroot00000000000000clikit-0.6.2/.github/workflows/000077500000000000000000000000001366776574300164235ustar00rootroot00000000000000clikit-0.6.2/.github/workflows/push.yml000066400000000000000000000051751366776574300201350ustar00rootroot00000000000000name: Tests on: [push, pull_request] jobs: Linting: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python 3.8 uses: actions/setup-python@v1 with: python-version: 3.8 - name: Linting run: | pip install pre-commit pre-commit run --all-files Linux: needs: Linting runs-on: ubuntu-latest strategy: matrix: python-version: [2.7, 3.5, 3.6, 3.7, 3.8] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install Poetry run: | curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py python get-poetry.py --preview -y source $HOME/.poetry/env - name: Install dependencies run: | source $HOME/.poetry/env poetry install - name: Test run: | source $HOME/.poetry/env poetry run pytest -q tests MacOS: needs: Linting runs-on: macos-latest strategy: matrix: python-version: [2.7, 3.5, 3.6, 3.7, 3.8] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install Poetry run: | curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py python get-poetry.py --preview -y source $HOME/.poetry/env - name: Install dependencies run: | source $HOME/.poetry/env poetry install - name: Test run: | source $HOME/.poetry/env poetry run pytest -q tests Windows: needs: Linting runs-on: windows-latest strategy: matrix: python-version: [2.7, 3.5, 3.6, 3.7, 3.8] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install Poetry run: | Invoke-WebRequest https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py -O get-poetry.py python get-poetry.py --preview -y $env:Path += ";$env:Userprofile\.poetry\bin" - name: Install dependencies run: | $env:Path += ";$env:Userprofile\.poetry\bin" poetry install - name: Test run: | $env:Path += ";$env:Userprofile\.poetry\bin" poetry run pytest -q tests clikit-0.6.2/.gitignore000066400000000000000000000004211366776574300150130ustar00rootroot00000000000000*.pyc # Packages *.egg *.egg-info dist build .cache # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml .DS_Store .idea/* .vscode *.code-workspace .python-version test.py /test poetry.lock .pytest_cache pip-wheel-metadata setup.py clikit-0.6.2/.pre-commit-config.yaml000066400000000000000000000001721366776574300173070ustar00rootroot00000000000000repos: - repo: https://github.com/ambv/black rev: stable hooks: - id: black python_version: python3.7 clikit-0.6.2/CHANGELOG.md000066400000000000000000000113731366776574300146440ustar00rootroot00000000000000# Change Log ## [0.6.2] - 2020-06-09 ### Fixed - Fixed an error in the package's metadata causing errors on Python 3.5. ## [0.6.1] - 2020-05-31 ### Changed - Progress bars will now update at most every 100ms by default. This is configurable via the `min_seconds_between_redraws()` method ([#29](https://github.com/sdispater/clikit/pull/29)). - Progress bars and indicators now accept an `Output` instance as well as an `IO` instance. If an `IO` instance is passed the error output will be used ([#29](https://github.com/sdispater/clikit/pull/29)). - Slightly changed the exception trace rendering ([#30](https://github.com/sdispater/clikit/pull/30)). ### Fixed - Fixed an error where choices questions accepted negative choices ([#27](https://github.com/sdispater/clikit/pull/27)). ## [0.6.0] - 2020-04-17 ### Added - Support for error solutions ([#24](https://github.com/sdispater/clikit/pull/24)). - Ability to ignore files in the stack trace ([#24](https://github.com/sdispater/clikit/pull/24)). ### Changed - The stack trace will now be displayed above the actual error, so that the error is visible immediately and the read flow of the stack trace is more natural ([#24](https://github.com/sdispater/clikit/pull/24)). ### Fixed - Fixed the coloring of the code snippets of the stack trace for tokens that span multiple lines ([#24](https://github.com/sdispater/clikit/pull/24)). ## [0.5.1] - 2020-03-27 ### Fixed - Improved the error message display for multiline messages ([#21](https://github.com/sdispater/clikit/pull/21)). ## [0.5.0] - 2020-03-26 ### Added - Errors are now rendered in a nicer way for Python 3.6+ ([#19](https://github.com/sdispater/clikit/pull/19)). ## [0.4.3] - 2020-03-20 ### Fixed - Fixed encoding errors in questions for Python 2.7. ## [0.4.2] - 2020-02-28 ### Fixed - Fixed the terminal width being set to 0 in some circumstances ([#15](https://github.com/sdispater/clikit/pull/15)). - Fixed the comptibility with the latest version of [pastel](https://github.com/sdispater/pastel) ([#10](https://github.com/sdispater/clikit/pull/10)). ## [0.4.1] - 2019-12-06 ### Fixed - Fixed the rendering of exception traces on Python 2.7 ## [0.4.0] - 2019-10-25 ### Changed - Changed the way event names are stored and exposed. ### Fixed - Fixed parsing of options after a `--` token. ## [0.3.2] - 2019-09-20 ### Fixed - Fixed handling of `KeyboardInterrupt` exceptions. ## [0.3.1] - 2019-06-24 ### Fixed - Fixed hidden command being displayed. ## [0.3.0] - 2019-06-24 ### Added - Added support for displaying multiple, independent progress bars. ### Fixed - Fixed similar command names suggestions. - Fixed the `help` command not displaying the help text of commands. ## [0.2.4] - 2019-05-11 ### Fixed - Fixed `help` command not displaying help for sub commands. - Fixed possible errors for raised exceptions with a non-int `code` attribute. ## [0.2.3] - 2018-12-10 ### Fixed - Fixed handling of ANSI support detection in output. ## [0.2.2] - 2018-12-08 ### Changed - Write line methods will now always write `\n` instead of `os.linesep`. ## [0.2.1] - 2018-12-07 ### Changed - The `help` command will now insert the script name and command name where needed. ### Fixed - Fixed handling of paragraph in help. ## [0.2.0] - 2018-12-06 ### Added - Added a basic event system. - Added a `NullIO` class for no-op IO operations. - Added a progress bar component. - Added a `hidden` property on command configurations. ### Fixed - Fixed help display for multi valued options. - Fixed the progress indicator component. [Unreleased]: https://github.com/sdispater/tomlkit/compare/0.6.2...master [0.6.2]: https://github.com/sdispater/tomlkit/releases/tag/0.6.2 [0.6.1]: https://github.com/sdispater/tomlkit/releases/tag/0.6.1 [0.6.0]: https://github.com/sdispater/tomlkit/releases/tag/0.6.0 [0.5.1]: https://github.com/sdispater/tomlkit/releases/tag/0.5.1 [0.5.0]: https://github.com/sdispater/tomlkit/releases/tag/0.5.0 [0.4.3]: https://github.com/sdispater/tomlkit/releases/tag/0.4.3 [0.4.2]: https://github.com/sdispater/tomlkit/releases/tag/0.4.2 [0.4.1]: https://github.com/sdispater/tomlkit/releases/tag/0.4.1 [0.4.0]: https://github.com/sdispater/tomlkit/releases/tag/0.4.0 [0.3.2]: https://github.com/sdispater/tomlkit/releases/tag/0.3.2 [0.3.1]: https://github.com/sdispater/tomlkit/releases/tag/0.3.1 [0.3.0]: https://github.com/sdispater/tomlkit/releases/tag/0.3.0 [0.2.4]: https://github.com/sdispater/tomlkit/releases/tag/0.2.4 [0.2.3]: https://github.com/sdispater/tomlkit/releases/tag/0.2.3 [0.2.2]: https://github.com/sdispater/tomlkit/releases/tag/0.2.2 [0.2.1]: https://github.com/sdispater/tomlkit/releases/tag/0.2.1 [0.2.0]: https://github.com/sdispater/tomlkit/releases/tag/0.2.0 [0.1.0]: https://github.com/sdispater/tomlkit/releases/tag/0.1.0 clikit-0.6.2/LICENSE000066400000000000000000000020461366776574300140350ustar00rootroot00000000000000Copyright (c) 2018 Sébastien Eustace 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. clikit-0.6.2/README.md000066400000000000000000000002451366776574300143060ustar00rootroot00000000000000# CliKit CliKit is a group of utilities to build beautiful and testable command line interfaces. This is at the core of [Cleo](https://github.com/sdispater/cleo). clikit-0.6.2/pyproject.toml000066400000000000000000000024321366776574300157430ustar00rootroot00000000000000[tool.poetry] name = "clikit" version = "0.6.2" description = "CliKit is a group of utilities to build beautiful and testable command line interfaces." authors = ["Sébastien Eustace "] license = "MIT" readme = "README.md" repository = "https://github.com/sdispater/clikit" keywords = ["packaging", "dependency", "poetry"] packages = [ {include = "clikit", from = "src"}, # This trips up pip when installing in editable mode # so until it's fixed in Poetry we have to comment # {include = "tests", format = "sdist"} ] [tool.poetry.dependencies] python = "~2.7 || ^3.4" pastel = "^0.2.0" pylev = "^1.3" # Crashtest is only needed for Python ^3.6 to provide # better error messsages crashtest = { version = "^0.3.0", python = "^3.6" } # The typing module is not in the stdlib in Python 2.7 and 3.4 typing = { version = "^3.6", python = "~2.7 || ~3.4" } typing-extensions = { version = "^3.6", markers = 'python_version >= "3.5" and python_full_version < "3.5.4"' } # enum34 is needed for Python 2.7 enum34 = { version = "^1.1", python = "~2.7" } [tool.poetry.dev-dependencies] pytest = "^4.0" pytest-cov = "^2.6" tox = "^3.5" pre-commit = "^1.12" pytest-mock = "^2.0.0" [build-system] requires = ["poetry_core>=1.0.0a5"] build-backend = "poetry.core.masonry.api" clikit-0.6.2/src/000077500000000000000000000000001366776574300136155ustar00rootroot00000000000000clikit-0.6.2/src/clikit/000077500000000000000000000000001366776574300150745ustar00rootroot00000000000000clikit-0.6.2/src/clikit/__init__.py000066400000000000000000000003201366776574300172000ustar00rootroot00000000000000from .api.config.application_config import ApplicationConfig from .config.default_application_config import DefaultApplicationConfig from .console_application import ConsoleApplication __version__ = "0.6.2" clikit-0.6.2/src/clikit/adapter/000077500000000000000000000000001366776574300165145ustar00rootroot00000000000000clikit-0.6.2/src/clikit/adapter/__init__.py000066400000000000000000000000001366776574300206130ustar00rootroot00000000000000clikit-0.6.2/src/clikit/adapter/style_converter.py000066400000000000000000000015341366776574300223200ustar00rootroot00000000000000from pastel.style import Style as PastelStyle from clikit.api.formatter import Style class StyleConverter(object): """ Converts a TTY Style instance to a Pastel Style instance. """ @classmethod def convert(cls, style): # type: (Style) -> PastelStyle options = [] if style.is_bold(): options.append("bold") if style.is_italic(): options.append("italic") if style.is_dark(): options.append("dark") if style.is_underlined(): options.append("underline") if style.is_blinking(): options.append("blink") if style.is_inverse(): options.append("reverse") if style.is_hidden(): options.append("conceal") return PastelStyle(style.foreground_color, style.background_color, options) clikit-0.6.2/src/clikit/api/000077500000000000000000000000001366776574300156455ustar00rootroot00000000000000clikit-0.6.2/src/clikit/api/__init__.py000066400000000000000000000000001366776574300177440ustar00rootroot00000000000000clikit-0.6.2/src/clikit/api/application/000077500000000000000000000000001366776574300201505ustar00rootroot00000000000000clikit-0.6.2/src/clikit/api/application/__init__.py000066400000000000000000000000451366776574300222600ustar00rootroot00000000000000from .application import Application clikit-0.6.2/src/clikit/api/application/application.py000066400000000000000000000033571366776574300230350ustar00rootroot00000000000000from clikit.api.args.format.args_format import ArgsFormat from clikit.api.args.raw_args import RawArgs from clikit.api.command.command_collection import CommandCollection from clikit.api.config.application_config import ApplicationConfig from clikit.api.io import InputStream from clikit.api.io import OutputStream from clikit.api.resolver.resolved_command import ResolvedCommand class Application: """ A console application. """ @property def config(self): # type: () -> ApplicationConfig raise NotImplementedError() @property def global_args_format(self): # type: () -> ArgsFormat raise NotImplementedError() def get_command(self, name): # type: (str) -> Command raise NotImplementedError() @property def commands(self): # type: () -> CommandCollection raise NotImplementedError() def has_command(self, name): # type: (str) -> bool raise NotImplementedError() def has_commands(self): # type: () -> bool raise NotImplementedError() @property def named_commands(self): # type: () -> CommandCollection raise NotImplementedError() def has_named_commands(self): # type: () -> bool raise NotImplementedError() @property def default_commands(self): # type: () -> CommandCollection raise NotImplementedError() def has_default_commands(self): # type: () -> bool raise NotImplementedError() def resolve_command(self, args): # type: (RawArgs) -> ResolvedCommand raise NotImplementedError() def run( self, args=None, input_stream=None, output_stream=None, error_stream=None ): # type: (RawArgs, InputStream, OutputStream, OutputStream) -> int raise NotImplementedError() clikit-0.6.2/src/clikit/api/args/000077500000000000000000000000001366776574300166015ustar00rootroot00000000000000clikit-0.6.2/src/clikit/api/args/__init__.py000066400000000000000000000001311366776574300207050ustar00rootroot00000000000000from .args import Args from .args_parser import ArgsParser from .raw_args import RawArgs clikit-0.6.2/src/clikit/api/args/args.py000066400000000000000000000073751366776574300201230ustar00rootroot00000000000000from typing import Any from typing import Dict from typing import Optional from typing import Union from .format.args_format import ArgsFormat from .raw_args import RawArgs class Args(object): """ The parsed console arguments. """ def __init__(self, fmt, raw_args=None): # type: (ArgsFormat, RawArgs) -> None self._fmt = fmt self._raw_args = raw_args self._options = {} self._arguments = {} @property def format(self): # type: () -> ArgsFormat return self._fmt @property def raw_args(self): # type: () -> RawArgs return self._raw_args @property def script_name(self): # type: () -> Optional[str] if self._raw_args: return self._raw_args.script_name @property def command_names(self): return self._fmt.get_command_names() @property def command_options(self): return self._fmt.get_command_options() def option(self, name): option = self._fmt.get_option(name) if option.long_name in self._options: return self._options[option.long_name] if option.accepts_value(): return option.default return False def options(self, include_defaults=True): options = self._options.copy() if include_defaults: for option in self._fmt.get_options().values(): name = option.long_name if not name in options: default = False if option.accepts_value(): default = option.default options[name] = default return options def set_option(self, name, value=True): option = self._fmt.get_option(name) if option.is_multi_valued(): if not isinstance(value, list): value = [value] for i, v in enumerate(value): value[i] = option.parse(v) elif option.accepts_value(): value = option.parse(value) elif value is False: if option.long_name in self._options: del self._options[option.long_name] return self else: value = True self._options[option.long_name] = value return self def is_option_set(self, name): # type: (str) -> bool return name in self._options def is_option_defined(self, name): # type: (str) -> bool return self._fmt.has_option(name) def argument(self, name): # type: (Union[str, int]) -> Any argument = self._fmt.get_argument(name) if argument.name in self._arguments: return self._arguments[name] return argument.default def arguments(self, include_defaults=True): # type: (bool) -> Dict[str, Any] arguments = {} for argument in self._fmt.get_arguments().values(): name = argument.name if name in self._arguments: arguments[name] = self._arguments[name] elif include_defaults: arguments[name] = argument.default return arguments def set_argument(self, name, value): # type: (Union[str, int], Any) -> Args argument = self._fmt.get_argument(name) if argument.is_multi_valued(): if not isinstance(value, list): value = [value] for i, v in enumerate(value): value[i] = argument.parse(v) else: value = argument.parse(value) self._arguments[argument.name] = value return self def is_argument_set(self, name): # type: (Union[str, int]) -> bool return name in self._arguments def is_argument_defined(self, name): # type: (Union[str, int]) -> bool return self._fmt.has_argument(name) clikit-0.6.2/src/clikit/api/args/args_parser.py000066400000000000000000000005361366776574300214670ustar00rootroot00000000000000from .args import Args from .format.args_format import ArgsFormat from .raw_args import RawArgs class ArgsParser(object): """ Parses raw console arguments and returns the parsed arguments. """ def parse( self, args, fmt, lenient=False ): # type: (RawArgs, ArgsFormat, bool) -> Args raise NotImplementedError() clikit-0.6.2/src/clikit/api/args/exceptions.py000066400000000000000000000037171366776574300213440ustar00rootroot00000000000000from clikit.api.exceptions import CliKitException class CannotAddOptionException(RuntimeError): @classmethod def already_exists(cls, name): return cls( 'An option named "{}{}" exists already.'.format( "--" if len(name) > 1 else "-", name ) ) class NoSuchOptionException(RuntimeError): def __init__(self, name): message = 'The "{}{}" option does not exist.'.format( "--" if len(name) > 1 else "-", name ) super(NoSuchOptionException, self).__init__(message) class CannotAddArgumentException(RuntimeError): @classmethod def already_exists(cls, name): return cls('An argument named "{}" exists already.'.format(name)) @classmethod def cannot_add_after_multi_valued(cls): return cls("Cannot add an argument after a multi-valued argument.") @classmethod def cannot_add_required_after_optional(cls): return cls("Cannot add a required argument after an optional one.") class NoSuchArgumentException(RuntimeError): def __init__(self, name): if isinstance(name, int): message = "The argument at position {} does not exist.".format(name) else: message = 'The "{}" argument does not exist.'.format(name) super(NoSuchArgumentException, self).__init__(message) class CannotParseArgsException(RuntimeError, CliKitException): @classmethod def too_many_arguments(cls): return cls("Too many arguments.") @classmethod def option_does_not_accept_value(cls, name): if len(name) > 1: name = "--" + name else: name = "--" + name return cls('The "{}" option does not accept a value.'.format(name)) @classmethod def option_requires_value(cls, name): if len(name) > 1: name = "--" + name else: name = "--" + name return cls('The "{}" option requires a value.'.format(name)) clikit-0.6.2/src/clikit/api/args/format/000077500000000000000000000000001366776574300200715ustar00rootroot00000000000000clikit-0.6.2/src/clikit/api/args/format/__init__.py000066400000000000000000000003411366776574300222000ustar00rootroot00000000000000from .args_format import ArgsFormat from .args_format_builder import ArgsFormatBuilder from .argument import Argument from .command_name import CommandName from .command_option import CommandOption from .option import Option clikit-0.6.2/src/clikit/api/args/format/abstract_option.py000066400000000000000000000104101366776574300236320ustar00rootroot00000000000000import re from typing import Optional from clikit.utils._compat import basestring class AbstractOption(object): """ Base class for command line options. """ PREFER_LONG_NAME = 1 PREFER_SHORT_NAME = 2 def __init__( self, long_name, short_name=None, flags=0, description=None ): # type: (str, Optional[str], int, Optional[str]) -> None long_name = self._remove_double_dash_prefix(long_name) short_name = self._remove_dash_prefix(short_name) self._validate_flags(flags) self._validate_long_name(long_name) self._validate_short_name(short_name, flags) self._long_name = long_name self._short_name = short_name self._description = description flags = self._add_default_flags(flags) self._flags = flags @property def long_name(self): # type: () -> str return self._long_name @property def short_name(self): # type: () -> Optional[str] return self._short_name @property def flags(self): # type: () -> int return self._flags @property def description(self): # type: () -> Optional[str] return self._description def is_long_name_preferred(self): # type: () -> bool return bool(self._flags & self.PREFER_LONG_NAME) def is_short_name_preferred(self): # type: () -> bool return bool(self._flags & self.PREFER_SHORT_NAME) def _remove_double_dash_prefix(self, string): # type: (str) -> str if not isinstance(string, basestring): return string if string.startswith("--"): string = string[2:] return string def _remove_dash_prefix(self, string): # type: (Optional[str]) -> Optional[str] if string is None: return string if not isinstance(string, basestring): return string if string.startswith("-"): string = string[1:] return string def _validate_flags(self, flags): # type: (int) -> None if flags & self.PREFER_SHORT_NAME and flags & self.PREFER_LONG_NAME: raise ValueError( "The option flags PREFER_SHORT_NAME and PREFER_LONG_NAME cannot be combined." ) def _validate_long_name(self, long_name): # type: (Optional[str]) -> None if long_name is None: raise ValueError("The long option name must not be null.") if not isinstance(long_name, basestring): raise ValueError( "The long option name must be a string. Got: {}".format(type(long_name)) ) if not long_name: raise ValueError("The long option name must not be empty.") if len(long_name) < 2: raise ValueError( 'The long option name must contain more than one character. Got: "{}"'.format( len(long_name) ) ) if not long_name[:1].isalpha(): raise ValueError("The long option name must start with a letter") if not re.match(r"^[a-zA-Z0-9\-]+$", long_name): raise ValueError( "The long option name must contain letters, digits and hyphens only." ) def _validate_short_name( self, short_name, flags ): # type: (Optional[str], int) -> None if short_name is None: if flags & self.PREFER_SHORT_NAME: raise ValueError( "The short option name must be given if the option flag PREFER_SHORT_NAME is selected." ) return if not isinstance(short_name, basestring): raise ValueError( "The short option name must be a string. Got: {}".format( type(short_name) ) ) if not short_name: raise ValueError("The short option name must not be empty.") if not re.match(r"^[a-zA-Z]$", short_name): raise ValueError("The short option name must be exactly one letter.") def _add_default_flags(self, flags): # type: (int) -> int if not flags & (self.PREFER_LONG_NAME | self.PREFER_SHORT_NAME): flags |= ( self.PREFER_SHORT_NAME if self._short_name else self.PREFER_LONG_NAME ) return flags clikit-0.6.2/src/clikit/api/args/format/args_format.py000066400000000000000000000202631366776574300227520ustar00rootroot00000000000000from typing import Any from typing import Dict from typing import List from typing import Optional from typing import Union from ..exceptions import NoSuchArgumentException from ..exceptions import NoSuchOptionException from .argument import Argument from .option import Option from .command_name import CommandName from .command_option import CommandOption class ArgsFormat(object): """ The format used to parse a RawArgs instance. """ def __init__( self, elements=None, base_format=None ): # type: (Optional[Union[List[Any], ArgsFormatBuilder]], Optional[ArgsFormat]) from .args_format_builder import ArgsFormatBuilder if elements is None: elements = [] if isinstance(elements, ArgsFormatBuilder): builder = elements else: builder = self._create_builder_for_elements(elements) if base_format is None: base_format = builder.base_format self._base_format = base_format self._command_names = builder.get_command_names(False) self._command_options = {} self._command_options_by_short_name = {} self._arguments = builder.get_arguments(False) self._options = builder.get_options(False) self._options_by_short_name = {} self._has_multi_valued_arg = builder.has_multi_valued_argument(False) self._hash_optional_arg = builder.has_optional_argument(False) for option in self._options.values(): if option.short_name: self._options_by_short_name[option.short_name] = option for command_option in builder.get_command_options(): self._command_options[command_option.long_name] = command_option if command_option.short_name: self._command_options_by_short_name[ command_option.short_name ] = command_option for long_alias in command_option.long_aliases: self._command_options[long_alias] = command_option for short_alias in command_option.short_aliases: self._command_options_by_short_name[short_alias] = command_option @property def base_format(self): # type: () -> ArgsFormat return self._base_format def has_command_names(self, include_base=True): # type: (bool) -> bool if self._command_names: return True if include_base and self._base_format: return self._base_format.has_command_names() return False def get_command_names(self, include_base=True): # type: (bool) -> List[CommandName] command_names = self._command_names if include_base and self._base_format: command_names = self._base_format.get_command_names() + command_names return command_names def has_command_option(self, name, include_base=True): # type: (str, bool) -> bool if name in self._command_options or name in self._command_options_by_short_name: return True if include_base and self._base_format: return self._base_format.has_command_option(name) return False def has_command_options(self, include_base=True): # type: (bool) -> bool if self._command_options: return True if include_base and self._base_format: return self._base_format.has_command_options() return False def get_command_option( self, name, include_base=True ): # type: (str, bool) -> CommandOption if name in self._command_options: return self._command_options[name] if name in self._command_options_by_short_name: return self._command_options_by_short_name[name] if include_base and self._base_format: return self._base_format.get_command_option(name) raise NoSuchOptionException(name) def get_command_options( self, include_base=True ): # type: (bool) -> List[CommandOption] command_options = list(self._command_options.values()) if include_base and self._base_format: command_options += self._base_format.get_command_options() return command_options def has_argument( self, name, include_base=True ): # type: (Union[str, int], bool) -> bool arguments = self.get_arguments(include_base) if isinstance(name, int): return name < len(arguments) return name in arguments def has_multi_valued_argument(self, include_base=True): # type: (bool) -> bool if self._has_multi_valued_arg: return True if include_base and self._base_format: return self._base_format.has_multi_valued_argument() return False def has_optional_argument(self, include_base=True): # type: (bool) -> bool if self._hash_optional_arg: return True if include_base and self._base_format: return self._base_format.has_optional_argument() return False def has_required_argument(self, include_base=True): # type: (bool) -> bool if not self._hash_optional_arg and self._arguments: return True if include_base and self._base_format: return self._base_format.has_required_argument() return False def has_arguments(self, include_base=True): # type: (bool) -> bool if self._arguments: return True if include_base and self._base_format: return self._base_format.has_arguments() return False def get_argument( self, name, include_base=True ): # type: (Union[str, int], bool) -> Argument if isinstance(name, int): arguments = list(self.get_arguments(include_base).values()) if name >= len(arguments): raise NoSuchArgumentException(name) else: arguments = self.get_arguments(include_base) if name not in arguments: raise NoSuchArgumentException(name) return arguments[name] def get_arguments(self, include_base=True): # type: (bool) -> Dict[str, Argument] arguments = self._arguments.copy() if include_base and self._base_format: base_arguments = self._base_format.get_arguments() base_arguments.update(arguments) arguments = base_arguments return arguments def has_option(self, name, include_base=True): # type: (str, bool) -> bool if name in self._options or name in self._options_by_short_name: return True if include_base and self._base_format: return self._base_format.has_option(name) return False def has_options(self, include_base=True): # type: (bool) -> bool if self._options: return True if include_base and self._base_format: return self._base_format.has_options() return False def get_option(self, name, include_base=True): # type: (str, bool) -> Option if name in self._options: return self._options[name] if name in self._options_by_short_name: return self._options_by_short_name[name] if include_base and self._base_format: return self._base_format.get_option(name) raise NoSuchOptionException(name) def get_options(self, include_base=True): # type: (bool) -> Dict[str, Option] options = self._options.copy() if include_base and self._base_format: options.update(self._base_format.get_options()) return options def _create_builder_for_elements( self, elements, base_format=None ): # type: (List[Any], Optional[ArgsFormat]) -> ArgsFormatBuilder from .args_format_builder import ArgsFormatBuilder builder = ArgsFormatBuilder(base_format) for element in elements: if isinstance(element, CommandName): builder.add_command_name(element) elif isinstance(element, CommandOption): builder.add_command_option(element) elif isinstance(element, Option): builder.add_option(element) elif isinstance(element, Argument): builder.add_argument(element) return builder clikit-0.6.2/src/clikit/api/args/format/args_format_builder.py000066400000000000000000000260541366776574300244640ustar00rootroot00000000000000from typing import Dict from typing import Iterable from typing import List from typing import Optional from typing import Tuple from typing import Union from clikit.utils._compat import OrderedDict from ..exceptions import CannotAddArgumentException from ..exceptions import CannotAddOptionException from ..exceptions import NoSuchArgumentException from ..exceptions import NoSuchOptionException from .args_format import ArgsFormat from .argument import Argument from .option import Option from .command_name import CommandName from .command_option import CommandOption class ArgsFormatBuilder(object): """ A builder for ArgsFormat instances. """ def __init__(self, base_format=None): # type: (Optional[ArgsFormat]) -> None self._base_format = base_format self._command_names = [] self._command_options = OrderedDict() self._command_options_by_short_name = OrderedDict() self._arguments = OrderedDict() self._options = OrderedDict() self._options_by_short_name = OrderedDict() self._has_multi_valued_arg = False self._hash_optional_arg = False @property def base_format(self): # type: () -> Optional[ArgsFormat] return self._base_format def set_command_names( self, *command_names ): # type: (Tuple[CommandName]) -> ArgsFormatBuilder self._command_names = [] self.add_command_names(*command_names) return self def add_command_names( self, *command_names ): # type: (Tuple[CommandName]) -> ArgsFormatBuilder for command_name in command_names: self.add_command_name(command_name) return self def add_command_name( self, command_name ): # type: (CommandName) -> ArgsFormatBuilder self._command_names.append(command_name) return self def has_command_names(self, include_base=True): # type: (bool) -> bool if self._command_names: return True if include_base and self._base_format: return self._base_format.has_command_names() return False def get_command_names(self, include_base=True): # type: (bool) -> List[CommandName] command_names = self._command_names if include_base and self._base_format: command_names = self._base_format.get_command_names() + command_names return command_names def set_command_options( self, *command_options ): # type: (Tuple[CommandOption]) -> ArgsFormatBuilder self._command_options = {} self._command_options_by_short_name = {} self.add_command_options(*command_options) def add_command_options( self, *command_options ): # type: (Tuple[CommandOption]) -> ArgsFormatBuilder for command_option in command_options: self.add_command_option(command_option) def add_command_option( self, command_option ): # type: (CommandOption) -> ArgsFormatBuilder long_name = command_option.long_name short_name = command_option.short_name long_aliases = command_option.long_aliases short_aliases = command_option.short_aliases if self.has_option(long_name) or self.has_command_option(long_name): raise CannotAddOptionException.already_exists(long_name) for long_alias in long_aliases: if self.has_option(long_alias) or self.has_command_option(long_alias): raise CannotAddOptionException.already_exists(long_alias) if self.has_option(short_name) or self.has_command_option(short_name): raise CannotAddOptionException.already_exists(short_name) for short_alias in short_aliases: if self.has_option(short_alias) or self.has_command_option(short_alias): raise CannotAddOptionException.already_exists(short_alias) self._command_options[long_name] = command_option if short_name: self._command_options_by_short_name[short_name] = command_option for long_alias in long_aliases: self._command_options[long_alias] = command_option for short_alias in short_aliases: self._command_options_by_short_name[short_alias] = command_option return self def has_command_option(self, name, include_base=True): # type: (str, bool) -> bool if name in self._command_options or name in self._command_options_by_short_name: return True if include_base and self._base_format: return self._base_format.has_command_option(name) return False def has_command_options(self, include_base=True): # type: (bool) -> bool if self._command_options: return True if include_base and self._base_format: return self._base_format.has_command_options() return False def get_command_option( self, name, include_base=True ): # type: (str, bool) -> CommandOption if name in self._command_options: return self._command_options[name] if name in self._command_options_by_short_name: return self._command_options_by_short_name[name] if include_base and self._base_format: return self._base_format.get_command_option(name) raise NoSuchOptionException(name) def get_command_options( self, include_base=True ): # type: (bool) -> Iterable[CommandOption] command_options = list(self._command_options.values()) if include_base and self._base_format: command_options += self._base_format.get_command_options() return command_options def set_arguments( self, *arguments ): # type: (Iterable[Argument]) -> ArgsFormatBuilder self._arguments = {} self._has_multi_valued_arg = False self._hash_optional_arg = False self.add_arguments(*arguments) return self def add_arguments( self, *arguments ): # type: (Iterable[Argument]) -> ArgsFormatBuilder for argument in arguments: self.add_argument(argument) return self def add_argument(self, argument): # type: (Argument) -> ArgsFormatBuilder name = argument.name if self.has_argument(name): raise CannotAddArgumentException.already_exists(name) if self.has_multi_valued_argument(): raise CannotAddArgumentException.cannot_add_after_multi_valued() if argument.is_required() and self.has_optional_argument(): raise CannotAddArgumentException.cannot_add_required_after_optional() if argument.is_multi_valued(): self._has_multi_valued_arg = True if argument.is_optional(): self._hash_optional_arg = True self._arguments[name] = argument return self def has_argument( self, name, include_base=True ): # type: (Union[str, int], bool) -> bool arguments = self.get_arguments(include_base) if isinstance(name, int): return name < len(arguments) return name in arguments def has_multi_valued_argument(self, include_base=True): # type: (bool) -> bool if self._has_multi_valued_arg: return True if include_base and self._base_format: return self._base_format.has_multi_valued_argument() return False def has_optional_argument(self, include_base=True): # type: (bool) -> bool if self._hash_optional_arg: return True if include_base and self._base_format: return self._base_format.has_optional_argument() return False def has_required_argument(self, include_base=True): # type: (bool) -> bool if not self._hash_optional_arg and self._arguments: return True if include_base and self._base_format: return self._base_format.has_required_argument() return False def has_arguments(self, include_base=True): # type: (bool) -> bool if self._arguments: return True if include_base and self._base_format: return self._base_format.has_arguments() return False def get_argument( self, name, include_base=True ): # type: (Union[str, int], bool) -> Argument if isinstance(name, int): arguments = list(self.get_arguments(include_base).values()) if name >= len(arguments): raise NoSuchArgumentException(name) else: arguments = self.get_arguments(include_base) if name not in arguments: raise NoSuchArgumentException(name) return arguments[name] def get_arguments(self, include_base=True): # type: (bool) -> Dict[str, Argument] arguments = self._arguments.copy() if include_base and self._base_format: arguments.update(self._base_format.get_arguments()) return arguments def set_options(self, *options): # type: (Iterable[Option]) -> ArgsFormatBuilder self._options = {} self._options_by_short_name = {} self.add_options(*options) def add_options(self, *options): # type: (Iterable[Option]) -> ArgsFormatBuilder for option in options: self.add_option(option) def add_option(self, option): # type: (Option) -> ArgsFormatBuilder long_name = option.long_name short_name = option.short_name if self.has_option(long_name) or self.has_command_option(long_name): raise CannotAddOptionException.already_exists(long_name) if self.has_option(short_name) or self.has_command_option(short_name): raise CannotAddOptionException.already_exists(short_name) self._options[long_name] = option if short_name: self._options_by_short_name[short_name] = option return self def has_option(self, name, include_base=True): # type: (str, bool) -> bool if name in self._options or name in self._options_by_short_name: return True if include_base and self._base_format: return self._base_format.has_option(name) return False def has_options(self, include_base=True): # type: (bool) -> bool if self._options: return True if include_base and self._base_format: return self._base_format.has_options() return False def get_option(self, name, include_base=True): # type: (str, bool) -> Option if name in self._options: return self._options[name] if name in self._options_by_short_name: return self._options_by_short_name[name] if include_base and self._base_format: return self._base_format.get_option(name) raise NoSuchOptionException(name) def get_options(self, include_base=True): # type: (bool) -> Dict[str, Option] options = self._options.copy() if include_base and self._base_format: options.update(self._base_format.get_options()) return options @property def format(self): # type: () -> ArgsFormat return ArgsFormat(self, self._base_format) clikit-0.6.2/src/clikit/api/args/format/argument.py000066400000000000000000000121461366776574300222710ustar00rootroot00000000000000import re from typing import Any from typing import Optional from clikit.utils._compat import basestring from clikit.utils.string import parse_boolean from clikit.utils.string import parse_float from clikit.utils.string import parse_int from clikit.utils.string import parse_string class Argument(object): """ An input argument. """ REQUIRED = 1 OPTIONAL = 2 MULTI_VALUED = 4 STRING = 16 BOOLEAN = 32 INTEGER = 64 FLOAT = 128 NULLABLE = 256 def __init__( self, name, flags=0, description=None, default=None ): # type: (str, int, Optional[str], Any) -> None if not isinstance(name, basestring): raise ValueError( "The argument name must be a string. Got: {}".format(type(name)) ) if not name: raise ValueError("The argument name must not be empty.") if not name[:1].isalpha(): raise ValueError("The argument name must start with a letter") if not re.match(r"^[a-zA-Z0-9\-]+$", name): raise ValueError( "The argument name must contain letters, digits and hyphens only." ) if description is not None: if not isinstance(description, basestring): raise ValueError( "The argument description must be a string. Got: {}".format( type(description) ) ) if not description: raise ValueError("The argument description must not be empty.") self._validate_flags(flags) flags = self._add_default_flags(flags) self._name = name self._flags = flags self._description = description if self.is_multi_valued(): self._default = [] else: self._default = None if self.is_optional() or default is not None: self.set_default(default) @property def name(self): # type: () -> str return self._name @property def flags(self): # type: () -> int return self._flags @property def description(self): # type: () -> Optional[str] return self._description @property def default(self): # type: () -> Any return self._default def is_required(self): # type: () -> bool return bool(self.REQUIRED & self._flags) def is_optional(self): # type: () -> bool return bool(self.OPTIONAL & self._flags) def is_multi_valued(self): # type: () -> bool return bool(self.MULTI_VALUED & self._flags) def set_default(self, default=None): # type: (Any) -> None if self.is_required(): raise ValueError("Required arguments do not accept default values.") if self.is_multi_valued(): if default is None: default = [] elif not isinstance(default, list): raise ValueError( "The default value of a multi-valued argument must be a list. " "Got: {}".format(type(default)) ) self._default = default def parse(self, value): # type: (Any) -> Any nullable = bool(self._flags & self.NULLABLE) if self._flags & self.BOOLEAN: return parse_boolean(value, nullable) elif self._flags & self.INTEGER: return parse_int(value, nullable) elif self._flags & self.FLOAT: return parse_float(value, nullable) return parse_string(value, nullable) def _validate_flags(self, flags): # type: (int) -> None if flags & self.REQUIRED and flags & self.OPTIONAL: raise ValueError( "The argument flags REQUIRED and OPTIONAL cannot be combined." ) if flags & self.STRING: if flags & self.BOOLEAN: raise ValueError( "The argument flags STRING and BOOLEAN cannot be combined." ) if flags & self.INTEGER: raise ValueError( "The argument flags STRING and INTEGER cannot be combined." ) if flags & self.FLOAT: raise ValueError( "The argument flags STRING and FLOAT cannot be combined." ) elif flags & self.BOOLEAN: if flags & self.INTEGER: raise ValueError( "The argument flags BOOLEAN and INTEGER cannot be combined." ) if flags & self.FLOAT: raise ValueError( "The argument flags BOOLEAN and FLOAT cannot be combined." ) elif flags & self.INTEGER: if flags & self.FLOAT: raise ValueError( "The argument flags INTEGER and FLOAT cannot be combined." ) def _add_default_flags(self, flags): # type: (int) -> int if not flags & (self.REQUIRED | self.OPTIONAL): flags |= self.OPTIONAL if not flags & (self.STRING | self.BOOLEAN | self.INTEGER | self.FLOAT): flags |= self.STRING return flags clikit-0.6.2/src/clikit/api/args/format/command_name.py000066400000000000000000000014741366776574300230670ustar00rootroot00000000000000from typing import List from typing import Optional class CommandName(object): """ A command name in the console arguments. """ def __init__( self, string, aliases=None ): # type: (str, Optional[List[str]]) -> None if aliases is None: aliases = [] self._string = string self._aliases = aliases @property def string(self): # type: () -> str return self._string @property def aliases(self): # type: () -> List[str] return self._aliases def match(self, string): # type: (str) -> bool return self._string == string or string in self._aliases def __str__(self): # type: () -> str return self._string def __repr__(self): # type: () -> str return 'CommandName("{}")'.format(self._string) clikit-0.6.2/src/clikit/api/args/format/command_option.py000066400000000000000000000034001366776574300234460ustar00rootroot00000000000000import re from typing import List from typing import Optional from .abstract_option import AbstractOption class CommandOption(AbstractOption): """ A command option in the console arguments. """ def __init__( self, long_name, short_name=None, aliases=None, flags=0, description=None ): # type: (str, Optional[str], Optional[List[str]], int, Optional[str]) -> None super(CommandOption, self).__init__(long_name, short_name, flags, description) if aliases is None: aliases = [] self._long_aliases = [] self._short_aliases = [] for alias in aliases: alias = self._remove_dash_prefix(alias) if len(alias) == 1: self._validate_short_alias(alias) self._short_aliases.append(alias) else: self._validate_long_alias(alias) self._long_aliases.append(alias) @property def long_aliases(self): # type: () -> List[str] return self._long_aliases @property def short_aliases(self): # type: () -> List[str] return self._short_aliases def _validate_long_alias(self, alias): # type: (str) -> None if not alias[:1].isalpha(): raise ValueError("A long option alias must start with a letter.") if not re.match("^[a-zA-Z0-9\-]+$", alias): raise ValueError( "A long option alias must contain letters, digits and hyphens only." ) def _validate_short_alias(self, alias): # type: (str) -> None if not re.match("^[a-zA-Z]$", alias): raise ValueError( 'A short option alias must be exactly one letter. Got: "{}"'.format( alias ) ) clikit-0.6.2/src/clikit/api/args/format/option.py000066400000000000000000000123001366776574300217470ustar00rootroot00000000000000from typing import Any from typing import Optional from clikit.utils.string import parse_boolean from clikit.utils.string import parse_float from clikit.utils.string import parse_int from clikit.utils.string import parse_string from .abstract_option import AbstractOption class Option(AbstractOption): """ An input option """ NO_VALUE = 4 REQUIRED_VALUE = 8 OPTIONAL_VALUE = 16 MULTI_VALUED = 32 STRING = 128 BOOLEAN = 256 INTEGER = 512 FLOAT = 1024 NULLABLE = 2048 def __init__( self, long_name, short_name=None, flags=0, description=None, default=None, value_name="...", ): # type: (str, Optional[str], int, Optional[str], Any, str) -> None self._validate_flags(flags) super(Option, self).__init__(long_name, short_name, flags, description) self._value_name = value_name if self.is_multi_valued(): self._default = [] else: self._default = None if self.accepts_value() or default is not None: self.set_default(default) @property def default(self): # type: () -> Any return self._default @property def value_name(self): # type: () -> str return self._value_name def accepts_value(self): # type: () -> bool return not bool(self.NO_VALUE & self._flags) def parse(self, value): # type: (Any) -> Any nullable = bool(self._flags & self.NULLABLE) if self._flags & self.BOOLEAN: return parse_boolean(value, nullable) elif self._flags & self.INTEGER: return parse_int(value, nullable) elif self._flags & self.FLOAT: return parse_float(value, nullable) return parse_string(value, nullable) def is_value_required(self): # type: () -> bool return bool(self.REQUIRED_VALUE & self._flags) def is_value_optional(self): # type: () -> bool return bool(self.OPTIONAL_VALUE & self._flags) def is_multi_valued(self): # type: () -> bool return bool(self.MULTI_VALUED & self._flags) def set_default(self, default=None): # type: (Any) -> None if not self.accepts_value(): raise ValueError( "Cannot set a default value when using the flag VALUE_NONE." ) if self.is_multi_valued(): if default is None: default = [] elif not isinstance(default, list): raise ValueError( "The default value of a multi-valued option must be a list. " "Got: {}".format(type(default)) ) self._default = default def _validate_flags(self, flags): # type: (int) -> None super(Option, self)._validate_flags(flags) if flags & self.NO_VALUE: if flags & self.REQUIRED_VALUE: raise ValueError( "The option flags VALUE_NONE and VALUE_REQUIRED cannot be combined." ) if flags & self.OPTIONAL_VALUE: raise ValueError( "The option flags VALUE_NONE and VALUE_OPTIONAL cannot be combined." ) if flags & self.MULTI_VALUED: raise ValueError( "The option flags VALUE_NONE and MULTI_VALUED cannot be combined." ) if flags & self.OPTIONAL_VALUE and flags & self.MULTI_VALUED: raise ValueError( "The option flags VALUE_OPTIONAL and MULTI_VALUED cannot be combined." ) if flags & self.STRING: if flags & self.BOOLEAN: raise ValueError( "The option flags STRING and BOOLEAN cannot be combined." ) if flags & self.INTEGER: raise ValueError( "The option flags STRING and INTEGER cannot be combined." ) if flags & self.FLOAT: raise ValueError( "The option flags STRING and FLOAT cannot be combined." ) elif flags & self.BOOLEAN: if flags & self.INTEGER: raise ValueError( "The option flags BOOLEAN and INTEGER cannot be combined." ) if flags & self.FLOAT: raise ValueError( "The option flags BOOLEAN and FLOAT cannot be combined." ) elif flags & self.INTEGER: if flags & self.FLOAT: raise ValueError( "The option flags INTEGER and FLOAT cannot be combined." ) def _add_default_flags(self, flags): # type: (int) -> int flags = super(Option, self)._add_default_flags(flags) if not flags & ( self.NO_VALUE | self.REQUIRED_VALUE | self.OPTIONAL_VALUE | self.MULTI_VALUED ): flags |= self.NO_VALUE if not flags & (self.STRING | self.BOOLEAN | self.INTEGER | self.FLOAT): flags |= self.STRING if flags & self.MULTI_VALUED and not flags & self.REQUIRED_VALUE: flags |= self.REQUIRED_VALUE return flags clikit-0.6.2/src/clikit/api/args/raw_args.py000066400000000000000000000014541366776574300207640ustar00rootroot00000000000000from typing import List from typing import Optional class RawArgs(object): """ The unparsed console arguments. """ @property def script_name(self): # type: () -> Optional[str] raise NotImplementedError() @property def tokens(self): # type: () -> List[str] raise NotImplementedError() def has_token(self, token): # type: (str) -> bool raise NotImplementedError() def to_string(self, script_name=True): # type: (bool) -> str raise NotImplementedError() @property def option_tokens(self): # type: () -> List[str] raise NotImplementedError() return list(itertools.takewhile(lambda arg: arg != "--", self.tokens)) def has_option_token(self, token): # type: (str) -> bool raise NotImplementedError() clikit-0.6.2/src/clikit/api/command/000077500000000000000000000000001366776574300172635ustar00rootroot00000000000000clikit-0.6.2/src/clikit/api/command/__init__.py000066400000000000000000000001171366776574300213730ustar00rootroot00000000000000from .command import Command from .command_collection import CommandCollection clikit-0.6.2/src/clikit/api/command/command.py000066400000000000000000000125741366776574300212640ustar00rootroot00000000000000from typing import List from typing import Optional from clikit.api.args.args import Args from clikit.api.args.raw_args import RawArgs from clikit.api.args.format.args_format import ArgsFormat from clikit.api.config.command_config import CommandConfig from clikit.api.event import PRE_HANDLE from clikit.api.event import PreHandleEvent from clikit.api.io import IO class Command(object): """ A console command. """ def __init__( self, config, application=None, parent_command=None ): # type: (CommandConfig, Optional[Application], Optional[Command]) -> None from .command_collection import CommandCollection if not config.name: raise RuntimeError("The name of the command config must be set.") self._name = config.name self._short_name = None self._aliases = config.aliases self._config = config self._application = application self._parent_command = parent_command self._sub_commands = CommandCollection() self._named_sub_commands = CommandCollection() self._default_sub_commands = CommandCollection() self._args_format = config.build_args_format(self.base_format) self._dispatcher = application.config.dispatcher if application else None for sub_config in config.sub_command_configs: self.add_sub_command(sub_config) @property def name(self): # type: () -> str return self._name @property def short_name(self): # type: () -> str return self._short_name @property def full_name(self): # type: () -> str if self._parent_command: return "{} {}".format(str(self._parent_command.full_name), self._name) return self._name @property def aliases(self): # type: () -> List[str] return self._aliases def has_aliases(self): # type: () -> bool return len(self._aliases) > 0 @property def config(self): # type: () -> CommandConfig return self._config @property def args_format(self): # type: () -> ArgsFormat return self._args_format @property def application(self): # type: () -> Application return self._application @property def parent_command(self): # type: () -> Command return self._parent_command @property def sub_commands(self): # type: () -> CommandCollection return self._sub_commands def get_sub_command(self, name): # type: (str) -> Command return self._sub_commands.get(name) def has_sub_commands(self): # type: () -> bool return len(self._sub_commands) > 0 @property def named_sub_commands(self): # type: () -> CommandCollection return self._named_sub_commands def get_named_sub_command(self, name): # type: (str) -> Command return self._named_sub_commands.get(name) def has_named_sub_commands(self): # type: () -> bool return len(self._named_sub_commands) > 0 @property def default_sub_commands(self): # type: () -> CommandCollection return self._default_sub_commands def get_default_sub_command(self, name): # type: (str) -> Command return self._default_sub_commands.get(name) def has_default_sub_commands(self): # type: () -> bool return len(self._default_sub_commands) > 0 def parse(self, args, lenient=None): # type: (RawArgs, Optional[bool]) -> Args if lenient is None: lenient = self._config.is_lenient_args_parsing_enabled() return self._config.args_parser.parse(args, self._args_format, lenient) def run(self, args, io): # type: (RawArgs, IO) -> int return self.handle(self.parse(args), io) def handle(self, args, io): # type: (Args, IO) -> int try: status_code = self._do_handle(args, io) except KeyboardInterrupt: if io.is_debug(): raise status_code = 1 # Any empty value is considered a success if not status_code: return 0 # Anything else is normalized to a valid error status code return min(max(int(status_code), 1), 255) @property def base_format(self): # type: () -> Optional[ArgsFormat] if self._parent_command: return self._parent_command.args_format if self._application: return self._application.global_args_format return def add_sub_command(self, config): # type: (CommandConfig) -> None if not config.is_enabled(): return command = self.__class__(config, self._application, self) # TODO: Validate command self._sub_commands.add(command) if config.is_default(): self._default_sub_commands.add(command) if not config.is_anonymous(): self._named_sub_commands.add(command) def _do_handle(self, args, io): # type: (Args, IO) -> Optional[int] if self._dispatcher and self._dispatcher.has_listeners(PRE_HANDLE): event = PreHandleEvent(args, io, self) self._dispatcher.dispatch(PRE_HANDLE, event) if event.is_handled(): return event.status_code handler = self._config.handler handler_method = self._config.handler_method return getattr(handler, handler_method)(args, io, self) def __repr__(self): # type: () -> str return "".format(self.full_name) clikit-0.6.2/src/clikit/api/command/command_collection.py000066400000000000000000000036121366776574300234700ustar00rootroot00000000000000from typing import List from clikit.utils._compat import OrderedDict from .command import Command from .exceptions import NoSuchCommandException class CommandCollection(object): """ A collection of named commands. """ def __init__(self, commands=None): # type: (List[Command]) -> None if commands is None: commands = [] self._commands = OrderedDict() self._short_name_index = OrderedDict() self._alias_index = OrderedDict() for command in commands: self.add(command) def add(self, command): # type: (Command) -> CommandCollection name = command.name self._commands[name] = command short_name = command.short_name if short_name: self._short_name_index[short_name] = name for alias in command.aliases: self._alias_index[alias] = name return self def get(self, name): # type: (str) -> Command if name in self._commands: return self._commands[name] if name in self._short_name_index: return self._commands[self._short_name_index[name]] if name in self._alias_index: return self._commands[self._alias_index[name]] raise NoSuchCommandException(name) def is_empty(self): # type: () -> bool return not self._commands def get_names(self, include_aliases=False): # type: (bool) -> List[str] names = list(self._commands.keys()) if include_aliases: names += list(self._alias_index.keys()) names.sort() return names def __contains__(self, name): return ( name in self._commands or name in self._short_name_index or name in self._alias_index ) def __iter__(self): return iter(self._commands.values()) def __len__(self): return len(self._commands) clikit-0.6.2/src/clikit/api/command/exceptions.py000066400000000000000000000012531366776574300220170ustar00rootroot00000000000000from clikit.api.exceptions import CliKitException class NoSuchCommandException(RuntimeError, CliKitException): def __init__(self, name): # type: (str) -> None message = 'The command "{}" does not exist.'.format(name) super(NoSuchCommandException, self).__init__(message) class CannotAddCommandException(RuntimeError): @classmethod def name_exists(cls, name): return cls('A command named "{}" already exists.'.format(name)) @classmethod def option_exists(cls, name): return cls('A option named "{}" already exists.'.format(name)) @classmethod def name_empty(cls): return cls("The command name must be set.") clikit-0.6.2/src/clikit/api/config/000077500000000000000000000000001366776574300171125ustar00rootroot00000000000000clikit-0.6.2/src/clikit/api/config/__init__.py000066400000000000000000000001341366776574300212210ustar00rootroot00000000000000from .application_config import ApplicationConfig from .command_config import CommandConfig clikit-0.6.2/src/clikit/api/config/application_config.py000066400000000000000000000167301366776574300233230ustar00rootroot00000000000000import re from contextlib import contextmanager from typing import Callable try: from typing import ContextManager except ImportError: from typing_extensions import ContextManager from typing import List from typing import Optional from clikit.api.event import EventDispatcher from clikit.api.formatter import Style from clikit.api.formatter import StyleSet from clikit.api.command.exceptions import NoSuchCommandException from clikit.utils._compat import PY36 from .command_config import CommandConfig from .config import Config class ApplicationConfig(Config): """ The configuration of a console application. """ def __init__( self, name=None, version=None ): # type: (Optional[str], Optional[str]) -> None self._name = name self._version = version self._display_name = None self._help = None self._command_configs = [] # type: List[CommandConfig] self._catch_exceptions = True self._terminate_after_run = True self._command_resolver = None self._io_factory = None self._debug = False self._style_set = None self._dispatcher = None self._pre_resolve_hooks = [] # type: List[Callable] if PY36: from crashtest.solution_providers.solution_provider_repository import ( SolutionProviderRepository, ) self._solution_provider_repository = SolutionProviderRepository() else: self._solution_provider_repository = None super(ApplicationConfig, self).__init__() @property def name(self): # type: () -> Optional[str] return self._name def set_name(self, name): # type: (Optional[str]) -> ApplicationConfig self._name = name return self @property def display_name(self): # type: () -> str """ Returns the application name as it is displayed in the help. """ if self._display_name is not None: return self._display_name return self.default_display_name def set_display_name( self, display_name ): # type: (Optional[str]) -> ApplicationConfig self._display_name = display_name return self @property def version(self): # type: () -> Optional[str] return self._version def set_version(self, version): # type: (Optional[str]) -> ApplicationConfig self._version = version return self @property def help(self): # type: () -> Optional[str] return self._help def set_help(self, help): # type: (Optional[str]) -> ApplicationConfig self._help = help return self @property def dispatcher(self): # type: () -> EventDispatcher return self._dispatcher def set_event_dispatcher( self, dispatcher ): # type: (EventDispatcher) -> ApplicationConfig self._dispatcher = dispatcher return self def add_event_listener( self, event_name, listener, priority=0 ): # type: (str, Callable, int) -> ApplicationConfig if self._dispatcher is None: self._dispatcher = EventDispatcher() self._dispatcher.add_listener(event_name, listener, priority) return self def is_exception_caught(self): # type: () -> bool return self._catch_exceptions def set_catch_exceptions( self, catch_exceptions ): # type: (bool) -> ApplicationConfig self._catch_exceptions = catch_exceptions return self def is_terminated_after_run(self): # type: () -> bool return self._terminate_after_run def set_terminate_after_run( self, terminate_after_run ): # type: (bool) -> ApplicationConfig self._terminate_after_run = terminate_after_run return self @property def command_resolver(self): # type: () -> CommandResolver if self._command_resolver is None: self._command_resolver = self.default_command_resolver return self._command_resolver def set_command_resolver( self, command_resolver ): # type: (CommandResolver) -> ApplicationConfig self._command_resolver = command_resolver return self @property def io_factory(self): # type: () -> Optional[Callable] return self._io_factory def set_io_factory( self, io_factory ): # type: (Optional[Callable]) -> ApplicationConfig self._io_factory = io_factory return self def is_debug(self): # type: () -> bool return self._debug def debug(self, debug=True): # type: (bool) -> ApplicationConfig self._debug = debug return self @property def style_set(self): # type: () -> StyleSet if self._style_set is None: self._style_set = self.default_style_set return self._style_set def set_style_set(self, style_set): # type: (StyleSet) -> ApplicationConfig self._style_set = style_set return self def add_style(self, style): # type: (Style) -> ApplicationConfig self.style_set.add(style) return self def add_styles(self, styles): # type: (List[Style]) -> ApplicationConfig for style in styles: self.add_style(style) return self def remove_style(self, tag): # type: (str) -> ApplicationConfig self.style_set.remove(tag) @contextmanager def command(self, name): # type: (str) -> ContextManager[CommandConfig] command_config = CommandConfig(name) self.add_command_config(command_config) yield command_config def create_command(self, name): # type: (str) -> CommandConfig command_config = CommandConfig(name) self.add_command_config(command_config) return command_config @contextmanager def edit_command(self, name): # type: (str) -> CommandConfig command_config = self.get_command_config(name) yield command_config def add_command_config( self, command_config ): # type: (CommandConfig) -> ApplicationConfig self._command_configs.append(command_config) return self def add_command_configs( self, command_configs ): # type: (List[CommandConfig]) -> ApplicationConfig for command_config in command_configs: self.add_command_config(command_config) return self def get_command_config(self, name): # type: (str) -> CommandConfig for command_config in self._command_configs: if command_config.name == name: return command_config raise NoSuchCommandException(name) @property def command_configs(self): # type: () -> List[CommandConfig] return self._command_configs def has_command_config(self, name): # type: (str) -> bool for command_config in self._command_configs: if command_config.name == name: return True raise False def has_command_configs(self): # type: () -> bool return len(self._command_configs) > 0 @property def default_display_name(self): # type: () -> Optional[str] if self._name is None: return return re.sub(r"[\s\-_]+", " ", self._name).title() @property def default_style_set(self): # type: () -> StyleSet raise NotImplementedError() @property def default_command_resolver(self): raise NotImplementedError() @property def solution_provider_repository(self): return self._solution_provider_repository clikit-0.6.2/src/clikit/api/config/command_config.py000066400000000000000000000161601366776574300224330ustar00rootroot00000000000000from contextlib import contextmanager from typing import Any from typing import List from typing import Optional from clikit.api.args.args_parser import ArgsParser from clikit.api.args.format.args_format import ArgsFormat from clikit.api.args.format.args_format_builder import ArgsFormatBuilder from clikit.api.args.format.command_name import CommandName from clikit.api.command.exceptions import NoSuchCommandException from .config import Config class CommandConfig(Config): """ The configuration of a console command. """ def __init__(self, name=None): # type: (Optional[str]) -> None super(CommandConfig, self).__init__() self._name = name self._aliases = [] self._description = "" self._help = None self._enabled = True self._hidden = False self._process_title = None self._default = None self._anonymous = None self._sub_command_configs = [] # type: List[CommandConfig] self._parent_config = None # type: Optional[CommandConfig] @property def name(self): # type: () -> Optional[str] return self._name def set_name(self, name): # type: (Optional[str]) -> CommandConfig self._name = name return self @property def aliases(self): # type: () -> List[str] return self._aliases def add_alias(self, alias): # type: (str) -> CommandConfig self._aliases.append(alias) return self def add_aliases(self, aliases): # type: (List[str]) -> CommandConfig for alias in aliases: self.add_alias(alias) return self def set_aliases(self, aliases): # type: (List[str]) -> CommandConfig self._aliases = [] return self.add_aliases(aliases) @property def description(self): # type: () -> str return self._description def set_description(self, description): # type: (str) -> CommandConfig self._description = description return self @property def help(self): # type: () -> Optional[str] return self._help def set_help(self, help): # type: (Optional[str]) -> CommandConfig self._help = help return self def is_enabled(self): # type: () -> bool return self._enabled def enable(self): # type: () -> CommandConfig self._enabled = True return self def disable(self): # type: () -> CommandConfig self._enabled = False return self def is_hidden(self): # type: () -> bool return self._hidden def hide(self, hidden=True): # type: (bool) -> CommandConfig self._hidden = hidden return self @property def process_title(self): # type: () -> Optional[str] return self._process_title def set_process_title( self, process_title ): # type: (Optional[str]) -> CommandConfig self._process_title = process_title return self def default(self, default=True): # type: (bool) -> CommandConfig """ Marks the command as the default command. """ self._default = default self._anonymous = False return self def anonymous(self): # type: () -> CommandConfig self._default = True self._anonymous = True return self def is_default(self): # type: () -> bool return self._default def is_anonymous(self): # type: () -> bool return self._anonymous @property def parent_config(self): # type: () -> Optional[CommandConfig] return self._parent_config def set_parent_config( self, parent_config ): # type: (Optional[CommandConfig]) -> CommandConfig self._parent_config = parent_config return self def is_sub_command_config(self): # type: () -> bool return self._parent_config is not None def build_args_format( self, base_format=None ): # type: (Optional[ArgsFormat]) -> ArgsFormat builder = ArgsFormatBuilder(base_format) if not self._anonymous: builder.add_command_name(CommandName(self.name, self.aliases)) builder.add_options(*self.options.values()) builder.add_arguments(*self.arguments.values()) return builder.format @contextmanager def sub_command(self, name): # type: (str) -> CommandConfig sub_command_config = CommandConfig(name) self.add_sub_command_config(sub_command_config) yield sub_command_config def create_sub_command(self, name): # type: (str) -> CommandConfig sub_command_config = CommandConfig(name) self.add_sub_command_config(sub_command_config) return sub_command_config @contextmanager def edit_sub_command(self, name): # type: (str) -> CommandConfig sub_command_config = self.get_sub_command_config(name) yield sub_command_config def add_sub_command_config( self, sub_command_config ): # type: (CommandConfig) -> CommandConfig self._sub_command_configs.append(sub_command_config) return self def add_sub_command_configs( self, sub_command_configs ): # type: (List[CommandConfig]) -> CommandConfig for sub_command_config in sub_command_configs: self.add_sub_command_config(sub_command_config) return self def get_sub_command_config(self, name): # type: (str) -> CommandConfig for sub_command_config in self._sub_command_configs: if sub_command_config.name == name: return sub_command_config raise NoSuchCommandException(name) @property def sub_command_configs(self): # type: () -> List[CommandConfig] return self._sub_command_configs def has_sub_command_config(self, name): # type: (str) -> bool for sub_command_config in self._sub_command_configs: if sub_command_config.name == name: return True return False def has_sub_command_configs(self): # type: () -> bool return len(self._sub_command_configs) > 0 @property def default_helper_set(self): # type: () -> HelperSet if self._parent_config: return self._parent_config.default_helper_set return super(CommandConfig, self).default_helper_set @property def default_args_parser(self): # type: () -> ArgsParser if self._parent_config: return self._parent_config.default_args_parser return super(CommandConfig, self).default_args_parser @property def default_lenient_args_parsing(self): # type: () -> bool if self._parent_config: return self._parent_config.default_lenient_args_parsing return super(CommandConfig, self).default_lenient_args_parsing @property def default_handler(self): # type: () -> Any if self._parent_config: return self._parent_config.default_handler return super(CommandConfig, self).default_handler @property def default_handler_method(self): # type: () -> str if self._parent_config: return self._parent_config.default_handler_method return super(CommandConfig, self).default_handler_method clikit-0.6.2/src/clikit/api/config/config.py000066400000000000000000000100051366776574300207250ustar00rootroot00000000000000from typing import Any from typing import Dict from typing import Optional from clikit.api.args.args_parser import ArgsParser from clikit.api.args.format.args_format_builder import ArgsFormatBuilder from clikit.api.args.format.argument import Argument from clikit.api.args.format.option import Option from clikit.args.default_args_parser import DefaultArgsParser class Config(object): """ Implements methods shared by all configurations. """ def __init__(self): self._format_builder = ArgsFormatBuilder() self._helper_set = None self._args_parser = None self._lenient_args_parsing = None self._handler = None self._handler_method = None self.configure() @property def arguments(self): # type: () -> Dict[str, Argument] return self._format_builder.get_arguments() def add_argument( self, name, flags=0, description=None, default=None ): # type: (str, int, Optional[str], Any) -> Config argument = Argument(name, flags, description, default) self._format_builder.add_argument(argument) return self @property def options(self): # type: () -> Dict[str, Option] return self._format_builder.get_options() def add_option( self, long_name, short_name=None, flags=0, description=None, default=None, value_name="...", ): # type: (str, Optional[str], int, Optional[str], Any, str) -> Config option = Option(long_name, short_name, flags, description, default, value_name) self._format_builder.add_option(option) return self @property def helper_set(self): # type: () -> HelperSet if self._helper_set is None: return self.default_helper_set return self._helper_set def set_helper_set(self, helper_set): # type: (HelperSet) -> Config self._helper_set = helper_set return self @property def args_parser(self): # type: () -> ArgsParser if self._args_parser is None: return self.default_args_parser return self._args_parser def set_args_parser(self, args_parser): # type: (ArgsParser) -> Config self._args_parser = args_parser return self def is_lenient_args_parsing_enabled(self): # type: () -> bool if self._lenient_args_parsing is None: return self.default_lenient_args_parsing return self._lenient_args_parsing def enable_lenient_args_parsing(self): # type: () -> Config self._lenient_args_parsing = True return self def disable_lenient_args_parsing(self): # type: () -> Config self._lenient_args_parsing = False return self @property def handler(self): # type: () -> Any if self._handler is None: return self.default_handler if callable(self._handler): return self._handler() return self._handler def set_handler(self, handler): # type: (Any) -> Config self._handler = handler return self @property def handler_method(self): # type: () -> str if self._handler_method is None: return self.default_handler_method return self._handler_method def set_handler_method(self, handler_method): # type: (str) -> Config self._handler_method = handler_method return self @property def default_helper_set(self): # type: () -> HelperSet return HelperSet() @property def default_args_parser(self): # type: () -> ArgsParser return DefaultArgsParser() @property def default_lenient_args_parsing(self): # type: () -> bool return False @property def default_handler(self): # type: () -> Any return lambda: None @property def default_handler_method(self): # type: () -> Any return "handle" def configure(self): # type: () -> None """ Adds the default configuration. Should be overridden in subclasses """ clikit-0.6.2/src/clikit/api/event/000077500000000000000000000000001366776574300167665ustar00rootroot00000000000000clikit-0.6.2/src/clikit/api/event/__init__.py000066400000000000000000000004731366776574300211030ustar00rootroot00000000000000from .config_event import ConfigEvent from .console_events import CONFIG from .console_events import PRE_HANDLE from .console_events import PRE_RESOLVE from .event import Event from .event_dispatcher import EventDispatcher from .pre_handle_event import PreHandleEvent from .pre_resolve_event import PreResolveEvent clikit-0.6.2/src/clikit/api/event/config_event.py000066400000000000000000000006001366776574300220020ustar00rootroot00000000000000from .event import Event class ConfigEvent(Event): """ Dispatched after the configuration is built. Use this event to add custom configuration to the application. """ def __init__(self, config): # type: (ApplicationConfig) -> None self._config = config @property def config(self): # type: () -> ApplicationConfig return self._config clikit-0.6.2/src/clikit/api/event/console_events.py000066400000000000000000000001121366776574300223600ustar00rootroot00000000000000PRE_RESOLVE = "pre-resolve" PRE_HANDLE = "pre-handle" CONFIG = "config" clikit-0.6.2/src/clikit/api/event/event.py000066400000000000000000000005101366776574300204550ustar00rootroot00000000000000class Event(object): """ Event """ def __init__(self): # type: () -> None self._propagation_stopped = False def is_propagation_stopped(self): # type: () -> bool return self._propagation_stopped def stop_propagation(self): # type: () -> None self._propagation_stopped = True clikit-0.6.2/src/clikit/api/event/event_dispatcher.py000066400000000000000000000057571366776574300227050ustar00rootroot00000000000000from typing import Callable from typing import Dict from typing import List from typing import Optional from typing import Union from .event import Event class EventDispatcher(object): def __init__(self): # type: () -> None self._listeners = {} self._sorted = {} def dispatch(self, event_name, event=None): # type: (str, Event) -> Event if event is None: event = Event() listeners = self.get_listeners(event_name) if listeners: self._do_dispatch(listeners, event_name, event) return event def get_listeners( self, event_name=None ): # type: (str) -> Union[List[Callable], Dict[str, Callable]] if event_name is not None: if event_name not in self._listeners: return [] if event_name not in self._sorted: self._sort_listeners(event_name) return self._sorted[event_name] for event_name, event_listeners in self._listeners.items(): if event_name not in self._sorted: self._sort_listeners(event_name) return self._sorted def get_listener_priority( self, event_name, listener ): # type: (str, Callable) -> Optional[int] if event_name not in self._listeners: return for priority, listeners in self._listeners[event_name].items(): for v in listeners: if v == listener: return priority def has_listeners(self, event_name=None): # type: (Optional[str]) -> bool if event_name is not None: if event_name not in self._listeners: return False return len(self._listeners[event_name]) > 0 for event_listeners in self._listeners.values(): if event_listeners: return True return False def add_listener( self, event_name, listener, priority=0 ): # type: (str, Callable, int) -> None if event_name not in self._listeners: self._listeners[event_name] = {} if priority not in self._listeners[event_name]: self._listeners[event_name][priority] = [] self._listeners[event_name][priority].append(listener) if event_name in self._sorted: del self._sorted[event_name] def _do_dispatch( self, listeners, event_name, event ): # type: (List[Callable], str, Event) -> None for listener in listeners: if event.is_propagation_stopped(): break listener(event, event_name, self) def _sort_listeners(self, event_name): # type: (str) -> None """ Sorts the internal list of listeners for the given event by priority. """ self._sorted[event_name] = [] for priority, listeners in sorted( self._listeners[event_name].items(), key=lambda t: -t[0] ): for listener in listeners: self._sorted[event_name].append(listener) clikit-0.6.2/src/clikit/api/event/pre_handle_event.py000066400000000000000000000022161366776574300226430ustar00rootroot00000000000000from clikit.api.args import Args from clikit.api.io import IO from .event import Event class PreHandleEvent(Event): """ Dispatched before a command is handled. Add a listener for this event to execute custom logic before or instead of the default handler. """ def __init__(self, args, io, command): # type: (Args, IO, Command) -> None super(PreHandleEvent, self).__init__() self._args = args self._io = io self._command = command self._handled = False self._status_code = 0 @property def args(self): # type: () -> Args return self._args @property def io(self): # type: () -> IO return self._io @property def command(self): # type: () -> Command return self._command @property def status_code(self): # type: () -> int return self._status_code def is_handled(self): # type: () -> bool return self._handled def handled(self, handled): # type: (bool) -> None self._handled = handled def set_status_code(self, status_code): # type: (int) -> None self._status_code = status_code clikit-0.6.2/src/clikit/api/event/pre_resolve_event.py000066400000000000000000000020311366776574300230620ustar00rootroot00000000000000from clikit.api.args import RawArgs from clikit.api.resolver import ResolvedCommand from .event import Event class PreResolveEvent(Event): """ Dispatched before the console arguments are resolved to a command. Add a listener for this event to customize the command used for the given console arguments. """ def __init__(self, raw_args, application): # type: (RawArgs, Application) -> None super(PreResolveEvent, self).__init__() self._raw_args = raw_args self._application = application self._resolved_command = None @property def raw_args(self): # type: () -> RawArgs return self._raw_args @property def application(self): # type: () -> Application return self._application @property def resolved_command(self): # type: () -> ResolvedCommand return self._resolved_command def set_resolved_command( self, resolved_command=None ): # type: (ResolvedCommand) -> None self._resolved_command = resolved_command clikit-0.6.2/src/clikit/api/exceptions.py000066400000000000000000000001411366776574300203740ustar00rootroot00000000000000class CliKitException(Exception): """ Base class for CliKit exceptions """ pass clikit-0.6.2/src/clikit/api/formatter/000077500000000000000000000000001366776574300176505ustar00rootroot00000000000000clikit-0.6.2/src/clikit/api/formatter/__init__.py000066400000000000000000000001321366776574300217550ustar00rootroot00000000000000from .formatter import Formatter from .style import Style from .style_set import StyleSet clikit-0.6.2/src/clikit/api/formatter/formatter.py000066400000000000000000000012311366776574300222220ustar00rootroot00000000000000class Formatter(object): """ Formats strings. """ def format(self, string, style=None): # type: (str, Style) -> str """ Formats the given string. """ raise NotImplementedError() def remove_format(self, string): # type: (str) -> str """ Removes the format tags from the given string. """ raise NotImplementedError() def disable_ansi(self): # type: () -> bool raise NotImplementedError() def force_ansi(self): # type: () -> bool raise NotImplementedError() def add_style(self, style): # type: (Style) -> None raise NotImplementedError() clikit-0.6.2/src/clikit/api/formatter/style.py000066400000000000000000000053721366776574300213710ustar00rootroot00000000000000from typing import Optional class Style(object): """ A formatter style. """ def __init__(self, tag=None): # type: (Optional[str]) -> None self._tag = tag self._fg_color = None self._bg_color = None self._bold = False self._underlined = False self._italic = False self._dark = False self._blinking = False self._inverse = False self._hidden = False @property def tag(self): # type: () -> Optional[str] return self._tag @property def foreground_color(self): # type: () -> str return self._fg_color @property def background_color(self): # type: () -> str return self._bg_color def fg(self, color): # type: (str) -> Style """ Sets the foreground color. """ self._fg_color = color return self def bg(self, color): # type: (str) -> Style """ Sets the background color. """ self._bg_color = color return self def bold(self, bold=True): # type: (bool) -> Style """ Sets or unsets the font weight to bold. """ self._bold = bold return self def underlined(self, underlined=True): # type: (bool) -> Style """ Enables or disables underlining. """ self._underlined = underlined return self def italic(self, italic=True): # type: (bool) -> Style """ Enables or disables italic """ self._italic = italic return self def dark(self, dark=True): # type: (bool) -> Style """ Enables or disables dark """ self._dark = dark return self def blinking(self, blinking=True): # type: (bool) -> Style """ Enables or disables blinking. """ self._blinking = blinking return self def inverse(self, inverse=True): # type: (bool) -> Style """ Enables or disables inverse colors. """ self._inverse = inverse return self def hidden(self, hidden=True): # type: (bool) -> Style """ Hides or shows the text. """ self._hidden = hidden return self def is_bold(self): # type: () -> bool return self._bold def is_underlined(self): # type: () -> bool return self._underlined def is_italic(self): # type: () -> bool return self._italic def is_dark(self): # type: () -> bool return self._dark def is_blinking(self): # type: () -> bool return self._blinking def is_inverse(self): # type: () -> bool return self._inverse def is_hidden(self): # type: () -> bool return self._hidden clikit-0.6.2/src/clikit/api/formatter/style_set.py000066400000000000000000000017251366776574300222420ustar00rootroot00000000000000from typing import Dict from typing import List from typing import Optional from .style import Style class StyleSet(object): """ A set of styles used by the formatter. """ def __init__(self, styles=None): # type: (Optional[List[Style]]) -> None if styles is None: styles = [] self._styles = {} for style in styles: self.add(style) @property def styles(self): # type: () -> (Dict[str, Style]) return self._styles def add(self, style): # type: (Style) -> None if not style.tag: raise ValueError("The tag of a style added to the style set must be set.") self._styles[style.tag] = style def replace(self, styles): # type: (Optional[List[Style]]) -> None self._styles = {} for style in styles: self.add(style) def remove(self, tag): # type: (str) -> None if tag in self._styles: del self._styles[tag] clikit-0.6.2/src/clikit/api/io/000077500000000000000000000000001366776574300162545ustar00rootroot00000000000000clikit-0.6.2/src/clikit/api/io/__init__.py000066400000000000000000000002731366776574300203670ustar00rootroot00000000000000from .input import Input from .input_stream import InputStream from .io import IO from .io_exception import IOException from .output import Output from .output_stream import OutputStream clikit-0.6.2/src/clikit/api/io/flags.py000066400000000000000000000003761366776574300177300ustar00rootroot00000000000000# Flag: Always write data NORMAL = 0 # Flag: Only write if the verbosity is "verbose" or greater. VERBOSE = 1 # Flag: Only write if the verbosity is "very verbose" or greater. VERY_VERBOSE = 2 # Flag: Only write if the verbosity is "debug". DEBUG = 4 clikit-0.6.2/src/clikit/api/io/input.py000066400000000000000000000035131366776574300177670ustar00rootroot00000000000000from typing import Optional from .input_stream import InputStream class Input(object): """ The console input. This class wraps an input stream and adds convenience functionality for reading that stream. """ def __init__(self, stream): # type: (InputStream) -> None self._stream = stream self._interactive = True def read(self, length, default=None): # type: (int, Optional[str]) -> str """ Reads the given amount of characters from the input stream. :raises: IOException """ if not self._interactive: return default return self._stream.read(length) def read_line( self, length=None, default=None ): # type: (Optional[int], Optional[str]) -> str """ Reads a line from the input stream. :raises: IOException """ if not self._interactive: return default return self._stream.read_line(length=length) def close(self): # type: () -> None """ Closes the input. """ self._stream.close() def is_closed(self): # type: () -> bool """ Returns whether the input is closed. """ return self._stream.is_closed() def set_stream(self, stream): # type: (InputStream) -> None """ Sets the underlying stream. """ self._stream = stream @property def stream(self): # type: () -> InputStream return self._stream def set_interactive(self, interactive): # type: (bool) -> None """ Enables or disables interaction with the user. """ self._interactive = interactive def is_interactive(self): # type: () -> bool """ Returns whether the user may be asked for input. """ return self._interactive clikit-0.6.2/src/clikit/api/io/input_stream.py000066400000000000000000000013331366776574300213400ustar00rootroot00000000000000from typing import Optional class InputStream(object): """ The console input stream. """ def read(self, length): # type: (int) -> str """ Reads the given amount of characters from the stream. """ raise NotImplementedError() def read_line(self, length=None): # type: (Optional[int]) -> str """ Reads a line from the stream. """ raise NotImplementedError() def close(self): # type: () -> None """ Closes the stream """ raise NotImplementedError() def is_closed(self): # type: () -> bool """ Returns whether the stream is closed or not """ raise NotImplementedError() clikit-0.6.2/src/clikit/api/io/io.py000066400000000000000000000155171366776574300172460ustar00rootroot00000000000000from typing import Optional from clikit.api.formatter import Formatter from .input import Input from .output import Output class IO(Formatter): """ Provides methods to access the console input and output. """ def __init__( self, input, output, error_output ): # type: (Input, Output, Output) -> None self._input = input self._output = output self._error_output = error_output self._terminal_dimensions = None @property def input(self): # type: () -> Input return self._input @property def output(self): # type: () -> Output return self._output @property def error_output(self): # type: () -> Output return self._error_output def read(self, length, default=None): # type: (int, Optional[str]) -> str """ Reads the given amount of characters from the standard input. :raises: IOException """ return self._input.read(length, default=default) def read_line( self, length=None, default=None ): # type: (Optional[int], Optional[str]) -> str """ Reads a line from the standard input. :raises: IOException """ return self._input.read_line(length=length, default=default) def write(self, string, flags=None): # type: (str, Optional[int]) -> None """ Writes a string to the standard output. The string is formatted before it is written to the output. """ self._output.write(string, flags=flags) def write_line(self, string, flags=None): # type: (str, Optional[int]) -> None """ Writes a line to the standard output. The string is formatted before it is written to the output. """ self._output.write_line(string, flags=flags) def write_raw(self, string, flags=None): # type: (str, Optional[int]) -> None """ Writes a string to the standard output without formatting. """ self._output.write_raw(string, flags=flags) def write_line_raw(self, string, flags=None): # type: (str, Optional[int]) -> None """ Writes a line to the standard output without formatting. """ self._output.write_raw(string, flags=flags) def error(self, string, flags=None): # type: (str, Optional[int]) -> None """ Writes a string to the error output. The string is formatted before it is written to the output. """ self._error_output.write(string, flags=flags) def error_line(self, string, flags=None): # type: (str, Optional[int]) -> None """ Writes a line to the error output. The string is formatted before it is written to the output. """ self._error_output.write_line(string, flags=flags) def error_raw(self, string, flags=None): # type: (str, Optional[int]) -> None """ Writes a string to the error output without formatting. """ self._error_output.write_raw(string, flags=flags) def error_line_raw(self, string, flags=None): # type: (str, Optional[int]) -> None """ Writes a line to the error output without formatting. """ self._error_output.write_raw(string, flags=flags) def flush(self): # type: () -> None """ Flushes the outputs and forces all pending text to be written out. """ self._output.flush() self._error_output.flush() def close(self): # type: () -> None """ Closes the input and the outputs. """ self._input.close() self._output.close() self._error_output.close() def set_interactive(self, interactive): # type: (bool) -> None """ Enables or disables interaction with the user. """ self._input.set_interactive(interactive) def is_interactive(self): # type: () -> bool """ Returns whether the user may be asked for input. """ return self._input.is_interactive() def set_verbosity(self, verbosity): # type: (int) -> None """ Sets the verbosity of the output. """ self._output.set_verbosity(verbosity) self._error_output.set_verbosity(verbosity) def is_verbose(self): # type: () -> bool """ Returns whether the verbosity is VERBOSE or greater. """ return self._output.is_verbose() def is_very_verbose(self): # type: () -> bool """ Returns whether the verbosity is VERY_VERBOSE or greater. """ return self._output.is_very_verbose() def is_debug(self): # type: () -> bool """ Returns whether the verbosity is DEBUG. """ return self._output.is_debug() @property def verbosity(self): # type: () -> int return self._output.verbosity def set_quiet(self, quiet): # type: (bool) -> None """ Sets whether all output should be suppressed. """ self._output.set_quiet(quiet) self._error_output.set_quiet(quiet) def is_quiet(self): # type: () -> bool """ Returns whether all output is suppressed. """ return self._output.is_quiet() def set_terminal_dimensions(self, dimensions): # type: (Rectangle) -> None """ Sets the dimensions of the terminal. """ self._terminal_dimensions = dimensions @property def terminal_dimensions(self): # type: () -> Rectangle if not self._terminal_dimensions: self._terminal_dimensions = self.get_default_terminal_dimensions() return self._terminal_dimensions def get_default_terminal_dimensions(self): # type: () -> Rectangle """ Returns the default terminal dimensions. """ from clikit.ui.rectangle import Rectangle return Rectangle(80, 20) def set_formatter(self, formatter): # type: (Formatter) -> None """ Sets the output formatter. """ self._output.set_formatter(formatter) self._error_output.set_formatter(formatter) def supports_ansi(self): # type: () -> bool return self._output.supports_ansi() @property def formatter(self): # type: () -> Formatter """ Returns the output formatter. """ return self._output.formatter def format(self, string, style=None): # type: (str, Style) -> str """ Formats the given string. """ return self._output.formatter.format(string, style=style) def remove_format(self, string): # type: (str) -> str """ Removes the format tags from the given string. """ return self._output.formatter.remove_format(string) def section(self): return self.__class__( self._input, self._output.section(), self._error_output.section() ) clikit-0.6.2/src/clikit/api/io/io_exception.py000066400000000000000000000001341366776574300213110ustar00rootroot00000000000000class IOException(RuntimeError): """ Thrown if an error happens during I/O. """ clikit-0.6.2/src/clikit/api/io/output.py000066400000000000000000000142231366776574300201700ustar00rootroot00000000000000import os from typing import Optional from clikit.api.formatter import Formatter from clikit.formatter import NullFormatter from clikit.utils._compat import to_str from .flags import DEBUG from .flags import NORMAL from .flags import VERBOSE from .flags import VERY_VERBOSE from .output_stream import OutputStream class Output(Formatter): """ The console output. This class wraps an output stream and adds convenience functionality for writing that stream. """ def __init__( self, stream, formatter=None ): # type: (OutputStream, Optional[Formatter]) -> None self._stream = stream if formatter is None: formatter = NullFormatter() self._formatter = formatter self._quiet = False self._format_output = ( self._stream.supports_ansi() and not formatter.disable_ansi() or formatter.force_ansi() ) self._verbosity = 0 self._section_outputs = [] def write( self, string, flags=None, new_line=False ): # type: (str, Optional[int], bool) -> None """ Writes a string to the output stream. The string is formatted before it is written to the output stream. """ if self._may_write(flags): if self._format_output: formatted = self.format(string) else: formatted = self.remove_format(string) if new_line: formatted += "\n" self._stream.write(to_str(formatted)) def write_line(self, string, flags=None): # type: (str, Optional[int]) -> None """ Writes a line of text to the output stream. The string is formatted before it is written to the output stream. """ self.write(string, flags=flags, new_line=True) def write_raw(self, string, flags=None): # type: (str, Optional[int]) -> None """ Writes a string to the output stream without formatting. """ if self._may_write(flags): self._stream.write(to_str(string)) def write_line_raw(self, string, flags=None): # type: (str, Optional[int]) -> None """ Writes a string to the output stream without formatting. """ if self._may_write(flags): self._stream.write(to_str(string.rstrip("\n") + "\n")) def flush(self): # type: () -> None """ Forces all pending text to be written out. """ self._stream.flush() def close(self): # type: () -> None """ Closes the output. """ self._stream.close() def is_closed(self): # type: () -> bool """ Returns whether the output is closed. """ return self._stream.is_closed() def set_stream(self, stream): # type: (OutputStream) -> None """ Sets the underlying stream. """ self._stream = stream if self._formatter.force_ansi(): self._format_output = True else: self._format_output = self._stream.supports_ansi() @property def stream(self): # type: () -> OutputStream """ Returns the underlying stream. """ return self._stream def set_formatter(self, formatter): # type: (Formatter) -> None """ Sets the underlying formatter. """ self._formatter = formatter if formatter.force_ansi(): self._format_output = True else: self._format_output = self._stream.supports_ansi() @property def formatter(self): # type: () -> Formatter """ Returns the underlying formatter. """ return self._formatter def set_verbosity(self, verbosity): # type: (int) -> None """ Sets the verbosity level of the output. """ if verbosity not in {NORMAL, VERBOSE, VERY_VERBOSE, DEBUG}: raise ValueError( "The verbosity must be one of NORMAL, VERBOSE, VERY_VERBOSE or DEBUG." ) self._verbosity = verbosity @property def verbosity(self): # type: () -> int """ Returns the current verbosity level. """ return self._verbosity def is_verbose(self): # type: () -> bool """ Returns whether the verbosity is VERBOSE or greater. """ return self._verbosity >= VERBOSE def is_very_verbose(self): # type: () -> bool """ Returns whether the verbosity is VERY_VERBOSE or greater. """ return self._verbosity >= VERY_VERBOSE def is_debug(self): # type: () -> bool """ Returns whether the verbosity is DEBUG. """ return self._verbosity == DEBUG def set_quiet(self, quiet): # type: (bool) -> None """ Sets whether all output should be suppressed. """ self._quiet = quiet def is_quiet(self): # type: () -> bool """ Returns whether all output is suppressed. """ return self._quiet def format(self, string, style=None): # type: (str, Style) -> str """ Formats the given string. """ return self._formatter.format(string, style) def remove_format(self, string): # type: (str) -> str """ Removes the format tags from the given string. """ return self._formatter.remove_format(string) def supports_ansi(self): # type: () -> bool return self._format_output def section(self): # type: (SectionOutput) -> SectionOutput from .section_output import SectionOutput return SectionOutput(self._stream, self._section_outputs, self._formatter) def _may_write(self, flags): # type: (int) -> bool """ Returns whether an output may be written for the given flags. """ if flags is None: flags = 0 if self._quiet: return False if flags & VERBOSE: return self._verbosity >= VERBOSE if flags & VERY_VERBOSE: return self._verbosity >= VERY_VERBOSE if flags & DEBUG: return self._verbosity >= DEBUG return True clikit-0.6.2/src/clikit/api/io/output_stream.py000066400000000000000000000015311366776574300215410ustar00rootroot00000000000000class OutputStream(object): """ The console output stream. """ def write(self, string): # type: (str) -> None """ Writes a string to the stream. """ raise NotImplementedError() def flush(self): # type: () -> None """ Flushes the stream and forces all pending text to be written out. """ raise NotImplementedError() def supports_ansi(self): # type: () -> bool """ Returns whether the stream supports ANSI format codes. """ raise NotImplementedError() def close(self): # type: () -> None """ Closes the stream. """ raise NotImplementedError() def is_closed(self): # type: () -> bool """ Returns whether the stream is closed. """ raise NotImplementedError() clikit-0.6.2/src/clikit/api/io/section_output.py000066400000000000000000000060201366776574300217100ustar00rootroot00000000000000import math from typing import List from typing import Optional from clikit.api.formatter import Formatter from clikit.utils.terminal import Terminal from .output import Output from .output_stream import OutputStream class SectionOutput(Output): def __init__( self, stream, sections, formatter=None ): # type: (OutputStream, List[SectionOutput], Optional[Formatter]) -> None super(SectionOutput, self).__init__(stream, formatter=formatter) self._content = [] self._lines = 0 sections.insert(0, self) self._sections = sections self._terminal = Terminal() @property def content(self): # type: () -> str return "".join(self._content) @property def lines(self): # type: () -> int return self._lines def clear(self, lines=None): # type: (Optional[int]) -> None if ( not self._content or not self.supports_ansi() and not self._formatter.force_ansi() ): return if lines: # Multiply lines by 2 to cater for each new line added between content del self._content[-(lines * 2) :] else: lines = self._lines self._content = [] self._lines -= lines super(SectionOutput, self).write( self._pop_stream_content_until_current_section(lines) ) def overwrite(self, message): # type: (str) -> None self.clear() self.write_line(message) def add_content(self, content): # type: (str) -> None for line_content in content.split("\n"): self._lines += ( math.ceil( len(self.remove_format(line_content).replace("\t", " ")) / self._terminal.width ) or 1 ) self._content.append(line_content) self._content.append("\n") def write( self, string, flags=None, new_line=False ): # type: (str, Optional[int], bool) -> None if not self.supports_ansi() and not self._formatter.force_ansi(): return super(SectionOutput, self).write(string, flags=flags) erased_content = self._pop_stream_content_until_current_section() self.add_content(string) super(SectionOutput, self).write(string, new_line=True) super(SectionOutput, self).write(erased_content) def _pop_stream_content_until_current_section( self, lines_to_clear_count=0 ): # type: (int) -> str erased_content = [] for section in self._sections: if section is self: break lines_to_clear_count += section.lines erased_content.append(section.content) if lines_to_clear_count > 0: # Move cursor up n lines super(SectionOutput, self).write("\x1b[{}A".format(lines_to_clear_count)) # Erase to end of screen super(SectionOutput, self).write("\x1b[0J") return "".join(reversed(erased_content)) clikit-0.6.2/src/clikit/api/resolver/000077500000000000000000000000001366776574300175065ustar00rootroot00000000000000clikit-0.6.2/src/clikit/api/resolver/__init__.py000066400000000000000000000001341366776574300216150ustar00rootroot00000000000000from .command_resolver import CommandResolver from .resolved_command import ResolvedCommand clikit-0.6.2/src/clikit/api/resolver/command_resolver.py000066400000000000000000000005301366776574300234150ustar00rootroot00000000000000from clikit.api.args import RawArgs from .resolved_command import ResolvedCommand class CommandResolver(object): """ Returns the command to execute for the given console arguments. """ def resolve( self, args, application ): # type: (RawArgs, Application) -> ResolvedCommand raise NotImplementedError() clikit-0.6.2/src/clikit/api/resolver/exceptions.py000066400000000000000000000014131366776574300222400ustar00rootroot00000000000000from clikit.api.exceptions import CliKitException from clikit.utils.command import find_similar_command_names class CannotResolveCommandException(RuntimeError, CliKitException): @classmethod def name_not_found(cls, name, commands): message = 'The command "{}" is not defined.'.format(name) suggested_names = find_similar_command_names(name, commands) if suggested_names: if len(suggested_names) == 1: message += "\n\nDid you mean this?\n " else: message += "\n\nDid you mean one of these?\n " message += "\n ".join(suggested_names) return cls(message) @classmethod def no_default_command(cls): return cls("No default command is defined.") clikit-0.6.2/src/clikit/api/resolver/resolved_command.py000066400000000000000000000006771366776574300234130ustar00rootroot00000000000000from clikit.api.args import Args from clikit.api.command import Command class ResolvedCommand(object): """ A resolved command. """ def __init__(self, command, args): # type: (Command, Args) -> None self._command = command self._args = args @property def command(self): # type: () -> Command return self._command @property def args(self): # type: () -> Args return self._args clikit-0.6.2/src/clikit/args/000077500000000000000000000000001366776574300160305ustar00rootroot00000000000000clikit-0.6.2/src/clikit/args/__init__.py000066400000000000000000000001671366776574300201450ustar00rootroot00000000000000from .argv_args import ArgvArgs from .default_args_parser import DefaultArgsParser from .string_args import StringArgs clikit-0.6.2/src/clikit/args/argv_args.py000066400000000000000000000023561366776574300203630ustar00rootroot00000000000000import itertools import sys from typing import List from typing import Optional from clikit.api.args.raw_args import RawArgs class ArgvArgs(RawArgs): """ Console arguments passed via sys.argv. """ def __init__(self, argv=None): # type: (Optional[List[str]]) -> None if argv is None: argv = list(sys.argv) argv = argv[:] self._script_name = argv.pop(0) self._tokens = argv self._option_tokens = list( itertools.takewhile(lambda arg: arg != "--", self.tokens) ) @property def script_name(self): # type: () -> str return self._script_name @property def tokens(self): # type: () -> List[str] return self._tokens @property def option_tokens(self): # type: () -> List[str] return self._option_tokens def has_token(self, token): # type: (str) -> bool return token in self._tokens def has_option_token(self, token): # type: (str) -> bool return token in self._option_tokens def to_string(self, script_name=True): # type: (bool) -> str string = " ".join(self._tokens) if script_name: string = self._script_name.lstrip() + " " + string return string clikit-0.6.2/src/clikit/args/default_args_parser.py000066400000000000000000000267131366776574300224270ustar00rootroot00000000000000from typing import Dict from typing import List from typing import Optional from clikit.api.args.args import Args from clikit.api.args.args_parser import ArgsParser from clikit.api.args.format import ArgsFormat from clikit.api.args.format import Argument from clikit.api.args.format import CommandName from clikit.api.args.exceptions import CannotParseArgsException from clikit.api.args.exceptions import NoSuchOptionException from clikit.api.args.raw_args import RawArgs from clikit.utils._compat import OrderedDict class DefaultArgsParser(ArgsParser): """ Default parser for RawArgs instances. """ def __init__(self): # type: () -> None self._arguments = OrderedDict() self._options = OrderedDict() def parse( self, args, fmt, lenient=False ): # type: (RawArgs, ArgsFormat, bool) -> Args self._arguments = OrderedDict() arguments = OrderedDict() command_names = OrderedDict() i = 1 for j, command_name in enumerate(fmt.get_command_names()): arg_name = "cmd{}{}".format(j + 1, i) while fmt.has_argument(arg_name): i += 1 arg_name = "cmd{}{}".format(j + 1, i) arguments[arg_name] = Argument(arg_name, Argument.REQUIRED) command_names[arg_name] = command_name arguments.update(fmt.get_arguments()) _fmt = ArgsFormat( fmt.get_command_names() + list(arguments.values()) + list(fmt.get_options().values()) ) try: self._parse(args, _fmt, lenient) except (CannotParseArgsException, NoSuchOptionException): if not lenient: raise self._insert_missing_command_names(arguments, command_names, lenient) # Validate missing_arguments = [ arg.name for arg in _fmt.get_arguments().values() if arg.name not in self._arguments and arg.is_required() ] if missing_arguments and not lenient: raise CannotParseArgsException( 'Not enough arguments (missing: "{}").'.format( ", ".join(missing_arguments) ) ) parsed_args = Args(fmt, args) for name, value in self._arguments.items(): if fmt.has_argument(name): parsed_args.set_argument(name, value) for name, value in self._options.items(): if fmt.has_option(name): parsed_args.set_option(name, value) return parsed_args def _parse( self, raw_args, fmt, lenient ): # type: (RawArgs, ArgsFormat, bool) -> None tokens = raw_args.tokens[:] parse_options = True while True: try: token = tokens.pop(0) except IndexError: break if parse_options and token == "": self._parse_argument(token, fmt, lenient) elif parse_options and token == "--": parse_options = False elif parse_options and token.find("--") == 0: self._parse_long_option(token, tokens, fmt, lenient) elif parse_options and token[0] == "-" and token != "-": self._parse_short_option(token, tokens, fmt, lenient) else: self._parse_argument(token, fmt, lenient) def _insert_missing_command_names( self, arguments, command_names, lenient=False ): # type: (Dict[str, Argument], Dict[str, CommandName], bool) -> None fixed_values = {} actual_values = self._flatten(self._arguments.values()) command_names_iterator = iter(command_names.values()) actual_values_iterator = iter(actual_values) arguments_iterator = iter(arguments.values()) actual_value, command_name = self._skip_command_names( actual_values_iterator, command_names_iterator, arguments_iterator ) _, argument = self._copy_argument_values( command_names_iterator, command_name, arguments_iterator, None, fixed_values, lenient, ) self._copy_argument_values( actual_values_iterator, actual_value, arguments_iterator, argument, fixed_values, lenient, ) for name, value in fixed_values.items(): self._arguments[name] = value def _skip_command_names(self, actual_values, command_names, arguments): arg = next(actual_values, None) command_name = next(command_names, None) while arg and command_name and command_name.match(arg): arg = next(actual_values, None) command_name = next(command_names, None) next(arguments, None) return arg, command_name def _copy_argument_values( self, actual_values, value, arguments, argument, fixed_values, lenient ): if value is None: value = next(actual_values, None) if argument is None: argument = next(arguments, None) while value is not None: if argument is None: if lenient: return raise CannotParseArgsException.too_many_arguments() name = argument.name # Append the value to multi-valued arguments if argument.is_multi_valued(): if name not in fixed_values: fixed_values[name] = [] fixed_values[name].append(value) # The multi-valued argument is the last one, so we don't # need to advance the array pointer anymore. else: fixed_values[name] = value argument = next(arguments, None) value = next(actual_values, None) return value, argument def _flatten(self, arguments, result=None): if result is None: result = [] for value in arguments: if isinstance(value, list): self._flatten(value, result) else: result.append(value) return result def _parse_argument( self, token, fmt, lenient ): # type: (str, ArgsFormat, bool) -> None c = len(self._arguments) # if input is expecting another argument, add it if fmt.has_argument(c): arg = fmt.get_argument(c) if arg.is_multi_valued(): if arg.name not in self._arguments: self._arguments[arg.name] = [] self._arguments[arg.name].append(token) else: self._arguments[arg.name] = token elif fmt.has_argument(c - 1) and fmt.get_argument(c - 1).is_multi_valued(): arg = fmt.get_argument(c - 1) if arg.name not in self._arguments: self._arguments[arg.name] = [] self._arguments[arg.name].append(token) # unexpected argument else: if not lenient: raise CannotParseArgsException.too_many_arguments() def _parse_long_option( self, token, tokens, fmt, lenient ): # type: (str, List[str], ArgsFormat, bool) -> None name = token[2:] pos = name.find("=") if pos != -1: self._add_long_option(name[:pos], name[pos + 1 :], tokens, fmt, lenient) else: if fmt.has_option(name) and fmt.get_option(name).accepts_value(): try: value = tokens.pop(0) except IndexError: value = None if value and value.startswith("-"): tokens.insert(0, value) value = None self._add_long_option(name, value, tokens, fmt, lenient) else: self._add_long_option(name, None, tokens, fmt, lenient) def _parse_short_option( self, token, tokens, fmt, lenient ): # type: (str, List[str], ArgsFormat, bool) -> None name = token[1:] if len(name) > 1: if fmt.has_option(name[0]) and fmt.get_option(name[0]).accepts_value(): # an option with a value (with no space) self._add_short_option(name[0], name[1:], tokens, fmt, lenient) else: self._parse_short_option_set(name, tokens, fmt, lenient) else: if fmt.has_option(name[0]) and fmt.get_option(name[0]).accepts_value(): try: value = tokens.pop(0) except IndexError: value = None if value and value.startswith("-"): tokens.insert(0, value) value = None self._add_short_option(name, value, tokens, fmt, lenient) else: self._add_short_option(name, None, tokens, fmt, lenient) def _parse_short_option_set( self, name, tokens, fmt, lenient ): # type: (str, List[str], ArgsFormat, bool) -> None l = len(name) for i in range(0, l): if not fmt.has_option(name[i]): raise NoSuchOptionException(name[i]) option = fmt.get_option(name[i]) if option.accepts_value(): self._add_long_option( option.long_name, None if l - 1 == i else name[i + 1 :], tokens, fmt, lenient, ) break else: self._add_long_option(option.long_name, None, tokens, fmt, lenient) def _add_long_option( self, name, value, tokens, fmt, lenient ): # type: (str, Optional[str], List[str], ArgsFormat, bool) -> None if not fmt.has_option(name): raise NoSuchOptionException(name) option = fmt.get_option(name) if value is False: value = None if value is not None and not option.accepts_value(): raise CannotParseArgsException.option_does_not_accept_value(name) if value is None and option.accepts_value() and len(tokens): # if option accepts an optional or mandatory argument # let's see if there is one provided try: nxt = tokens.pop(0) except IndexError: nxt = None if nxt and len(nxt) >= 1 and nxt[0] != "-": value = nxt elif not nxt: value = "" else: tokens.insert(0, nxt) # This test is here to handle cases like --foo= # and foo option value is optional if value == "": value = None if value is None: if option.is_value_required(): raise CannotParseArgsException.option_requires_value(name) if not option.is_multi_valued(): value = option.default if option.is_value_optional() else True if option.is_multi_valued(): if name not in self._options: self._options[name] = [] self._options[name].append(value) else: self._options[name] = value def _add_short_option( self, name, value, tokens, fmt, lenient ): # type: (str, Optional[str], List[str], ArgsFormat, bool) -> None if not fmt.has_option(name): raise NoSuchOptionException(name) self._add_long_option( fmt.get_option(name).long_name, value, tokens, fmt, lenient ) clikit-0.6.2/src/clikit/args/inputs/000077500000000000000000000000001366776574300173525ustar00rootroot00000000000000clikit-0.6.2/src/clikit/args/inputs/__init__.py000066400000000000000000000000001366776574300214510ustar00rootroot00000000000000clikit-0.6.2/src/clikit/args/string_args.py000066400000000000000000000020531366776574300207240ustar00rootroot00000000000000import itertools from typing import List from typing import Optional from clikit.api.args import RawArgs from .token_parser import TokenParser class StringArgs(RawArgs): """ Console arguments passed as a string. """ def __init__(self, string): # type: (str) -> None parser = TokenParser() self._tokens = parser.parse(string) self._option_tokens = list( itertools.takewhile(lambda arg: arg != "--", self.tokens) ) @property def script_name(self): # type: () -> Optional[str] return @property def tokens(self): # type: () -> List[str] return self._tokens @property def option_tokens(self): # type: () -> List[str] return self._option_tokens def has_token(self, token): # type: (str) -> bool return token in self._tokens def has_option_token(self, token): # type: (str) -> bool return token in self._option_tokens def to_string(self, script_name=True): # type: (bool) -> str return " ".join(self._tokens) clikit-0.6.2/src/clikit/args/token_parser.py000066400000000000000000000057271366776574300211110ustar00rootroot00000000000000from typing import List from typing import Optional class TokenParser(object): """ Parses tokens from a string passed to StringArgs. """ def __init__(self): # type: () -> None self._string = "" # type: str self._cursor = 0 # type: int self._current = None # type: Optional[str] self._next_ = None # type: Optional[str] def parse(self, string): # type: (str) -> List[str] self._string = string self._cursor = 0 self._current = None if len(string) > 0: self._current = string[0] self._next_ = None if len(string) > 1: self._next_ = string[1] tokens = self._parse() return tokens def _parse(self): # type: () -> List[str] tokens = [] while self._is_valid(): if self._current.isspace(): # Skip spaces self._next() continue if self._is_valid(): tokens.append(self._parse_token()) return tokens def _is_valid(self): # type: () -> bool return self._current is not None def _next(self): # type: () -> None """ Advances the cursor to the next position. """ if not self._is_valid(): return self._cursor += 1 self._current = self._next_ if self._cursor + 1 < len(self._string): self._next_ = self._string[self._cursor + 1] else: self._next_ = None def _parse_token(self): # type: () -> str token = "" while self._is_valid(): if self._current.isspace(): self._next() break if self._current == "\\": token += self._parse_escape_sequence() elif self._current in ["'", '"']: token += self._parse_quoted_string() else: token += self._current self._next() return token def _parse_quoted_string(self): # type: () -> str string = "" delimiter = self._current # Skip first delimiter self._next() while self._is_valid(): if self._current == delimiter: # Skip last delimiter self._next() break if self._current == "\\": string += self._parse_escape_sequence() elif self._current == '"': string += '"{}"'.format(self._parse_quoted_string()) elif self._current == "'": string += "'{}'".format(self._parse_quoted_string()) else: string += self._current self._next() return string def _parse_escape_sequence(self): # type: () -> str if self._next_ in ['"', "'"]: sequence = self._next_ else: sequence = "\\" + self._next_ self._next() self._next() return sequence clikit-0.6.2/src/clikit/config/000077500000000000000000000000001366776574300163415ustar00rootroot00000000000000clikit-0.6.2/src/clikit/config/__init__.py000066400000000000000000000001011366776574300204420ustar00rootroot00000000000000from .default_application_config import DefaultApplicationConfig clikit-0.6.2/src/clikit/config/default_application_config.py000066400000000000000000000133621366776574300242540ustar00rootroot00000000000000from clikit.api.config.application_config import ApplicationConfig from clikit.api.args.raw_args import RawArgs from clikit.api.args.format.argument import Argument from clikit.api.args.format.option import Option from clikit.api.event import PRE_HANDLE from clikit.api.event import PRE_RESOLVE from clikit.api.event import EventDispatcher from clikit.api.event import PreHandleEvent from clikit.api.event import PreResolveEvent from clikit.api.io import IO from clikit.api.io import Input from clikit.api.io import InputStream from clikit.api.io import Output from clikit.api.io import OutputStream from clikit.api.io.flags import DEBUG from clikit.api.io.flags import VERBOSE from clikit.api.io.flags import VERY_VERBOSE from clikit.api.resolver import ResolvedCommand from clikit.formatter import AnsiFormatter from clikit.formatter import DefaultStyleSet from clikit.formatter import PlainFormatter from clikit.handler.help import HelpTextHandler from clikit.io import ConsoleIO from clikit.io.input_stream import StandardInputStream from clikit.io.output_stream import ErrorOutputStream from clikit.io.output_stream import StandardOutputStream from clikit.resolver.default_resolver import DefaultResolver from clikit.resolver.help_resolver import HelpResolver from clikit.ui.components import NameVersion class DefaultApplicationConfig(ApplicationConfig): """ The default application configuration. """ def configure(self): self.set_io_factory(self.create_io) self.add_event_listener(PRE_RESOLVE, self.resolve_help_command) self.add_event_listener(PRE_HANDLE, self.print_version) self.add_option("help", "h", Option.NO_VALUE, "Display this help message") self.add_option("quiet", "q", Option.NO_VALUE, "Do not output any message") self.add_option( "verbose", "v", Option.OPTIONAL_VALUE, "Increase the verbosity of messages: " '"-v" for normal output, ' '"-vv" for more verbose output ' 'and "-vvv" for debug', ) self.add_option( "version", "V", Option.NO_VALUE, "Display this application version" ) self.add_option("ansi", None, Option.NO_VALUE, "Force ANSI output") self.add_option("no-ansi", None, Option.NO_VALUE, "Disable ANSI output") self.add_option( "no-interaction", "n", Option.NO_VALUE, "Do not ask any interactive question", ) with self.command("help") as c: c.default() c.set_description("Display the manual of a command") c.add_argument( "command", Argument.OPTIONAL | Argument.MULTI_VALUED, "The command name" ) c.set_handler(HelpTextHandler(HelpResolver())) def create_io( self, application, args, input_stream=None, output_stream=None, error_stream=None, ): # type: (Application, RawArgs, InputStream, OutputStream, OutputStream) -> IO if input_stream is None: input_stream = StandardInputStream() if output_stream is None: output_stream = StandardOutputStream() if error_stream is None: error_stream = ErrorOutputStream() style_set = application.config.style_set if args.has_option_token("--no-ansi"): output_formatter = error_formatter = PlainFormatter(style_set) elif args.has_option_token("--ansi"): output_formatter = error_formatter = AnsiFormatter(style_set, True) else: if output_stream.supports_ansi(): output_formatter = AnsiFormatter(style_set) else: output_formatter = PlainFormatter(style_set) if error_stream.supports_ansi(): error_formatter = AnsiFormatter(style_set) else: error_formatter = PlainFormatter(style_set) io = self.io_class( Input(input_stream), Output(output_stream, output_formatter), Output(error_stream, error_formatter), ) if args.has_option_token("-vvv") or self.is_debug(): io.set_verbosity(DEBUG) elif args.has_option_token("-vv"): io.set_verbosity(VERY_VERBOSE) elif args.has_option_token("-v"): io.set_verbosity(VERBOSE) if args.has_option_token("--quiet") or args.has_option_token("-q"): io.set_quiet(True) if args.has_option_token("--no-interaction") or args.has_option_token("-n"): io.set_interactive(False) return io @property def io_class(self): # type: () -> IO.__class__ return ConsoleIO @property def default_style_set(self): # type: () -> DefaultStyleSet return DefaultStyleSet() @property def default_command_resolver(self): # type: () -> DefaultResolver return DefaultResolver() def resolve_help_command( self, event, event_name, dispatcher ): # type: (PreResolveEvent, str, EventDispatcher) -> None args = event.raw_args application = event.application if args.has_option_token("-h") or args.has_option_token("--help"): command = application.get_command("help") # Enable lenient parsing parsed_args = command.parse(args, True) event.set_resolved_command(ResolvedCommand(command, parsed_args)) event.stop_propagation() def print_version( self, event, event_name, dispatcher ): # type: (PreHandleEvent, str, EventDispatcher) -> None if event.args.is_option_set("version"): version = NameVersion(event.command.application.config) version.render(event.io) event.handled(True) clikit-0.6.2/src/clikit/console_application.py000066400000000000000000000135601366776574300215000ustar00rootroot00000000000000import sys from .api.application import Application as BaseApplication from .api.args.raw_args import RawArgs from .api.args.format.args_format import ArgsFormat from .api.command import Command from .api.command import CommandCollection from .api.command.exceptions import CannotAddCommandException from .api.config.application_config import ApplicationConfig from .api.config.command_config import CommandConfig from .api.event import CONFIG from .api.event import PRE_RESOLVE from .api.event import ConfigEvent from .api.event import PreResolveEvent from .api.exceptions import CliKitException from .api.io import IO from .api.io import InputStream from .api.io import OutputStream from .api.io.flags import VERY_VERBOSE from .api.resolver.resolved_command import ResolvedCommand from .args.argv_args import ArgvArgs from .io import ConsoleIO from .ui.components.exception_trace import ExceptionTrace class ConsoleApplication(BaseApplication): """ A console application """ def __init__(self, config): # type: (ApplicationConfig) -> None self._preliminary_io = ConsoleIO() # Enable trace output for exceptions thrown during boot self._preliminary_io.set_verbosity(VERY_VERBOSE) self._dispatcher = None try: dispatcher = config.dispatcher if dispatcher and dispatcher.has_listeners(CONFIG): dispatcher.dispatch(CONFIG, ConfigEvent(config)) self._config = config self._dispatcher = config.dispatcher self._commands = CommandCollection() self._named_commands = CommandCollection() self._default_commands = CommandCollection() self._global_args_format = ArgsFormat( list(config.arguments.values()) + list(config.options.values()) ) for command_config in config.command_configs: self.add_command(command_config) except Exception as e: if not config.is_exception_caught(): raise # Render the trace to the preliminary IO trace = ExceptionTrace(e) trace.render(self._preliminary_io) # Ignore is_terminated_after_run() setting. This is a fatal error. sys.exit(self.exception_to_exit_code(e)) @property def config(self): # type: () -> ApplicationConfig return self._config @property def global_args_format(self): # type: () -> ArgsFormat return self._global_args_format def get_command(self, name): # type: (str) -> Command return self._commands.get(name) @property def commands(self): # type: () -> CommandCollection return self._commands def has_command(self, name): # type: (str) -> bool return name in self._commands def has_commands(self): # type: () -> bool return not self._commands.is_empty() @property def named_commands(self): # type: () -> CommandCollection return self._named_commands def has_named_commands(self): # type: () -> bool return not self._named_commands.is_empty() @property def default_commands(self): # type: () -> CommandCollection return self._default_commands def has_default_commands(self): # type: () -> bool return not self._default_commands.is_empty() def resolve_command(self, args): # type: (RawArgs) -> ResolvedCommand if self._dispatcher and self._dispatcher.has_listeners(PRE_RESOLVE): event = PreResolveEvent(args, self) self._dispatcher.dispatch(PRE_RESOLVE, event) resolved_command = event.resolved_command if resolved_command: return resolved_command return self._config.command_resolver.resolve(args, self) def run( self, args=None, input_stream=None, output_stream=None, error_stream=None ): # type: (RawArgs, InputStream, OutputStream, OutputStream) -> int # Render errors to the preliminary IO until the final IO is created io = self._preliminary_io try: if args is None: args = ArgvArgs() io_factory = self._config.io_factory io = io_factory( self, args, input_stream, output_stream, error_stream ) # type: IO resolved_command = self.resolve_command(args) command = resolved_command.command parsed_args = resolved_command.args status_code = command.handle(parsed_args, io) except KeyboardInterrupt: status_code = 1 except Exception as e: if not self._config.is_exception_caught(): raise trace = ExceptionTrace( e, solution_provider_repository=self._config.solution_provider_repository, ) trace.render(io, simple=isinstance(e, CliKitException)) status_code = self.exception_to_exit_code(e) if self._config.is_terminated_after_run(): sys.exit(status_code) return status_code def exception_to_exit_code(self, e): # type: (Exception) -> int if not hasattr(e, "code") or not isinstance(e, int): return 1 return min(max(e.code, 1), 255) def add_command(self, config): # type: (CommandConfig) -> None if not config.is_enabled(): return self._validate_command_name(config.name) command = Command(config, self) self._commands.add(command) if config.is_default(): self._default_commands.add(command) if not config.is_anonymous(): self._named_commands.add(command) def _validate_command_name(self, name): # type: (Optional[str]) -> None if not name: raise CannotAddCommandException.name_empty() if name in self._commands: raise CannotAddCommandException.name_exists(name) clikit-0.6.2/src/clikit/formatter/000077500000000000000000000000001366776574300170775ustar00rootroot00000000000000clikit-0.6.2/src/clikit/formatter/__init__.py000066400000000000000000000002571366776574300212140ustar00rootroot00000000000000from .ansi_formatter import AnsiFormatter from .default_style_set import DefaultStyleSet from .null_formatter import NullFormatter from .plain_formatter import PlainFormatter clikit-0.6.2/src/clikit/formatter/ansi_formatter.py000066400000000000000000000035141366776574300224710ustar00rootroot00000000000000from typing import Optional from pastel import Pastel from clikit.adapter.style_converter import StyleConverter from clikit.api.formatter import Formatter from clikit.api.formatter import Style from clikit.api.formatter import StyleSet from .default_style_set import DefaultStyleSet class AnsiFormatter(Formatter): """ A formatter that replaces style tags by ANSI format codes. """ def __init__(self, style_set=None, forced=False): # type: (StyleSet) -> None self._formatter = Pastel(True) self._forced = forced if style_set is None: style_set = DefaultStyleSet() for tag, style in style_set.styles.items(): pastel_style = StyleConverter.convert(style) self._formatter.add_style( tag, pastel_style.foreground, pastel_style.background, pastel_style.options, ) def format(self, string, style=None): # type: (str, Optional[Style]) -> str if style is not None: self._formatter._style_stack.push(StyleConverter.convert(style)) formatted = self._formatter.colorize(string) if style is not None: self._formatter._style_stack.pop() return formatted def remove_format(self, string): # type: (str) -> str with self._formatter.colorized(False): return self._formatter.colorize(string) def disable_ansi(self): # type: () -> bool return False def force_ansi(self): # type: () -> bool return self._forced def add_style(self, style): # type: (Style) -> None pastel_style = StyleConverter.convert(style) self._formatter.add_style( style.tag, pastel_style.foreground, pastel_style.background, pastel_style.options, ) clikit-0.6.2/src/clikit/formatter/default_style_set.py000066400000000000000000000011301366776574300231630ustar00rootroot00000000000000from clikit.api.formatter import Style from clikit.api.formatter import StyleSet class DefaultStyleSet(StyleSet): """ The Default TTY style set """ def __init__(self): # type: () -> None styles = [ Style("info").fg("green"), Style("comment").fg("cyan"), Style("question").fg("blue"), Style("error").fg("red").bold(), Style("b").bold(), Style("u").underlined(), Style("c1").fg("cyan"), Style("c2").fg("yellow"), ] super(DefaultStyleSet, self).__init__(styles) clikit-0.6.2/src/clikit/formatter/null_formatter.py000066400000000000000000000007601366776574300225110ustar00rootroot00000000000000from typing import Optional from clikit.api.formatter import Formatter class NullFormatter(Formatter): """ A formatter that returns all text unchanged. """ def format(self, string, style=None): # type: (str, Optional[Style]) -> str return string def remove_format(self, string): # type: (str) -> str return string def force_ansi(self): # type: () -> bool return False def add_style(self, style): # type: (Style) -> None pass clikit-0.6.2/src/clikit/formatter/plain_formatter.py000066400000000000000000000027751366776574300226520ustar00rootroot00000000000000from typing import Optional from pastel import Pastel from clikit.adapter.style_converter import StyleConverter from clikit.api.formatter import Formatter from clikit.api.formatter import Style from clikit.api.formatter import StyleSet from .default_style_set import DefaultStyleSet class PlainFormatter(Formatter): """ A formatter that removes all format tags. """ def __init__(self, style_set=None): # type: (StyleSet) -> None self._formatter = Pastel(False) if style_set is None: style_set = DefaultStyleSet() for tag, style in style_set.styles.items(): pastel_style = StyleConverter.convert(style) self._formatter.add_style( tag, pastel_style.foreground, pastel_style.background, pastel_style.options, ) def format(self, string, style=None): # type: (str, Optional[Style]) -> str return self._formatter.colorize(string) def remove_format(self, string): # type: (str) -> str return self._formatter.colorize(string) def disable_ansi(self): # type: () -> bool return True def force_ansi(self): # type: () -> bool return False def add_style(self, style): # type: (Style) -> None pastel_style = StyleConverter.convert(style) self._formatter.add_style( style.tag, pastel_style.foreground, pastel_style.background, pastel_style.options, ) clikit-0.6.2/src/clikit/handler/000077500000000000000000000000001366776574300165115ustar00rootroot00000000000000clikit-0.6.2/src/clikit/handler/__init__.py000066400000000000000000000000001366776574300206100ustar00rootroot00000000000000clikit-0.6.2/src/clikit/handler/callback_handler.py000066400000000000000000000006011366776574300223110ustar00rootroot00000000000000from typing import Callable from clikit.api.args import Args from clikit.api.io import IO class CallbackHandler: """ Delegates command handling to a callable. """ def __init__(self, callback): # type: (Callable) -> None self._calllback = callback def handle(self, args, io, _): # type: (Args, IO, ...) -> int return self._calllback(args, io) clikit-0.6.2/src/clikit/handler/help/000077500000000000000000000000001366776574300174415ustar00rootroot00000000000000clikit-0.6.2/src/clikit/handler/help/__init__.py000066400000000000000000000000571366776574300215540ustar00rootroot00000000000000from .help_text_handler import HelpTextHandler clikit-0.6.2/src/clikit/handler/help/help_handler.py000066400000000000000000000003561366776574300224440ustar00rootroot00000000000000from clikit.api.args.args import Args from clikit.api.io import IO class HelpHandler: """ Handler for the "help" command. """ def handle(self, args, io): # type: (Args, IO) -> int print(args) return 0 clikit-0.6.2/src/clikit/handler/help/help_text_handler.py000066400000000000000000000015361366776574300235110ustar00rootroot00000000000000from clikit.api.args import Args from clikit.api.command import Command from clikit.api.io import IO from clikit.api.resolver import CommandResolver from clikit.ui.help import ApplicationHelp from clikit.ui.help import CommandHelp class HelpTextHandler: """ Displays help as text format. """ def __init__(self, resolver): # type: (CommandResolver) -> None self._resolver = resolver def handle(self, args, io, command): # type: (Args, IO, Command) -> int application = command.application if args.is_argument_set("command"): resolved_command = self._resolver.resolve(args.raw_args, application) the_command = resolved_command.command usage = CommandHelp(the_command) else: usage = ApplicationHelp(application) usage.render(io) return 0 clikit-0.6.2/src/clikit/io/000077500000000000000000000000001366776574300155035ustar00rootroot00000000000000clikit-0.6.2/src/clikit/io/__init__.py000066400000000000000000000001421366776574300176110ustar00rootroot00000000000000from .buffered_io import BufferedIO from .console_io import ConsoleIO from .null_io import NullIO clikit-0.6.2/src/clikit/io/buffered_io.py000066400000000000000000000031761366776574300203350ustar00rootroot00000000000000from typing import Optional from clikit.api.formatter import Formatter from clikit.api.io import IO from clikit.api.io import Input from clikit.api.io import Output from clikit.formatter import PlainFormatter from .input_stream import StringInputStream from .output_stream import BufferedOutputStream class BufferedIO(IO): """ An I/O that reads from and writes to a buffer. """ def __init__( self, input_data="", formatter=None ): # type: (str, Optional[Formatter]) -> None if formatter is None: formatter = PlainFormatter() input = Input(StringInputStream(input_data)) output = Output(BufferedOutputStream(), formatter) error_output = Output(BufferedOutputStream(), formatter) super(BufferedIO, self).__init__(input, output, error_output) def set_input(self, data): # type: (str) -> None self.input.stream.set(data) def append_input(self, data): # type: (str) -> None self.input.stream.append(data) def clear_input(self): # type: () -> None self.input.stream.clear() def fetch_output(self): # type: () -> str return self.output.stream.fetch() def clear_output(self): # type: () -> None self.output.stream.clear() def fetch_error(self): # type: () -> str return self.error_output.stream.fetch() def clear_error(self): # type: () -> None self.error_output.stream.clear() def section(self): io = self.__class__() io._input = self._input io._output = self._output.section() io._error_output = self._error_output.section() return io clikit-0.6.2/src/clikit/io/console_io.py000066400000000000000000000031051366776574300202050ustar00rootroot00000000000000from typing import Optional from clikit.api.io import IO from clikit.api.io import Input from clikit.api.io import Output from clikit.formatter import AnsiFormatter from clikit.formatter import PlainFormatter from clikit.utils.terminal import Terminal from clikit.ui.rectangle import Rectangle from .input_stream import StandardInputStream from .output_stream import ErrorOutputStream from .output_stream import StandardOutputStream class ConsoleIO(IO): """ An I/O that reads from/prints to the console. """ def __init__( self, input=None, output=None, error_output=None ): # type: (Optional[Input], Optional[Output], Optional[Output]) -> None if input is None: input_stream = StandardInputStream() input = Input(input_stream) if output is None: output_stream = StandardOutputStream() if output_stream.supports_ansi(): formatter = AnsiFormatter() else: formatter = PlainFormatter() output = Output(output_stream, formatter) if error_output is None: error_stream = ErrorOutputStream() if error_stream.supports_ansi(): formatter = AnsiFormatter() else: formatter = PlainFormatter() error_output = Output(error_stream, formatter) super(ConsoleIO, self).__init__(input, output, error_output) def get_default_terminal_dimensions(self): # type: () -> Rectangle terminal = Terminal() return Rectangle(terminal.width, terminal.height) clikit-0.6.2/src/clikit/io/input_stream/000077500000000000000000000000001366776574300202155ustar00rootroot00000000000000clikit-0.6.2/src/clikit/io/input_stream/__init__.py000066400000000000000000000003141366776574300223240ustar00rootroot00000000000000from .null_input_stream import NullInputStream from .standard_input_stream import StandardInputStream from .stream_input_stream import StreamInputStream from .string_input_stream import StringInputStream clikit-0.6.2/src/clikit/io/input_stream/null_input_stream.py000066400000000000000000000013151366776574300243330ustar00rootroot00000000000000from typing import Optional from clikit.api.io.input_stream import InputStream class NullInputStream(InputStream): """ An input stream that returns nothing. """ def read(self, length): # type: (int) -> str """ Reads the given amount of characters from the stream. """ return "" def read_line(self, length=None): # type: (Optional[int]) -> str """ Reads a line from the stream. """ return "" def close(self): # type: () -> None """ Closes the stream """ def is_closed(self): # type: () -> bool """ Returns whether the stream is closed or not """ return False clikit-0.6.2/src/clikit/io/input_stream/standard_input_stream.py000066400000000000000000000004411366776574300251600ustar00rootroot00000000000000import sys from .stream_input_stream import StreamInputStream class StandardInputStream(StreamInputStream): """ An input stream that reads from the standard input. """ def __init__(self): # type: () -> None super(StandardInputStream, self).__init__(sys.stdin) clikit-0.6.2/src/clikit/io/input_stream/stream_input_stream.py000066400000000000000000000026251366776574300246610ustar00rootroot00000000000000import io from typing import Optional from clikit.api.io.input_stream import InputStream class StreamInputStream(InputStream): """ An input stream that reads from a stream. """ def __init__(self, stream): # type: (io.TextIOWrapper) -> None self._stream = stream if hasattr("stream", "seekable") and stream.seekable(): stream.seek(0) def read(self, length): # type: (int) -> str """ Reads the given amount of characters from the stream. """ if self.is_closed(): raise io.UnsupportedOperation("Cannot read from a closed input.") try: data = self._stream.read(length) except EOFError: return "" if data: return data return "" def read_line(self, length=None): # type: (Optional[int]) -> str """ Reads a line from the stream. """ if self.is_closed(): raise io.UnsupportedOperation("Cannot read from a closed input.") try: return self._stream.readline(length) or "" except EOFError: return "" def close(self): # type: () -> None """ Closes the stream """ self._stream.close() def is_closed(self): # type: () -> bool """ Returns whether the stream is closed or not """ return self._stream.closed clikit-0.6.2/src/clikit/io/input_stream/string_input_stream.py000066400000000000000000000015571366776574300246770ustar00rootroot00000000000000from io import BytesIO from io import SEEK_END from clikit.utils._compat import encode from .stream_input_stream import StreamInputStream class StringInputStream(StreamInputStream): """ An input stream that reads from a string. """ def __init__(self, string=""): # type: (str) -> None self._stream = BytesIO() super(StringInputStream, self).__init__(self._stream) self.set(string) def clear(self): # type: () -> None self._stream.truncate(0) self._stream.seek(0) def set(self, string): # type: (str) -> None self.clear() self._stream.write(encode(string)) self._stream.seek(0) def append(self, string): # type: (str) -> None pos = self._stream.tell() self._stream.seek(0, SEEK_END) self._stream.write(encode(string)) self._stream.seek(pos) clikit-0.6.2/src/clikit/io/null_io.py000066400000000000000000000007561366776574300175260ustar00rootroot00000000000000from clikit.api.io import IO from clikit.api.io import Input from clikit.api.io import Output from .input_stream import NullInputStream from .output_stream import NullOutputStream class NullIO(IO): """ An I/O that does nothing. """ def __init__(self): # type: () -> None input = Input(NullInputStream()) output = Output(NullOutputStream()) error_output = Output(NullOutputStream()) super(NullIO, self).__init__(input, output, error_output) clikit-0.6.2/src/clikit/io/output_stream/000077500000000000000000000000001366776574300204165ustar00rootroot00000000000000clikit-0.6.2/src/clikit/io/output_stream/__init__.py000066400000000000000000000004131366776574300225250ustar00rootroot00000000000000from .buffered_output_stream import BufferedOutputStream from .error_output_stream import ErrorOutputStream from .null_output_stream import NullOutputStream from .standard_output_stream import StandardOutputStream from .stream_output_stream import StreamOutputStream clikit-0.6.2/src/clikit/io/output_stream/buffered_output_stream.py000066400000000000000000000024431366776574300255500ustar00rootroot00000000000000from clikit.api.io import OutputStream from clikit.utils._compat import decode class BufferedOutputStream(OutputStream): """ An output stream that writes to a buffer. """ def __init__(self): # type: () -> None self._buffer = "" self._closed = False def fetch(self): # type: () -> str return self._buffer def clear(self): # type: () -> None self._buffer = "" def write(self, string): # type: (str) -> None """ Writes a string to the stream. """ if self._closed: raise IOError("Cannot read from a closed input.") self._buffer += decode(string) def flush(self): # type: () -> None """ Flushes the stream and forces all pending text to be written out. """ if self._closed: raise IOError("Cannot read from a closed input.") def supports_ansi(self): # type: () -> bool """ Returns whether the stream supports ANSI format codes. """ return False def close(self): # type: () -> None """ Closes the stream. """ self._closed = True def is_closed(self): # type: () -> bool """ Returns whether the stream is closed. """ return self._closed clikit-0.6.2/src/clikit/io/output_stream/error_output_stream.py000066400000000000000000000004371366776574300251200ustar00rootroot00000000000000import sys from .stream_output_stream import StreamOutputStream class ErrorOutputStream(StreamOutputStream): """ An output stream that writes to the error output. """ def __init__(self): # type: () -> None super(ErrorOutputStream, self).__init__(sys.stderr) clikit-0.6.2/src/clikit/io/output_stream/null_output_stream.py000066400000000000000000000014371366776574300247420ustar00rootroot00000000000000from clikit.api.io.output_stream import OutputStream class NullOutputStream(OutputStream): """ An output stream that ignores all output. """ def write(self, string): # type: (str) -> None """ Writes a string to the stream. """ def flush(self): # type: () -> None """ Flushes the stream and forces all pending text to be written out. """ def supports_ansi(self): # type: () -> bool """ Returns whether the stream supports ANSI format codes. """ return False def close(self): # type: () -> None """ Closes the stream. """ def is_closed(self): # type: () -> bool """ Returns whether the stream is closed. """ return False clikit-0.6.2/src/clikit/io/output_stream/standard_output_stream.py000066400000000000000000000004501366776574300255620ustar00rootroot00000000000000import sys from .stream_output_stream import StreamOutputStream class StandardOutputStream(StreamOutputStream): """ An output stream that writes to the standard output. """ def __init__(self): # type: () -> None super(StandardOutputStream, self).__init__(sys.stdout) clikit-0.6.2/src/clikit/io/output_stream/stream_output_stream.py000066400000000000000000000063621366776574300252650ustar00rootroot00000000000000import io import os import platform import sys from clikit.api.io.output_stream import OutputStream class StreamOutputStream(OutputStream): """ An output stream that writes to a stream. """ def __init__(self, stream): # type: (io.TextIOWrapper) -> None self._stream = stream def write(self, string): # type: (str) -> None """ Writes a string to the stream. """ if self.is_closed(): raise io.UnsupportedOperation("Cannot write to a closed input.") self._stream.write(string) self._stream.flush() def flush(self): # type: () -> None """ Flushes the stream and forces all pending text to be written out. """ if self.is_closed(): raise io.UnsupportedOperation("Cannot write to a closed input.") self._stream.flush() def supports_ansi(self): # type: () -> bool """ Returns whether the stream supports ANSI format codes. """ if platform.system().lower() == "windows": shell_supported = ( os.getenv("ANSICON") is not None or "ON" == os.getenv("ConEmuANSI") or "xterm" == os.getenv("Term") ) if shell_supported: return True if not hasattr(self._stream, "fileno"): return False # Checking for Windows version # If we have a compatible version # activate color support windows_version = sys.getwindowsversion() major, build = windows_version[0], windows_version[2] if (major, build) < (10, 14393): return False # Activate colors if possible import ctypes import ctypes.wintypes FILE_TYPE_CHAR = 0x0002 FILE_TYPE_REMOTE = 0x8000 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 kernel32 = ctypes.windll.kernel32 fileno = self._stream.fileno() if fileno == 1: h = kernel32.GetStdHandle(-11) elif fileno == 2: h = kernel32.GetStdHandle(-12) else: return False if h is None or h == ctypes.wintypes.HANDLE(-1): return False if (kernel32.GetFileType(h) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR: return False mode = ctypes.wintypes.DWORD() if not kernel32.GetConsoleMode(h, ctypes.byref(mode)): return False if (mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0: kernel32.SetConsoleMode( h, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING ) return True return False if not hasattr(self._stream, "fileno"): return False try: return os.isatty(self._stream.fileno()) except io.UnsupportedOperation: return False def close(self): # type: () -> None """ Closes the stream. """ self._stream.close() def is_closed(self): # type: () -> bool """ Returns whether the stream is closed. """ return self._stream.closed clikit-0.6.2/src/clikit/resolver/000077500000000000000000000000001366776574300167355ustar00rootroot00000000000000clikit-0.6.2/src/clikit/resolver/__init__.py000066400000000000000000000000561366776574300210470ustar00rootroot00000000000000from .default_resolver import DefaultResolver clikit-0.6.2/src/clikit/resolver/default_resolver.py000066400000000000000000000130641366776574300226600ustar00rootroot00000000000000from typing import Iterator from typing import List from typing import Optional from clikit.api.args import RawArgs from clikit.api.command import Command from clikit.api.command import CommandCollection from clikit.api.resolver import CommandResolver from clikit.api.resolver import ResolvedCommand from clikit.api.resolver.exceptions import CannotResolveCommandException from .resolve_result import ResolveResult class DefaultResolver(CommandResolver): """ Parses the raw console arguments for the command to execute. """ def resolve( self, args, application ): # type: (RawArgs, Application) -> ResolvedCommand tokens = args.tokens named_commands = application.named_commands tokens = iter(tokens) arguments_to_test = self.get_arguments_to_test(tokens) options_to_test = self.get_options_to_test(tokens) result = self.process_arguments( args, named_commands, arguments_to_test, options_to_test ) if result: return self.create_resolved_command(result) # Try to find a command for the passed arguments and options. if arguments_to_test: raise CannotResolveCommandException.name_not_found( arguments_to_test[0], named_commands ) # If no arguments were passed, run the application's default command. result = self.process_default_commands(args, application.default_commands) if result: return self.create_resolved_command(result) raise CannotResolveCommandException.no_default_command() def process_arguments( self, args, named_commands, arguments_to_test, options_to_test ): # type: (RawArgs, CommandCollection, List[str], List[str]) -> Optional[ResolveResult] current_command = None # Parse the arguments for command names until we fail to find a # matching command for name in arguments_to_test: if name not in named_commands: break next_command = named_commands.get(name) current_command = next_command named_commands = current_command.named_sub_commands if not current_command: return return self.process_options(args, current_command, options_to_test) def process_options( self, args, current_command, options_to_test ): # type: (RawArgs, Command, List[str]) -> Optional[ResolveResult] for option in options_to_test: commands = current_command.named_sub_commands if option not in commands: continue next_command = commands.get(option) return self.process_default_sub_commands(args, current_command) def process_default_sub_commands( self, args, current_command ): # type: (RawArgs, Command) -> Optional[ResolveResult] result = self.process_default_commands( args, current_command.default_sub_commands ) if result: return result # No default commands, return the current command return ResolveResult(current_command, args) def process_default_commands( self, args, default_commands ): # type: (RawArgs, CommandCollection) -> Optional[ResolveResult] first_result = None for default_command in default_commands: resolved_command = ResolveResult(default_command, args) if resolved_command.is_parsable(): return resolved_command if not first_result: first_result = resolved_command # Return the first default command if one was found return first_result def get_arguments_to_test(self, tokens): # type: (Iterator[str]) -> List[str] arguments_to_test = [] token = next(tokens, None) while token: # "--" stops argument parsing if token == "--": break # Stop argument parsing when we reach the first option. # Command names must be passed before any option. The reason # is that we cannot determine whether an argument after an # option is the value of that option or an argument by itself # without getting the input definition of the corresponding # command first. # For example, in the command "server -f add" we don't know # whether "add" is the value of the "-f" option or an argument. # Hence we stop argument parsing after "-f" and assume that # "server" (or "server -f") is the command to execute. if token[:1] and token[0] == "-": break arguments_to_test.append(token) token = next(tokens, None) return arguments_to_test def get_options_to_test(self, tokens): # type: (Iterator[str]) -> List[str] options_to_test = [] token = next(tokens, None) while token: # "--" stops option parsing if token == "--": break if token[:1] and token[0] == "-": if token[:2] == "--" and len(token) > 2: options_to_test.append(token[2:]) elif len(token) == 2: options_to_test.append(token[1:]) token = next(tokens, None) return options_to_test def create_resolved_command( self, result ): # type: (ResolveResult) -> ResolvedCommand if not result.is_parsable(): raise result.parse_error return ResolvedCommand(result.command, result.parsed_args) clikit-0.6.2/src/clikit/resolver/help_resolver.py000066400000000000000000000020141366776574300221550ustar00rootroot00000000000000from clikit.api.args.raw_args import RawArgs from clikit.api.resolver import ResolvedCommand from .default_resolver import DefaultResolver from .resolve_result import ResolveResult class HelpResolver(DefaultResolver): """ A command resolver used by a help handler. """ def __init__(self, help_command_name="help"): # type: (str) -> None: self._help_command_name = help_command_name def resolve( self, args, application ): # type: (RawArgs, Application) -> ResolvedCommand if args.tokens and args.tokens[0] == self._help_command_name: del args.tokens[0] return super(HelpResolver, self).resolve(args, application) def create_resolved_command( self, result ): # type: (ResolveResult) -> ResolvedCommand result.command.config.enable_lenient_args_parsing() resolved_command = super(HelpResolver, self).create_resolved_command(result) result.command.config.disable_lenient_args_parsing() return resolved_command clikit-0.6.2/src/clikit/resolver/resolve_result.py000066400000000000000000000025621366776574300223710ustar00rootroot00000000000000from clikit.api.args import Args from clikit.api.args import RawArgs from clikit.api.args.exceptions import CannotParseArgsException from clikit.api.command import Command class ResolveResult(object): """ An intermediate result created during resolving. """ def __init__(self, command, raw_args): # type: (Command, RawArgs) -> None self._command = command self._raw_args = raw_args self._parsed_args = None self._parse_error = None self._parsed = False @property def command(self): # type: () -> Command return self._command @property def raw_args(self): # type: () -> RawArgs return self._raw_args @property def parsed_args(self): # type: () -> Args if not self._parsed: self._parse() return self._parsed_args @property def parse_error(self): # type: () -> CannotParseArgsException if not self._parsed: self._parse() return self._parse_error def is_parsable(self): # type: () -> bool if not self._parsed: self._parse() return self.parse_error is None def _parse(self): # type: () -> None try: self._parsed_args = self._command.parse(self._raw_args) except CannotParseArgsException as e: self._parse_error = e self._parsed = True clikit-0.6.2/src/clikit/ui/000077500000000000000000000000001366776574300155115ustar00rootroot00000000000000clikit-0.6.2/src/clikit/ui/__init__.py000066400000000000000000000001021366776574300176130ustar00rootroot00000000000000from .component import Component from .rectangle import Rectangle clikit-0.6.2/src/clikit/ui/alignment/000077500000000000000000000000001366776574300174675ustar00rootroot00000000000000clikit-0.6.2/src/clikit/ui/alignment/__init__.py000066400000000000000000000000541366776574300215770ustar00rootroot00000000000000from .label_alignment import LabelAlignment clikit-0.6.2/src/clikit/ui/alignment/label_alignment.py000066400000000000000000000022741366776574300231630ustar00rootroot00000000000000from typing import List from clikit.api.formatter import Formatter from clikit.ui.components import LabeledParagraph class LabelAlignment: """ Aligns labeled paragraphs. """ def __init__(self): # type: () -> None self._paragraphs = [] # type: List[LabeledParagraph] self._indentations = [] # type: List[int] self._text_offset = 0 def add(self, paragraph, indentation=0): # type: (LabeledParagraph, int) -> None if paragraph.is_aligned(): self._paragraphs.append(paragraph) self._indentations.append(indentation) def align(self, formatter, indentation=0): # type: (Formatter, int) -> None self._text_offset = 0 for i, paragraph in enumerate(self._paragraphs): label = formatter.remove_format(paragraph.label) text_offset = self._indentations[i] + len(label) + paragraph.padding self._text_offset = max(self._text_offset, text_offset) self._text_offset += indentation def set_text_offset(self, offset): # type: (int) -> None self._text_offset = offset @property def text_offset(self): # type: () -> int return self._text_offset clikit-0.6.2/src/clikit/ui/component.py000066400000000000000000000003451366776574300200670ustar00rootroot00000000000000from clikit.api.io import IO class Component(object): """ A UI component that can be rendered on the I/O. """ def render(self, io, indentation=0): # type: (IO, int) -> None raise NotImplementedError() clikit-0.6.2/src/clikit/ui/components/000077500000000000000000000000001366776574300176765ustar00rootroot00000000000000clikit-0.6.2/src/clikit/ui/components/__init__.py000066400000000000000000000010031366776574300220010ustar00rootroot00000000000000from .border_util import BorderUtil from .cell_wrapper import CellWrapper from .choice_question import ChoiceQuestion from .confirmation_question import ConfirmationQuestion from .empty_line import EmptyLine from .exception_trace import ExceptionTrace from .labeled_paragraph import LabeledParagraph from .name_version import NameVersion from .paragraph import Paragraph from .progress_bar import ProgressBar from .progress_indicator import ProgressIndicator from .question import Question from .table import Table clikit-0.6.2/src/clikit/ui/components/border_util.py000066400000000000000000000113501366776574300225620ustar00rootroot00000000000000from __future__ import unicode_literals import math from typing import List from clikit.api.formatter import Style from clikit.api.io import IO from clikit.ui.style.alignment import Alignment from clikit.ui.style.border_style import BorderStyle from clikit.utils.string import get_string_length class BorderUtil: """ Contains utility methods to draw borders and bordered cells. """ @classmethod def draw_top_border( cls, io, style, column_lengths, indentation=0 ): # type: (IO, BorderStyle, List[int], int) -> None cls.draw_border( io, column_lengths, indentation, style.line_ht_char, style.corner_tl_char, style.crossing_t_char, style.corner_tr_char, style.style, ) @classmethod def draw_middle_border( cls, io, style, column_lengths, indentation=0 ): # type: (IO, BorderStyle, List[int], int) -> None cls.draw_border( io, column_lengths, indentation, style.line_hc_char, style.crossing_l_char, style.crossing_c_char, style.crossing_r_char, style.style, ) @classmethod def draw_bottom_border( cls, io, style, column_lengths, indentation=0 ): # type: (IO, BorderStyle, List[int], int) -> None cls.draw_border( io, column_lengths, indentation, style.line_hb_char, style.corner_bl_char, style.crossing_b_char, style.corner_br_char, style.style, ) @classmethod def draw_row( cls, io, style, row, column_lengths, alignments, cell_format, padding_char, cell_style=None, indentation=0, ): # type: (IO, BorderStyle, List[str], List[int], List[int], str, str, Style, int) -> None total_lines = 0 # Split all cells into lines for col, cell in enumerate(row): row[col] = cell.split("\n") total_lines = max(total_lines, len(row[col])) nb_columns = len(row) border_vl_char = io.format(style.line_vl_char, style.style) border_vc_char = io.format(style.line_vc_char, style.style) border_vr_char = io.format(style.line_vr_char, style.style) for i in range(total_lines): line = " " * indentation line += border_vl_char for col, remaining_lines in enumerate(row): if remaining_lines: cell_line = remaining_lines.pop(0) else: cell_line = "" total_pad_length = column_lengths[col] - get_string_length( cell_line, io ) padding_left = "" padding_right = "" if total_pad_length >= 0: try: alignment = alignments[col] except IndexError: alignment = Alignment.LEFT if alignment == Alignment.LEFT: padding_right = padding_char * total_pad_length elif alignment == Alignment.RIGHT: padding_left = padding_char * total_pad_length else: left_pad_length = int(math.floor(total_pad_length / 2)) padding_left = padding_char * left_pad_length padding_right = padding_char * ( total_pad_length - left_pad_length ) line += io.format( cell_format.format(padding_left + cell_line + padding_right), cell_style, ) if col < nb_columns - 1: line += border_vc_char else: line += border_vr_char # Remove trailing space io.write(line.rstrip() + "\n") @classmethod def draw_border( cls, io, column_lengths, indentation, line_char, crossing_l_char, crossing_c_char, crossing_r_char, style=None, ): # type: (IO, List[int], int, str, str, str, str, Style) -> None line = " " * indentation line += crossing_l_char l = len(column_lengths) for i in range(l): line += line_char * column_lengths[i] if i < l - 1: line += crossing_c_char else: line += crossing_r_char line = line.rstrip() if line: io.write(io.format(line, style) + "\n") clikit-0.6.2/src/clikit/ui/components/cell_wrapper.py000066400000000000000000000161131366776574300227310ustar00rootroot00000000000000from __future__ import division import textwrap from typing import List from clikit.api.formatter import Formatter from clikit.utils.string import get_max_line_length from clikit.utils.string import get_max_word_length from clikit.utils.string import get_string_length class CellWrapper: """ Wraps cells to fit a given screen width with a given number of columns. """ def __init__(self): # type: () -> None self._cells = [] self._cell_lengths = [] self._wrapped_rows = [] self._nb_columns = 0 self._column_lengths = [] self._word_wraps = False self._word_cuts = False self._max_total_width = 0 self._total_width = 0 @property def cells(self): # type: () -> List[str] return self._cells @property def wrapped_rows(self): # type: () -> List[List[str]] return self._wrapped_rows @property def column_lengths(self): # type: () -> List[int] return self._column_lengths @property def nb_columns(self): # type: () -> int return self._nb_columns @property def max_total_width(self): # type: () -> int return self._max_total_width @property def total_width(self): # type: () -> int return self._total_width def add_cell(self, cell): # type: (str) -> CellWrapper self._cells.append(cell.rstrip()) return self def add_cells(self, cells): # type: (List[str]) -> CellWrapper for cell in cells: self.add_cell(cell) return self def get_estimated_nb_columns(self, max_total_width): # type: (int) -> int """ Returns an estimated number of columns for the given maximum width. """ row_width = 0 for i, cell in enumerate(self._cells): row_width += get_string_length(cell) if row_width > max_total_width: return i return len(self._cells) - 1 def has_word_wraps(self): # type: () -> bool return self._word_wraps def has_word_cuts(self): # type: () -> bool return self._word_cuts def fit( self, max_total_width, nb_columns, formatter ): # type: (int, int, Formatter) -> None self._reset_state(max_total_width, nb_columns) self._init_rows(formatter) # If the cells fit within the max width we're good if self._total_width <= max_total_width: return self._wrap_columns(formatter) def _reset_state(self, max_total_width, nb_columns): # type: (int, int) -> None self._wrapped_rows = [] self._nb_columns = nb_columns self._cell_lengths = [] self._column_lengths = [0] * nb_columns self._word_wraps = False self._word_cuts = False self._max_total_width = max_total_width self._total_width = 0 def _init_rows(self, formatter): # type: (Formatter) -> None col = 0 for i, cell in enumerate(self._cells): if col == 0: self._wrapped_rows.append([""] * self._nb_columns) self._cell_lengths.append([0] * self._nb_columns) self._wrapped_rows[-1][col] = cell self._cell_lengths[-1][col] = get_string_length(cell, formatter) self._column_lengths[col] = max( self._column_lengths[col], self._cell_lengths[-1][col] ) col = (col + 1) % self._nb_columns # Fill last row if col > 0: while col < self._nb_columns: self._wrapped_rows[-1][col] = "" self._cell_lengths[-1][col] = 0 col += 1 self._total_width = sum(self._column_lengths) def _wrap_columns(self, formatter): # type: (Formatter) -> None available_width = self._max_total_width long_column_lengths = self._column_lengths[:] # Filter "short" column, i.e. columns that are not wrapped # We distribute the available screen width by the number of columns # and decide that all columns that are shorter than their share are # "short". # This process is repeated until no more "short" columns are found. repeat = True while repeat: threshold = available_width / len(long_column_lengths) repeat = False for col, length in enumerate(long_column_lengths): if length is not None and length <= threshold: available_width -= length long_column_lengths[col] = None repeat = True # Calculate actual and available width actual_width = 0 last_adapted_col = 0 # "Long" columns, i.e. columns that need to be wrapped, are added to # the actual width for col, length in enumerate(long_column_lengths): if length is None: continue actual_width += length last_adapted_col = col # Fit columns into available width for col, length in enumerate(long_column_lengths): if length is None: continue # Keep ratios of column lengths and distribute them among the # available width self._column_lengths[col] = int( round((length / actual_width) * available_width) ) if col == last_adapted_col: # Fix rounding errors self._column_lengths[col] += self._max_total_width - sum( self._column_lengths ) self._wrap_column(col, self._column_lengths[col], formatter) # Recalculate the column length based on the actual wrapped length self._refresh_column_length(col) # Recalculate the actual width based on the changed length. actual_width = actual_width - length + self._column_lengths[col] self._total_width = sum(self._column_lengths) def _wrap_column( self, col, column_length, formatter ): # type: (int, List[int], Formatter) -> None for i, row in enumerate(self._wrapped_rows): cell = row[col] cell_length = self._cell_lengths[i][col] if cell_length > column_length: self._word_wraps = True if not self._word_cuts: min_length_without_cut = get_max_word_length(cell, formatter) if min_length_without_cut > column_length: self._word_cuts = True # TODO: use format aware wrapper wrapped_cell = "\n".join(textwrap.wrap(cell, column_length)) self._wrapped_rows[i][col] = wrapped_cell # Refresh cell length self._cell_lengths[i][col] = get_max_line_length( wrapped_cell, formatter ) def _refresh_column_length(self, col): # type: (int) -> None self._column_lengths[col] = 0 for i, row in enumerate(self._wrapped_rows): self._column_lengths[col] = max( self._column_lengths[col], self._cell_lengths[i][col] ) clikit-0.6.2/src/clikit/ui/components/choice_question.py000066400000000000000000000100641366776574300234320ustar00rootroot00000000000000import os import re from .question import Question class SelectChoiceValidator: def __init__(self, question): """ Constructor. """ self._question = question self._values = question.choices def validate(self, selected): """ Validate a choice. """ # Collapse all spaces. if isinstance(selected, int): selected = str(selected) selected_choices = selected.replace(" ", "") if self._question.supports_multiple_choices(): # Check for a separated comma values if not re.match("^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$", selected_choices): raise ValueError(self._question.error_message.format(selected)) selected_choices = selected_choices.split(",") else: selected_choices = [selected] multiselect_choices = [] for value in selected_choices: results = [] for key, choice in enumerate(self._values): if choice == value: results.append(key) if len(results) > 1: raise ValueError( "The provided answer is ambiguous. Value should be one of {}.".format( " or ".join(str(r) for r in results) ) ) try: result = self._values.index(value) result = self._values[result] except ValueError: try: value = int(value) if 0 <= value < len(self._values): result = self._values[value] else: result = False except ValueError: result = False if result is False: raise ValueError(self._question.error_message.format(value)) multiselect_choices.append(result) if self._question.supports_multiple_choices(): return multiselect_choices return multiselect_choices[0] class ChoiceQuestion(Question): """ Multiple choice question. """ def __init__(self, question, choices, default=None): super(ChoiceQuestion, self).__init__(question, default) self._multi_select = False self._choices = choices self._validator = SelectChoiceValidator(self).validate self._autocomplete_values = choices self._prompt = " > " self._error_message = 'Value "{}" is invalid' @property def error_message(self): return self._error_message @property def choices(self): return self._choices def supports_multiple_choices(self): return self._multi_select def set_multi_select(self, multi_select): self._multi_select = multi_select def set_error_message(self, message): self._error_message = message def _write_prompt(self, io): """ Outputs the question prompt. """ message = self._question default = self._default if default is None: message = "{}: ".format(message) elif self._multi_select: choices = self._choices default = default.split(",") for i, value in enumerate(default): default[i] = choices[int(value.strip())] message = "{} [{}]:".format( message, ", ".join(default) ) else: choices = self._choices message = "{} [{}]:".format( message, choices[int(default)] ) if len(self._choices) > 1: width = max(*map(len, [str(k) for k, _ in enumerate(self._choices)])) else: width = 1 messages = [message] for key, value in enumerate(self._choices): messages.append(" [{:{}}] {}".format(key, width, value)) io.error_line("\n".join(messages)) message = self._prompt io.error(message) clikit-0.6.2/src/clikit/ui/components/confirmation_question.py000066400000000000000000000017401366776574300246710ustar00rootroot00000000000000import re from .question import Question class ConfirmationQuestion(Question): """ Represents a yes/no question. """ def __init__(self, question, default=True, true_answer_regex="(?i)^y"): super(ConfirmationQuestion, self).__init__(question, default) self._true_answer_regex = true_answer_regex self._normalizer = self._get_default_normalizer def _write_prompt(self, io): message = self._question message = "{} (yes/no) [{}] ".format( message, "yes" if self._default else "no" ) io.error(message) def _get_default_normalizer(self, answer): """ Default answer normalizer. """ if isinstance(answer, bool): return answer answer_is_true = re.match(self._true_answer_regex, answer) is not None if self.default is False: return answer and answer_is_true return not answer or answer_is_true clikit-0.6.2/src/clikit/ui/components/empty_line.py000066400000000000000000000003321366776574300224130ustar00rootroot00000000000000from clikit.api.io import IO from clikit.ui import Component class EmptyLine(Component): """ An empty line. """ def render(self, io, indentation=0): # type: (IO, int) -> None io.write("\n") clikit-0.6.2/src/clikit/ui/components/exception_trace.py000077500000000000000000000370511366776574300234350ustar00rootroot00000000000000# -*- coding: utf-8 -*- import ast import inspect import io import keyword import os import re import sys import tokenize import traceback from clikit.api.io import IO from clikit.formatter.plain_formatter import PlainFormatter from clikit.utils._compat import PY2 from clikit.utils._compat import PY36 from clikit.utils._compat import decode from clikit.utils._compat import encode class Highlighter(object): TOKEN_DEFAULT = "token_default" TOKEN_COMMENT = "token_comment" TOKEN_STRING = "token_string" TOKEN_NUMBER = "token_number" TOKEN_KEYWORD = "token_keyword" TOKEN_BUILTIN = "token_builtin" TOKEN_OP = "token_op" LINE_MARKER = "line_marker" LINE_NUMBER = "line_number" DEFAULT_THEME = { TOKEN_STRING: "fg=yellow;options=bold", TOKEN_NUMBER: "fg=blue;options=bold", TOKEN_COMMENT: "fg=default;options=dark,italic", TOKEN_KEYWORD: "fg=magenta;options=bold", TOKEN_BUILTIN: "fg=default;options=bold", TOKEN_DEFAULT: "fg=default", TOKEN_OP: "fg=default;options=dark", LINE_MARKER: "fg=red;options=bold", LINE_NUMBER: "fg=default;options=dark", } KEYWORDS = set(keyword.kwlist) BUILTINS = set( __builtins__.keys() if type(__builtins__) is dict else dir(__builtins__) ) def __init__(self): self._theme = self.DEFAULT_THEME.copy() def code_snippet(self, source, line, lines_before=2, lines_after=2): token_lines = self.highlighted_lines(source) token_lines = self.line_numbers(token_lines, line) offset = line - lines_before - 1 offset = max(offset, 0) length = lines_after + lines_before + 1 token_lines = token_lines[offset : offset + length] return token_lines def highlighted_lines(self, source): source = source.replace("\r\n", "\n").replace("\r", "\n") return self.split_to_lines(source) def split_to_lines(self, source): lines = [] current_line = 1 current_col = 0 buffer = "" current_type = None source_io = io.BytesIO(encode(source)) formatter = PlainFormatter() def readline(): return encode(formatter.remove_format(decode(source_io.readline()))) tokens = tokenize.tokenize(readline) line = "" for token_info in tokens: token_type, token_string, start, end, _ = token_info lineno = start[0] if lineno == 0: # Encoding line continue if token_type == tokenize.ENDMARKER: # End of source lines.append(line) break if lineno > current_line: diff = lineno - current_line if diff > 1: lines += [""] * (diff - 1) line += "<{}>{}".format( self._theme[current_type], buffer.rstrip("\n") ) # New line lines.append(line) line = "" current_line = lineno current_col = 0 buffer = "" if token_string in self.KEYWORDS: new_type = self.TOKEN_KEYWORD elif token_string in self.BUILTINS or token_string == "self": new_type = self.TOKEN_BUILTIN elif token_type == tokenize.STRING: new_type = self.TOKEN_STRING elif token_type == tokenize.NUMBER: new_type = self.TOKEN_NUMBER elif token_type == tokenize.COMMENT: new_type = self.TOKEN_COMMENT elif token_type == tokenize.OP: new_type = self.TOKEN_OP elif token_type == tokenize.NEWLINE: continue else: new_type = self.TOKEN_DEFAULT if current_type is None: current_type = new_type if start[1] > current_col: buffer += token_info.line[current_col : start[1]] if current_type != new_type: line += "<{}>{}".format(self._theme[current_type], buffer) buffer = "" current_type = new_type if lineno < end[0]: # The token spans multiple lines lines.append(line) token_lines = token_string.split("\n") for l in token_lines[1:-1]: lines.append("<{}>{}".format(self._theme[current_type], l)) current_line = end[0] buffer = token_lines[-1][: end[1]] line = "" continue buffer += token_string current_col = end[1] current_line = lineno return lines def line_numbers(self, lines, mark_line=None): max_line_length = len(str(len(lines))) snippet_lines = [] marker = " <{}>→ ".format(self._theme[self.LINE_MARKER]) no_marker = " " for i, line in enumerate(lines): if mark_line is not None: if mark_line == i + 1: snippet = marker else: snippet = no_marker line_number = "{:>{}}".format(i + 1, max_line_length) snippet += "<{}>{}<{}>│ {}".format( "fg=default;options=bold" if mark_line == i + 1 else self._theme[self.LINE_NUMBER], line_number, self._theme[self.LINE_NUMBER], line, ) snippet_lines.append(snippet) return snippet_lines class ExceptionTrace(object): """ Renders the trace of an exception. """ THEME = { "comment": "", "keyword": "", "builtin": "", "literal": "", } AST_ELEMENTS = { "builtins": __builtins__.keys() if type(__builtins__) is dict else dir(__builtins__), "keywords": [ getattr(ast, cls) for cls in dir(ast) if keyword.iskeyword(cls.lower()) and inspect.isclass(getattr(ast, cls)) and issubclass(getattr(ast, cls), ast.AST) ], } _FRAME_SNIPPET_CACHE = {} def __init__( self, exception, solution_provider_repository=None ): # type: (Exception, ...) -> None self._exception = exception self._solution_provider_repository = solution_provider_repository self._exc_info = sys.exc_info() self._higlighter = Highlighter() self._ignore = None def ignore_files_in(self, ignore): # type: (str) -> ExceptionTrace self._ignore = ignore return self def render(self, io, simple=False): # type: (IO, bool) -> None if simple: io.write_line("{}".format(str(self._exception))) return if not PY36: return self._render_legacy(io) return self._render_exception(io, self._exception) def _render_legacy(self, io): if hasattr(self._exception, "__traceback__"): tb = self._exception.__traceback__ else: tb = self._exc_info[2] title = "\n{}\n\n{}".format( self._exception.__class__.__name__, str(self._exception) ) io.write_line(title) if io.is_verbose(): io.write_line("") self._render_traceback(io, tb) def _render_exception(self, io, exception): from crashtest.inspector import Inspector inspector = Inspector(exception) if not inspector.frames: return self._render_trace(io, inspector.frames) self._render_line( io, "{}".format(inspector.exception_name), True ) io.write_line("") exception_message = io.remove_format(inspector.exception_message).replace( "\n", "\n " ) self._render_line(io, "{}".format(exception_message)) current_frame = inspector.frames[-1] self._render_snippet(io, current_frame) self._render_solution(io, inspector) def _render_snippet(self, io, frame): self._render_line( io, "at {}:{} in {}".format( self._get_relative_file_path(frame.filename), frame.lineno, frame.function, ), True, ) code_lines = self._higlighter.code_snippet( frame.file_content, frame.lineno, 4, 4 ) for code_line in code_lines: self._render_line(io, code_line) def _render_solution(self, io, inspector): if self._solution_provider_repository is None: return solutions = self._solution_provider_repository.get_solutions_for_exception( inspector.exception ) for solution in solutions: title = solution.solution_title description = solution.solution_description links = solution.documentation_links description = description.replace("\n", "\n ").strip(" ") self._render_line( io, "{}: {}{}".format( title.rstrip("."), description, ",".join("\n {}".format(link) for link in links), ), True, ) def _render_trace(self, io, frames): from crashtest.frame_collection import FrameCollection stack_frames = FrameCollection() for frame in frames: if ( self._ignore and re.match(self._ignore, frame.filename) and not io.is_debug() ): continue stack_frames.append(frame) remaining_frames_length = len(stack_frames) - 1 if io.is_verbose() and remaining_frames_length: self._render_line(io, "Stack trace:", True) max_frame_length = len(str(remaining_frames_length)) frame_collections = stack_frames.compact() i = remaining_frames_length for collection in frame_collections: if collection.is_repeated(): if len(collection) > 1: frames_message = "{} frames".format( len(collection) ) else: frames_message = "frame" self._render_line( io, "{:>{}} Previous {} repeated {} times".format( "...", max_frame_length, frames_message, collection.repetitions, ), True, ) i -= len(collection) * collection.repetitions + len(collection) for frame in collection: self._render_line( io, "{:>{}} {}:{} in {}".format( i, max_frame_length, self._get_relative_file_path(frame.filename), frame.lineno, frame.function, ), True, ) if io.is_debug(): if (frame, 2, 2) not in self._FRAME_SNIPPET_CACHE: code_lines = self._higlighter.code_snippet( frame.file_content, frame.lineno, ) self._FRAME_SNIPPET_CACHE[(frame, 2, 2)] = code_lines code_lines = self._FRAME_SNIPPET_CACHE[(frame, 2, 2)] for code_line in code_lines: self._render_line( io, "{:>{}}{}".format(" ", max_frame_length, code_line), indent=1, ) else: self._render_line( io, "{:>{}} {}".format( " ", max_frame_length, frame.line.strip() ), ) i -= 1 def _render_line( self, io, line, new_line=False, indent=2 ): # type: (IO, str) -> None if new_line: io.write_line("") io.write_line("{}{}".format(indent * " ", line)) def _get_relative_file_path(self, filepath): cwd = os.getcwd() if cwd: filepath = filepath.replace(cwd + os.path.sep, "") home = os.path.expanduser("~") if home: filepath = filepath.replace(home + os.path.sep, "~" + os.path.sep) return filepath def _render_traceback(self, io, tb): # type: (IO, ...) -> None frames = [] while tb: frames.append(self._format_traceback_frame(io, tb)) tb = tb.tb_next io.write_line("Traceback (most recent call last):") io.write_line("".join(traceback.format_list(frames))) def _format_traceback_frame(self, io, tb): # type: (IO, ...) -> Tuple[Any] frame_info = inspect.getframeinfo(tb) filename = frame_info.filename lineno = frame_info.lineno function = frame_info.function line = frame_info.code_context[0] stripped_line = line.lstrip(" ") try: tree = ast.parse(stripped_line, mode="exec") formatted = self._format_tree(tree, stripped_line, io) formatted = (len(line) - len(stripped_line)) * " " + formatted except SyntaxError: formatted = line return ( io.format("{}".format(filename)), "{}".format(lineno) if not PY2 else lineno, "{}".format(function), formatted, ) def _format_tree(self, tree, source, io): offset = 0 chunks = [] nodes = [n for n in ast.walk(tree)] displayed_nodes = [] for node in nodes: nodecls = node.__class__ nodename = nodecls.__name__ if "col_offset" not in dir(node): continue if nodecls in self.AST_ELEMENTS["keywords"]: displayed_nodes.append((node, nodename.lower(), "keyword")) elif nodecls == ast.Name and node.id in self.AST_ELEMENTS["builtins"]: displayed_nodes.append((node, node.id, "builtin")) elif nodecls == ast.Str: displayed_nodes.append((node, "'{}'".format(node.s), "literal")) elif nodecls == ast.Num: displayed_nodes.append((node, str(node.n), "literal")) displayed_nodes.sort(key=lambda elem: elem[0].col_offset) for dn in displayed_nodes: node = dn[0] s = dn[1] theme = dn[2] begin_col = node.col_offset src_chunk = source[offset:begin_col] chunks.append(src_chunk) chunks.append(io.format("{}{}".format(self.THEME[theme], s))) offset = begin_col + len(s) chunks.append(source[offset:]) return "".join(chunks) clikit-0.6.2/src/clikit/ui/components/labeled_paragraph.py000066400000000000000000000037061366776574300236730ustar00rootroot00000000000000from __future__ import unicode_literals import re import textwrap from clikit.api.io import IO from clikit.ui import Component class LabeledParagraph(Component): """ A paragraph with a label on its left. """ def __init__( self, label, text, padding=2, aligned=True ): # type: (str, str, int, bool) -> None self._label = label self._text = text self._padding = padding self._aligned = aligned self._alignment = None @property def label(self): # type: () -> str return self._label @property def text(self): # type: () -> str return self._text @property def padding(self): # type: () -> int return self._padding def is_aligned(self): # type: () -> bool return self._aligned def set_alignment(self, alignment): # type: (LabelAlignment) -> None self._alignment = alignment def render(self, io, indentation=0): # type: (IO, int) -> None line_prefix = " " * indentation visible_label = io.remove_format(self._label) style_tag_length = len(self._label) - len(visible_label) if self._aligned and self._alignment: text_offset = self._alignment.text_offset - indentation else: text_offset = 0 text_offset = max(text_offset, len(visible_label) + self._padding) text_prefix = " " * text_offset # 1 trailing space text_width = io.terminal_dimensions.width - 1 - text_offset - indentation text = re.sub( r"\n(?!\n)", "\n" + line_prefix + text_prefix, "\n".join(textwrap.wrap(self._text, text_width)), ) # Add the total length of the style tags ("", ...) label_width = text_offset + style_tag_length io.write( "{}{:<{}}{}".format( line_prefix, self._label, label_width, text.rstrip() ).rstrip() + "\n" ) clikit-0.6.2/src/clikit/ui/components/name_version.py000066400000000000000000000015601366776574300227370ustar00rootroot00000000000000from clikit.api.io import IO from clikit.api.config import ApplicationConfig from clikit.ui import Component from .paragraph import Paragraph class NameVersion(Component): """ Renders the name and version of an application. """ def __init__(self, config): # type: (ApplicationConfig) -> None self._config = config def render(self, io, indentation=0): # type: (IO, int) -> None if self._config.display_name and self._config.version: paragraph = Paragraph( "{} version {}".format( self._config.display_name, self._config.version ) ) elif self._config.display_name: paragraph = Paragraph("{}".format(self._config.display_name)) else: paragraph = Paragraph("Console Tool") paragraph.render(io, indentation) clikit-0.6.2/src/clikit/ui/components/paragraph.py000066400000000000000000000012601366776574300222140ustar00rootroot00000000000000import re import textwrap from clikit.api.io import IO from clikit.ui import Component class Paragraph(Component): """ A paragraph of text. The paragraph is wrapped into the dimensions of the output. """ def __init__(self, text): # type: (str) -> None self._text = text def render(self, io, indentation=0): # type: (IO, int) -> None line_prefix = " " * indentation text_width = io.terminal_dimensions.width - 1 - indentation text = re.sub( r"\n(?!\n)", "\n" + line_prefix, "\n".join(textwrap.wrap(self._text, text_width)), ) io.write(line_prefix + text.rstrip() + "\n") clikit-0.6.2/src/clikit/ui/components/progress_bar.py000066400000000000000000000305701366776574300227450ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import division import time import re import math from typing import Union from clikit.api.io import IO from clikit.api.io.flags import DEBUG from clikit.api.io.flags import VERBOSE from clikit.api.io.flags import VERY_VERBOSE from clikit.api.io.section_output import SectionOutput from clikit.api.io.output import Output from clikit.utils.terminal import Terminal from clikit.utils.time import format_time class ProgressBar(object): """ The ProgressBar provides helpers to display progress output. """ # Options bar_width = 28 bar_char = None empty_bar_char = "-" progress_char = ">" redraw_freq = 1 formats = { "normal": " %current%/%max% [%bar%] %percent:3s%%", "normal_nomax": " %current% [%bar%]", "verbose": " %current%/%max% [%bar%] %percent:3s%% %elapsed:-6s%", "verbose_nomax": " %current% [%bar%] %elapsed:6s%", "very_verbose": " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%", "very_verbose_nomax": " %current% [%bar%] %elapsed:6s%", "debug": " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%", "debug_nomax": " %current% [%bar%] %elapsed:6s%", } def __init__( self, io, max=0, min_seconds_between_redraws=0.1 ): # type: (Union[IO, Output], int, float) -> None """ Constructor. """ # If we have an IO, ensure we write to the error output if isinstance(io, IO): io = io.error_output self._io = io self._terminal = Terminal() self._max = 0 self._step_width = None self._set_max_steps(max) self._step = 0 self._percent = 0.0 self._format = None self._internal_format = None self._format_line_count = 0 self._last_messages_length = 0 self._should_overwrite = True self._min_seconds_between_redraws = 0 self._max_seconds_between_redraws = 1 self._write_count = 0 if min_seconds_between_redraws > 0: self.redraw_freq = None self._min_seconds_between_redraws = min_seconds_between_redraws if not self._io.supports_ansi(): # Disable overwrite when output does not support ANSI codes. self._should_overwrite = False # Set a reasonable redraw frequency so output isn't flooded self.redraw_freq = None self._messages = {} self._start_time = time.time() self._last_write_time = 0 def set_message(self, message, name="message"): self._messages[name] = message def get_message(self, name="message"): return self._messages[name] def get_start_time(self): return self._start_time def get_max_steps(self): return self._max def get_progress(self): return self._step def get_progress_percent(self): return self._percent def set_bar_character(self, character): self.bar_char = character return self def get_bar_character(self): if self.bar_char is None: if self._max: return "=" return self.empty_bar_char return self.bar_char def get_bar_width(self): return self.bar_width def set_bar_width(self, width): self.bar_width = width return self def get_empty_bar_character(self): return self.empty_bar_char def set_empty_bar_character(self, character): self.empty_bar_char = character return self def get_progress_character(self): return self.progress_char def set_progress_character(self, character): self.progress_char = character return self def set_format(self, fmt): self._format = None self._internal_format = fmt def set_redraw_frequency(self, freq): if self.redraw_freq is not None: self.redraw_freq = max(freq, 1) def min_seconds_between_redraws(self, freq): # type: (float) -> None if freq > 0: self.redraw_freq = None self._min_seconds_between_redraws = freq def max_seconds_between_redraws(self, freq): # type: (float) -> None self._max_seconds_between_redraws = freq def start(self, max=None): """ Start the progress output. """ self._start_time = time.time() self._step = 0 self._percent = 0.0 if max is not None: self._set_max_steps(max) self.display() def advance(self, step=1): """ Advances the progress output X steps. """ self.set_progress(self._step + step) def set_progress(self, step): """ Sets the current progress. :param step: The current progress :type step: int """ if self._max and step > self._max: self._max = step elif step < 0: step = 0 redraw_freq = ( (self._max or 10) / 10 if self.redraw_freq is None else self.redraw_freq ) prev_period = int(self._step / redraw_freq) curr_period = int(step / redraw_freq) self._step = step if self._max: self._percent = self._step / self._max else: self._percent = 0.0 time_interval = time.time() - self._last_write_time # Draw regardless of other limits if step == self._max: self.display() return # Throttling if time_interval < self._min_seconds_between_redraws: return # Draw each step period, but not too late if ( prev_period != curr_period or time_interval >= self._max_seconds_between_redraws ): self.display() def finish(self): """ Finish the progress output. """ if not self._max: self._max = self._step if self._step == self._max and not self._should_overwrite: return self.set_progress(self._max) def display(self): """ Output the current progress string. """ if self._io.is_quiet(): return if self._format is None: self._set_real_format( self._internal_format or self._determine_best_format() ) self._overwrite( re.sub( r"(?i)%([a-z\-_]+)(?::([^%]+))?%", self._overwrite_callback, self._format, ) ) def _overwrite_callback(self, matches): if hasattr(self, "_formatter_{}".format(matches.group(1))): text = str(getattr(self, "_formatter_{}".format(matches.group(1)))()) elif matches.group(1) in self._messages: text = self._messages[matches.group(1)] else: return matches.group(0) if matches.group(2): if matches.group(2).startswith("-"): text = text.ljust(int(matches.group(2).lstrip("-").rstrip("s"))) else: text = text.rjust(int(matches.group(2).rstrip("s"))) return text def clear(self): """ Removes the progress bar from the current line. This is useful if you wish to write some output while a progress bar is running. Call display() to show the progress bar again. """ if not self._should_overwrite: return if self._format is None: self._set_real_format( self._internal_format or self._determine_best_format() ) self._overwrite("\n" * self._format_line_count) def _set_real_format(self, fmt): """ Sets the progress bar format. """ # try to use the _nomax variant if available if not self._max and fmt + "_nomax" in self.formats: self._format = self.formats[fmt + "_nomax"] elif fmt in self.formats: self._format = self.formats[fmt] else: self._format = fmt self._format_line_count = self._format.count("\n") def _set_max_steps(self, mx): """ Sets the progress bar maximal steps. """ self._max = max(0, mx) if self._max: self._step_width = len(str(self._max)) else: self._step_width = 4 def _overwrite(self, message): """ Overwrites a previous message to the output. """ lines = message.split("\n") # Append whitespace to match the line's length if self._last_messages_length is not None: for i, line in enumerate(lines): if self._last_messages_length > len(self._io.remove_format(line)): lines[i] = line.ljust(self._last_messages_length, "\x20") if self._should_overwrite: if isinstance(self._io, SectionOutput): lines_to_clear = ( int(math.floor(len(lines) / self._terminal.width)) + self._format_line_count + 1 ) self._io.clear(lines_to_clear) else: # move back to the beginning of the progress bar before redrawing it self._io.write("\x0D") if self._format_line_count: self._io.write("\033[{}A".format(self._format_line_count)) elif self._step > 0: # move to new line self._io.write_line("") self._io.write("\n".join(lines)) self._io.flush() self._last_messages_length = 0 for line in lines: length = len(self._io.remove_format(line)) if length > self._last_messages_length: self._last_messages_length = length self._last_write_time = time.time() self._write_count += 1 def _determine_best_format(self): verbosity = self._io.verbosity if verbosity == VERBOSE: if self._max: return "verbose" return "verbose_nomax" elif verbosity == VERY_VERBOSE: if self._max: return "very_verbose" return "very_verbose_nomax" elif verbosity == DEBUG: if self._max: return "debug" return "debug_nomax" if self._max: return "normal" return "normal_nomax" @property def bar_offset(self): # type: () -> int if self._max: return math.floor(self._percent * self.bar_width) else: if self.redraw_freq is None: return math.floor( (min(5, self.get_bar_width() / 15) * self._write_count) % self.bar_width ) return math.floor(self._step % self.bar_width) def _formatter_bar(self): complete_bars = self.bar_offset display = self.get_bar_character() * int(complete_bars) if complete_bars < self.bar_width: empty_bars = ( self.bar_width - complete_bars - len(self._io.remove_format(self.progress_char)) ) display += self.progress_char + self.empty_bar_char * int(empty_bars) return display def _formatter_elapsed(self): return format_time(time.time() - self._start_time) def _formatter_remaining(self): if not self._max: raise RuntimeError( "Unable to display the remaining time " "if the maximum number of steps is not set." ) if not self._step: remaining = 0 else: remaining = round( (time.time() - self._start_time) / self._step * (self._max - self._max) ) return format_time(remaining) def _formatter_estimated(self): if not self._max: raise RuntimeError( "Unable to display the estimated time " "if the maximum number of steps is not set." ) if not self._step: estimated = 0 else: estimated = round((time.time() - self._start_time) / self._step * self._max) return estimated def _formatter_current(self): return str(self._step).rjust(self._step_width, " ") def _formatter_max(self): return self._max def _formatter_percent(self): return int(math.floor(self._percent * 100)) clikit-0.6.2/src/clikit/ui/components/progress_indicator.py000066400000000000000000000125361366776574300241570ustar00rootroot00000000000000import re import time import threading from contextlib import contextmanager from typing import List from typing import Optional from typing import Union from clikit.api.io import IO from clikit.api.io import Output from clikit.utils.time import format_time class ProgressIndicator(object): """ A process indicator. """ NORMAL = " {indicator} {message}" NORMAL_NO_ANSI = " {message}" VERBOSE = " {indicator} {message} ({elapsed:6s})" VERBOSE_NO_ANSI = " {message} ({elapsed:6s})" VERY_VERBOSE = " {indicator} {message} ({elapsed:6s})" VERY_VERBOSE_NO_ANSI = " {message} ({elapsed:6s})" def __init__( self, io, fmt=None, interval=100, values=None ): # type: (Union[IO, Output], Optional[str], int, Optional[List[str]]) -> None if isinstance(io, IO): io = io.error_output self._io = io if fmt is None: fmt = self._determine_best_format() self._fmt = fmt if values is None: values = ["-", "\\", "|", "/"] if len(values) < 2: raise ValueError( "The progress indicator must have at least 2 indicator value characters." ) self._interval = interval self._values = values self._message = None self._update_time = None self._started = False self._current = 0 self._auto_running = None self._auto_thread = None self._start_time = None self._last_message_length = 0 @property def message(self): # type: () -> Optional[str] return self._message def set_message(self, message): # type: (Optional[str]) -> None self._message = message self._display() @property def current_value(self): # type: () -> str return self._values[self._current % len(self._values)] def start(self, message): # type: (str) -> None if self._started: raise RuntimeError("Progress indicator already started.") self._message = message self._started = True self._start_time = time.time() self._update_time = self._get_current_time_in_milliseconds() + self._interval self._current = 0 self._display() def advance(self): # type: () -> None if not self._started: raise RuntimeError("Progress indicator has not yet been started.") if not self._io.supports_ansi(): return current_time = self._get_current_time_in_milliseconds() if current_time < self._update_time: return self._update_time = current_time + self._interval self._current += 1 self._display() def finish(self, message, reset_indicator=False): # type: (str, bool) -> None if not self._started: raise RuntimeError("Progress indicator has not yet been started.") if self._auto_thread is not None: self._auto_running.set() self._auto_thread.join() self._message = message if reset_indicator: self._current = 0 self._display() self._io.write_line("") self._started = False @contextmanager def auto(self, start_message, end_message): """ Auto progress. """ self._auto_running = threading.Event() self._auto_thread = threading.Thread(target=self._spin) self.start(start_message) self._auto_thread.start() try: yield self except (Exception, KeyboardInterrupt): self._io.write_line("") self._auto_running.set() self._auto_thread.join() raise self.finish(end_message, reset_indicator=True) def _spin(self): while not self._auto_running.is_set(): self.advance() time.sleep(0.1) def _display(self): if self._io.is_quiet(): return self._overwrite( re.sub( r"(?i){([a-z\-_]+)(?::([^}]+))?}", self._overwrite_callback, self._fmt ) ) def _overwrite_callback(self, matches): if hasattr(self, "_formatter_{}".format(matches.group(1))): text = str(getattr(self, "_formatter_{}".format(matches.group(1)))()) else: text = matches.group(0) return text def _overwrite(self, message): """ Overwrites a previous message to the output. """ if self._io.supports_ansi(): self._io.write("\x0D\x1B[2K") self._io.write(message) else: self._io.write_line(message) def _determine_best_format(self): decorated = self._io.supports_ansi() if self._io.is_very_verbose(): if decorated: return self.VERY_VERBOSE return self.VERY_VERBOSE_NO_ANSI elif self._io.is_verbose(): if decorated: return self.VERY_VERBOSE return self.VERBOSE_NO_ANSI if decorated: return self.NORMAL return self.NORMAL_NO_ANSI def _get_current_time_in_milliseconds(self): return round(time.time() * 1000) def _formatter_indicator(self): return self.current_value def _formatter_message(self): return self.message def _formatter_elapsed(self): return format_time(time.time() - self._start_time) clikit-0.6.2/src/clikit/ui/components/question.py000066400000000000000000000166671366776574300221370ustar00rootroot00000000000000from __future__ import unicode_literals import getpass import os import subprocess from typing import Any from clikit.api.formatter import Style from clikit.api.io import IO from clikit.utils._compat import decode class Question(object): """ A question that will be asked in a Console. """ def __init__(self, question, default=None): # type: (str, Any) -> None self._question = question self._default = default self._attempts = None self._hidden = False self._hidden_fallback = True self._autocomplete_values = None self._validator = None self._normalizer = None self._error_message = None @property def question(self): # type: () -> str return self._question @property def default(self): # type: () -> Any return self._default @property def autocomplete_values(self): return self._autocomplete_values @property def max_attempts(self): return self._attempts def is_hidden(self): # type: () -> bool return self._hidden def hide(self, hidden=True): # type: (bool) -> None if hidden is True and self._autocomplete_values: raise RuntimeError("A hidden question cannot use the autocompleter.") self._hidden = hidden def set_autocomplete_values(self, autocomplete_values): if self.is_hidden(): raise RuntimeError("A hidden question cannot use the autocompleter.") self._autocomplete_values = autocomplete_values def set_max_attempts(self, attempts): self._attempts = attempts def set_validator(self, validator): self._validator = validator def ask(self, io): # type: (IO) -> str """ Asks the question to the user. """ if not io.is_interactive(): return self.default if not self._validator: return self._do_ask(io) interviewer = lambda: self._do_ask(io) return self._validate_attempts(interviewer, io) def _do_ask(self, io): # type: (IO) -> str """ Asks the question to the user. """ self._write_prompt(io) autocomplete = self._autocomplete_values if autocomplete is None or not self._has_stty_available(): ret = False if self.is_hidden(): try: ret = self._get_hidden_response(io) except RuntimeError: if not self._hidden_fallback: raise if not ret: ret = self._read_from_input(io) else: ret = self._autocomplete(io) if len(ret) <= 0: ret = self._default if self._normalizer: return self._normalizer(ret) return ret def _write_prompt(self, io): """ Outputs the question prompt. """ message = self._question io.error("{} ".format(message)) def _write_error(self, io, error): """ Outputs an error message. """ message = "{}".format(decode(str(error))) io.error_line(message) def _autocomplete(self, io): # type: (IO) -> str """ Autocomplete a question. """ autocomplete = self._autocomplete_values ret = "" i = 0 ofs = -1 matches = [x for x in autocomplete] num_matches = len(matches) stty_mode = decode(subprocess.check_output(["stty", "-g"])).rstrip("\n") # Disable icanon (so we can read each keypress) and echo (we'll do echoing here instead) subprocess.check_output(["stty", "-icanon", "-echo"]) # Add highlighted text style style = Style("hl").fg("black").bg("white") io.error_output.formatter.add_style(style) # Read a keypress while True: c = io.read(1) # Backspace character if c == "\177": if num_matches == 0 and i != 0: i -= 1 # Move cursor backwards io.error("\033[1D") if i == 0: ofs = -1 matches = [x for x in autocomplete] num_matches = len(matches) else: num_matches = 0 # Pop the last character off the end of our string ret = ret[:i] # Did we read an escape sequence elif c == "\033": c += io.read(2) # A = Up Arrow. B = Down Arrow if c[2] == "A" or c[2] == "B": if c[2] == "A" and ofs == -1: ofs = 0 if num_matches == 0: continue ofs += -1 if c[2] == "A" else 1 ofs = (num_matches + ofs) % num_matches elif ord(c) < 32: if c == "\t" or c == "\n": if num_matches > 0 and ofs != -1: ret = matches[ofs] # Echo out remaining chars for current match io.error(ret[i:]) i = len(ret) if c == "\n": io.error(c) break num_matches = 0 continue else: io.error(c) ret += c i += 1 num_matches = 0 ofs = 0 for value in autocomplete: # If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) if value.startswith(ret) and i != len(value): num_matches += 1 matches[num_matches - 1] = value # Erase characters from cursor to end of line io.error("\033[K") if num_matches > 0 and ofs != -1: # Save cursor position io.error("\0337") # Write highlighted text io.error("" + matches[ofs][i:] + "") # Restore cursor position io.error("\0338") subprocess.call(["stty", "{}".format(decode(stty_mode))]) return ret def _get_hidden_response(self, io): # type: (IO) -> str """ Gets a hidden response from user. """ return getpass.getpass("", stream=io.error_output.stream) def _validate_attempts(self, interviewer, io): # type: (Callable, IO) -> str """ Validates an attempt. """ error = None attempts = self._attempts while attempts is None or attempts: if error is not None: self._write_error(io, error) try: return self._validator(interviewer()) except Exception as e: error = e if attempts is not None: attempts -= 1 raise error def _read_from_input(self, io): """ Read user input. """ ret = io.read_line(4096) if not ret: raise RuntimeError("Aborted") return decode(ret.strip()) def _has_stty_available(self): devnull = open(os.devnull, "w") try: exit_code = subprocess.call(["stty"], stdout=devnull, stderr=devnull) except Exception: exit_code = 2 return exit_code == 0 clikit-0.6.2/src/clikit/ui/components/table.py000066400000000000000000000121601366776574300213370ustar00rootroot00000000000000from typing import List from typing import Optional from clikit.api.formatter import Formatter from clikit.api.io import IO from clikit.ui import Component from clikit.ui.components import BorderUtil from clikit.ui.components import CellWrapper from clikit.ui.style import TableStyle from clikit.utils.string import get_string_length class Table(Component): """ A table of rows and columns. """ def __init__(self, style=None): # type: (Optional[TableStyle]) -> None self._header_row = [] self._rows = [] self._nb_columns = None if style is None: style = TableStyle.ascii() self._style = style def set_header_row(self, row): # type: (List[str]) -> Table if self._nb_columns is None: self._nb_columns = len(row) elif len(row) != self._nb_columns: raise ValueError( "Expected the header row to contain {} cells, but got {}.".format( self._nb_columns, len(row) ) ) self._header_row = row return self def add_row(self, row): # type: (List[str]) -> Table if self._nb_columns is None: self._nb_columns = len(row) elif len(row) != self._nb_columns: raise ValueError( "Expected the row to contain {} cells, but got {}.".format( self._nb_columns, len(row) ) ) self._rows.append(row) return self def add_rows(self, rows): # type: (List[List[str]]) -> Table for row in rows: self.add_row(row) return self def set_rows(self, rows): # type: (List[List[str]]) -> Table self._rows = [] for row in rows: self.add_row(row) return self def set_row(self, index, row): # type: (int, List[str]) -> Table if len(row) != self._nb_columns: raise ValueError( "Expected the row to contain {} cells, but got {}.".format( self._nb_columns, len(row) ) ) self._rows[index] = row return self def render(self, io, indentation=0): # type: (IO, int) -> None if not self._rows: return screen_width = io.terminal_dimensions.width excess_column_width = max( get_string_length(self._style.header_cell_format.format("")), get_string_length(self._style.cell_format.format("")), ) wrapper = self._get_cell_wrapper( io, screen_width, excess_column_width, indentation ) return self._render_rows( io, wrapper.wrapped_rows, wrapper.column_lengths, excess_column_width, indentation, ) def _get_cell_wrapper( self, formatter, screen_width, excess_column_width, indentation ): # type: (Formatter, int, int, int) -> CellWrapper border_style = self._style.border_style border_width = ( get_string_length(border_style.line_vl_char) + (self._nb_columns - 1) * get_string_length(border_style.line_vc_char) + get_string_length(border_style.line_vr_char) ) available_width = ( screen_width - indentation - border_width - self._nb_columns * excess_column_width ) wrapper = CellWrapper() for header_cell in self._header_row: wrapper.add_cell(header_cell) for row in self._rows: for cell in row: wrapper.add_cell(cell) wrapper.fit(available_width, self._nb_columns, formatter) return wrapper def _render_rows( self, io, rows, column_lengths, excess_column_length, indentation ): # type: (IO, List[List[str]], List[int], int, int) -> None alignments = self._style.get_column_alignments(len(column_lengths)) border_style = self._style.border_style border_column_lengths = [ length + excess_column_length for length in column_lengths ] BorderUtil.draw_top_border(io, border_style, border_column_lengths, indentation) if self._header_row: BorderUtil.draw_row( io, border_style, rows.pop(0), column_lengths, alignments, self._style.header_cell_format, self._style.padding_char, self._style.header_cell_style, indentation, ) BorderUtil.draw_middle_border( io, border_style, border_column_lengths, indentation ) for row in rows: BorderUtil.draw_row( io, border_style, row, column_lengths, alignments, self._style.cell_format, self._style.padding_char, self._style.cell_style, indentation, ) BorderUtil.draw_bottom_border( io, border_style, border_column_lengths, indentation ) clikit-0.6.2/src/clikit/ui/help/000077500000000000000000000000001366776574300164415ustar00rootroot00000000000000clikit-0.6.2/src/clikit/ui/help/__init__.py000066400000000000000000000001241366776574300205470ustar00rootroot00000000000000from .application_help import ApplicationHelp from .command_help import CommandHelp clikit-0.6.2/src/clikit/ui/help/abstract_help.py000066400000000000000000000125521366776574300216330ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import json from typing import Any from typing import Iterable from clikit.api.args.format import Argument from clikit.api.args.format import ArgsFormat from clikit.api.args.format import Option from clikit.api.io import IO from clikit.ui import Component from clikit.ui.components import EmptyLine from clikit.ui.components import LabeledParagraph from clikit.ui.components import Paragraph from clikit.ui.layout import BlockLayout class AbstractHelp(Component): """ Base class for rendering help pages. """ def render(self, io, indentation=0): # type: (IO, int) -> None layout = BlockLayout() self._render_help(layout) layout.render(io, indentation) def _render_help(self, layout): # type: (BlockLayout) -> None raise NotImplementedError() def _render_arguments( self, layout, arguments ): # type: (BlockLayout, Iterable[Argument]) -> None layout.add(Paragraph("ARGUMENTS")) with layout.block(): for argument in arguments: self._render_argument(layout, argument) layout.add(EmptyLine()) def _render_argument( self, layout, argument ): # type: (BlockLayout, Argument) -> None description = argument.description name = "<{}>".format(argument.name) default = argument.default if default is not None and (not isinstance(default, list) or len(default) > 0): description += " {}".format(self._format_value(default)) layout.add(LabeledParagraph(name, description)) def _render_options( self, layout, options ): # type: (BlockLayout, Iterable[Option]) -> None layout.add(Paragraph("OPTIONS")) with layout.block(): for option in options: self._render_option(layout, option) layout.add(EmptyLine()) def _render_global_options( self, layout, options ): # type: (BlockLayout, Iterable[Option]) -> None layout.add(Paragraph("GLOBAL OPTIONS")) with layout.block(): for option in options: self._render_option(layout, option) layout.add(EmptyLine()) def _render_option(self, layout, option): # type: (BlockLayout, Option) -> None description = option.description default = option.default alternative_name = None if option.is_long_name_preferred(): preferred_name = "--{}".format(option.long_name) if option.short_name: alternative_name = "-{}".format(option.short_name) else: preferred_name = "-{}".format(option.short_name) alternative_name = "--{}".format(option.long_name) name = "{}".format(preferred_name) if alternative_name: name += " ({})".format(alternative_name) if ( option.accepts_value() and default is not None and (not isinstance(default, list) or len(default) > 0) ): description += " (default: {})".format(self._format_value(default)) if option.is_multi_valued(): description += " (multiple values allowed)" layout.add(LabeledParagraph(name, description)) def _render_synopsis( self, layout, args_format, app_name, prefix="", last_optional=False ): # type: (BlockLayout, ArgsFormat, str, str, bool) -> None name_parts = [] argument_parts = [] name_parts.append("{}".format(app_name or "console")) for command_name in args_format.get_command_names(): name_parts.append("{}".format(command_name.string)) for command_option in args_format.get_command_options(): if command_option.is_long_name_preferred(): name_parts.append("--{}".format(command_option.long_name)) else: name_parts.append("-{}".format(command_option.short_name)) if last_optional: name_parts[-1] = "[{}]".format(name_parts[-1]) for option in args_format.get_options(False).values(): # \xC2\xA0 is a non-breaking space if option.is_value_required(): fmt = "{}\u00A0<{}>" elif option.is_value_optional(): fmt = "{}\u00A0[<{}>]" else: fmt = "{}" if option.is_long_name_preferred(): option_name = "--{}".format(option.long_name) else: option_name = "-{}".format(option.short_name) argument_parts.append( "[{}]".format(fmt.format(option_name, option.value_name)) ) for argument in args_format.get_arguments().values(): arg_name = argument.name argument_parts.append( ("<{}>" if argument.is_required() else "[<{}>]").format( arg_name + str(int(argument.is_multi_valued()) or "") ) ) if argument.is_multi_valued(): argument_parts.append("... [<{}N>]".format(arg_name)) args_opts = " ".join(argument_parts) name = " ".join(name_parts) layout.add(LabeledParagraph(prefix + name, args_opts, 1, False)) def _format_value(self, value): # type: (Any) -> str return json.dumps(value) clikit-0.6.2/src/clikit/ui/help/application_help.py000066400000000000000000000065551366776574300223410ustar00rootroot00000000000000from clikit.api.application import Application from clikit.api.args.format import ArgsFormat from clikit.api.args.format import ArgsFormatBuilder from clikit.api.args.format import Argument from clikit.api.command import Command from clikit.api.command import CommandCollection from clikit.ui.components import EmptyLine from clikit.ui.components import LabeledParagraph from clikit.ui.components import NameVersion from clikit.ui.components import Paragraph from clikit.ui.layout import BlockLayout from .abstract_help import AbstractHelp class ApplicationHelp(AbstractHelp): """ Renders the help of a console application. """ def __init__(self, application): # type: (Application) -> None self._application = application def _render_help(self, layout): # type: (BlockLayout) -> None help = self._application.config.help commands = self._application.named_commands global_args_format = self._application.global_args_format builder = ArgsFormatBuilder() builder.add_argument( Argument("command", Argument.REQUIRED, "The command to execute") ) builder.add_argument( Argument("arg", Argument.MULTI_VALUED, "The arguments of the command") ) builder.add_options(*global_args_format.get_options().values()) args_format = builder.format self._render_name(layout, self._application) self._render_usage(layout, self._application, args_format) self._render_arguments(layout, args_format.get_arguments().values()) if args_format.has_options(): self._render_global_options(layout, args_format.get_options().values()) if not commands.is_empty(): self._render_commands(layout, commands) if help: self._render_description(layout, help) def _render_name( self, layout, application ): # type: (BlockLayout, Application) -> None layout.add(NameVersion(application.config)) layout.add(EmptyLine()) def _render_usage( self, layout, application, args_format ): # type: (BlockLayout, Application, ArgsFormat) -> None app_name = application.config.name layout.add(Paragraph("USAGE")) with layout.block(): self._render_synopsis(layout, args_format, app_name) layout.add(EmptyLine()) def _render_commands( self, layout, commands ): # type: (BlockLayout, CommandCollection) -> None layout.add(Paragraph("AVAILABLE COMMANDS")) with layout.block(): for command in sorted(commands, key=lambda c: c.name): self._render_command(layout, command) layout.add(EmptyLine()) def _render_command(self, layout, command): # type: (BlockLayout, Command) -> None if command.config.is_hidden(): return description = command.config.description name = "{}".format(command.name) layout.add(LabeledParagraph(name, description)) def _render_description(self, layout, help): # type: (BlockLayout, str) -> None help = help.format(script_name=self._application.config.name or "console") layout.add(Paragraph("DESCRIPTION")) with layout.block(): for paragraph in help.split("\n"): layout.add(Paragraph(paragraph)) layout.add(EmptyLine()) clikit-0.6.2/src/clikit/ui/help/command_help.py000066400000000000000000000136501366776574300214460ustar00rootroot00000000000000from typing import Iterable from clikit.api.args.format import Argument from clikit.api.args.format import Option from clikit.api.command import Command from clikit.api.command import CommandCollection from clikit.ui.components import EmptyLine from clikit.ui.components import Paragraph from clikit.ui.layout import BlockLayout from .abstract_help import AbstractHelp class CommandHelp(AbstractHelp): """ Renders the help of a command. """ def __init__(self, command): # type: (Command) -> None self._command = command def _render_help(self, layout): # type: (BlockLayout) -> None help = self._command.config.help args_format = self._command.args_format sub_commands = self._command.named_sub_commands self._render_usage(layout, self._command) if args_format.has_arguments(): self._render_arguments(layout, args_format.get_arguments().values()) if not sub_commands.is_empty(): self._render_sub_commands(layout, sub_commands) if args_format.has_options(False): self._render_options(layout, args_format.get_options(False).values()) if args_format.base_format and args_format.base_format.has_options(): self._render_global_options( layout, args_format.base_format.get_options().values() ) if help: self._render_description(layout, help) def _render_usage(self, layout, command): # type: (BlockLayout, Command) -> None formats_to_print = [] # Start with the default commands if command.has_default_sub_commands(): # If the command has default commands, print them for sub_command in command.default_sub_commands: # The name of the sub command is only optional (i.e. printed # wrapped in brackets: "[sub]") if the command is not # anonymous name_optional = not sub_command.config.is_anonymous() formats_to_print.append((sub_command.args_format, name_optional)) else: # Otherwise print the command's usage itself formats_to_print.append((command.args_format, False)) # Add remaining sub-commands for sub_command in command.sub_commands: if sub_command.config.is_hidden(): continue # Don't duplicate default commands if not sub_command.config.is_default(): formats_to_print.append((sub_command.args_format, False)) app_name = command.application.config.name prefix = " " if len(formats_to_print) > 1 else "" layout.add(Paragraph("USAGE")) with layout.block(): for vars in formats_to_print: self._render_synopsis(layout, vars[0], app_name, prefix, vars[1]) prefix = "or: " if command.has_aliases(): layout.add(EmptyLine()) self._render_aliases(layout, command.aliases) layout.add(EmptyLine()) def _render_aliases( self, layout, aliases ): # type: (BlockLayout, Iterable[str]) -> None layout.add(Paragraph("aliases: {}".format(", ".join(aliases)))) def _render_sub_commands( self, layout, sub_commands ): # type: (BlockLayout, CommandCollection) -> None layout.add(Paragraph("COMMANDS")) with layout.block(): for sub_command in sorted(sub_commands, key=lambda c: c.name): self._render_sub_command(layout, sub_command) def _render_sub_command( self, layout, command ): # type: (BlockLayout, Command) -> None config = command.config if config.is_hidden(): return description = config.description help = config.help arguments = command.args_format.get_arguments(False) options = command.args_format.get_options(False) # TODO: option commands name = "{}".format(command.name) layout.add(Paragraph(name)) with layout.block(): if description: self._render_sub_command_description(layout, description) if help: self._render_sub_command_help(layout, help) if arguments: self._render_sub_command_arguments(layout, arguments.values()) if options: self._render_sub_command_options(layout, options.values()) if not description and not help and not arguments and not options: layout.add(EmptyLine()) def _render_sub_command_description( self, layout, description ): # type: (BlockLayout, str) -> None layout.add(Paragraph(description)) layout.add(EmptyLine()) def _render_sub_command_help( self, layout, help ): # type: (BlockLayout, str) -> None layout.add(Paragraph(help)) layout.add(EmptyLine()) def _render_sub_command_arguments( self, layout, arguments ): # type: (BlockLayout, Iterable[Argument]) -> None for argument in arguments: self._render_argument(layout, argument) layout.add(EmptyLine()) def _render_sub_command_options( self, layout, options ): # type: (BlockLayout, Iterable[Option]) -> None for option in options: self._render_option(layout, option) layout.add(EmptyLine()) def _render_description(self, layout, help): # type: (BlockLayout, str) -> None script_name = "console" application = self._command.application if application and application.config.name: script_name = application.config.name help = help.format(script_name=script_name, command_name=self._command.name) layout.add(Paragraph("DESCRIPTION")) with layout.block(): for paragraph in help.split("\n"): layout.add(Paragraph(paragraph)) layout.add(EmptyLine()) clikit-0.6.2/src/clikit/ui/layout/000077500000000000000000000000001366776574300170265ustar00rootroot00000000000000clikit-0.6.2/src/clikit/ui/layout/__init__.py000066400000000000000000000000461366776574300211370ustar00rootroot00000000000000from .block_layout import BlockLayout clikit-0.6.2/src/clikit/ui/layout/block_layout.py000066400000000000000000000023561366776574300220750ustar00rootroot00000000000000from contextlib import contextmanager from clikit.api.io import IO from clikit.ui import Component from clikit.ui.alignment import LabelAlignment from clikit.ui.components import LabeledParagraph class BlockLayout: """ Renders renderable objects in indented blocks. """ def __init__(self): # type: () -> None self._current_indentation = 0 self._elements = [] self._indentations = [] self._alignment = LabelAlignment() def add(self, element): # type: (Component) -> BlockLayout self._elements.append(element) self._indentations.append(self._current_indentation) if isinstance(element, LabeledParagraph): self._alignment.add(element, self._current_indentation) element.set_alignment(self._alignment) return self @contextmanager def block(self): # type: () -> BlockLayout self._current_indentation += 2 yield self self._current_indentation -= 2 def render(self, io, indentation=0): # type: (IO, int) -> None self._alignment.align(io, indentation) for i, element in enumerate(self._elements): element.render(io, self._indentations[i] + indentation) self._elements = [] clikit-0.6.2/src/clikit/ui/rectangle.py000066400000000000000000000001311366776574300200220ustar00rootroot00000000000000from collections import namedtuple Rectangle = namedtuple("Rectangle", "width height") clikit-0.6.2/src/clikit/ui/style/000077500000000000000000000000001366776574300166515ustar00rootroot00000000000000clikit-0.6.2/src/clikit/ui/style/__init__.py000066400000000000000000000001051366776574300207560ustar00rootroot00000000000000from .alignment import Alignment from .table_style import TableStyle clikit-0.6.2/src/clikit/ui/style/alignment.py000066400000000000000000000001561366776574300212030ustar00rootroot00000000000000class Alignment: """ Constants for text alignment. """ LEFT = 0 RIGHT = 1 CENTER = 2 clikit-0.6.2/src/clikit/ui/style/border_style.py000066400000000000000000000047541366776574300217320ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from clikit.api.formatter import Style class BorderStyle: """ Defines the style of a border. """ _none = None _ascii = None _solid = None def __init__(self): # type: () -> None self.line_ht_char = "-" self.line_hc_char = "-" self.line_hb_char = "-" self.line_vl_char = "|" self.line_vc_char = "|" self.line_vr_char = "|" self.corner_tl_char = "+" self.corner_tr_char = "+" self.corner_bl_char = "+" self.corner_br_char = "+" self.crossing_c_char = "+" self.crossing_l_char = "+" self.crossing_t_char = "+" self.crossing_r_char = "+" self.crossing_b_char = "+" self.style = None # type: Style @classmethod def none(cls): # type: () -> BorderStyle if cls._none is None: style = cls() style.line_ht_char = "" style.line_hc_char = "" style.line_hb_char = "" style.line_vl_char = "" style.line_vc_char = " " style.line_vr_char = "" style.corner_tl_char = "" style.corner_tr_char = "" style.corner_bl_char = "" style.corner_br_char = "" style.crossing_c_char = "" style.crossing_l_char = "" style.crossing_t_char = "" style.crossing_r_char = "" style.crossing_b_char = "" cls._none = style return cls._none @classmethod def ascii(cls): # type: () -> BorderStyle if cls._ascii is None: style = cls() cls._ascii = style return cls._ascii @classmethod def solid(cls): # type: () -> BorderStyle if cls._solid is None: style = cls() style.line_ht_char = "─" style.line_hc_char = "─" style.line_hb_char = "─" style.line_vl_char = "│" style.line_vc_char = "│" style.line_vr_char = "│" style.corner_tl_char = "┌" style.corner_tr_char = "┐" style.corner_bl_char = "└" style.corner_br_char = "┘" style.crossing_c_char = "┼" style.crossing_l_char = "├" style.crossing_r_char = "┤" style.crossing_t_char = "┬" style.crossing_b_char = "┴" cls._solid = style return cls._solid clikit-0.6.2/src/clikit/ui/style/table_style.py000066400000000000000000000043771366776574300215450ustar00rootroot00000000000000from __future__ import unicode_literals from .alignment import Alignment from .border_style import BorderStyle class TableStyle: """ Defines the style of a Table """ _borderless = None _ascii = None _solid = None def __init__(self): # type: () -> None self.padding_char = " " self.header_cell_format = "{}" self.cell_format = "{}" self.column_alignments = [] self.default_column_alignment = Alignment.LEFT self.border_style = None self.header_cell_style = None self.cell_style = None def get_column_alignments(self, nb_columns): # type: (int) -> List[int] default_alignments = [self.default_column_alignment] * nb_columns for i, alignment in enumerate(self.column_alignments): default_alignments[i] = alignment return default_alignments def set_column_alignment(self, col, alignment): # type: (int, int) -> TableStyle if col > len(self.column_alignments) - 1: diff = abs(len(self.column_alignments) - col) + 1 self.column_alignments += [self.default_column_alignment] * diff self.column_alignments[col] = alignment @classmethod def borderless(cls): # type: () -> TableStyle style = TableStyle() style.border_style = BorderStyle.none() style.border_style.line_hc_char = "=" style.border_style.line_vc_char = " " style.border_style.crossing_c_char = " " return style @classmethod def compact(cls): # type: () -> TableStyle style = TableStyle() style.border_style = BorderStyle.none() style.border_style.line_hc_char = "" style.border_style.line_vc_char = " " style.border_style.crossing_c_char = "" return style @classmethod def ascii(cls): # type: () -> TableStyle style = TableStyle() style.header_cell_format = " {} " style.cell_format = " {} " style.border_style = BorderStyle.ascii() return style @classmethod def solid(cls): # type: () -> TableStyle style = TableStyle() style.header_cell_format = " {} " style.cell_format = " {} " style.border_style = BorderStyle.solid() return style clikit-0.6.2/src/clikit/utils/000077500000000000000000000000001366776574300162345ustar00rootroot00000000000000clikit-0.6.2/src/clikit/utils/__init__.py000066400000000000000000000000001366776574300203330ustar00rootroot00000000000000clikit-0.6.2/src/clikit/utils/_compat.py000066400000000000000000000034261366776574300202350ustar00rootroot00000000000000import sys try: # Python 2 long = long unicode = unicode basestring = basestring except NameError: # Python 3 long = int unicode = str basestring = str PY2 = sys.version_info[0] == 2 PY35 = sys.version_info >= (3, 5) PY36 = sys.version_info >= (3, 6) PY38 = sys.version_info >= (3, 8) if not PY36: from collections import OrderedDict else: OrderedDict = dict WINDOWS = sys.platform == "win32" def decode(string, encodings=None): if not PY2 and not isinstance(string, bytes): return string if PY2 and isinstance(string, unicode): return string encodings = encodings or ["utf-8", "latin1", "ascii"] for encoding in encodings: try: return string.decode(encoding) except (UnicodeEncodeError, UnicodeDecodeError): pass return string.decode(encodings[0], errors="ignore") def encode(string, encodings=None): if not PY2 and isinstance(string, bytes): return string if PY2 and isinstance(string, str): return string encodings = encodings or ["utf-8", "latin1", "ascii"] for encoding in encodings: try: return string.encode(encoding) except (UnicodeEncodeError, UnicodeDecodeError): pass return string.encode(encodings[0], errors="ignore") def to_str(string): if isinstance(string, str) or not isinstance(string, (unicode, bytes)): return string if PY2: method = "encode" else: method = "decode" encodings = ["utf-8", "latin1", "ascii"] for encoding in encodings: try: return getattr(string, method)(encoding) except (UnicodeEncodeError, UnicodeDecodeError): pass return getattr(string, method)(encodings[0], errors="ignore") clikit-0.6.2/src/clikit/utils/command.py000066400000000000000000000024201366776574300202220ustar00rootroot00000000000000from typing import List from pylev import levenshtein from clikit.api.command import CommandCollection def find_similar_command_names( name, commands ): # type: (str, CommandCollection) -> List[str] """ Finds names similar to a given command name. """ threshold = 1e3 distance_by_name = {} suggested_names = [] # Include aliases in the search actual_names = commands.get_names(True) for actual_name in actual_names: # Get Levenshtein distance between the input and each command name distance = levenshtein(name, actual_name) is_similar = distance <= len(name) / 3 is_sub_string = actual_name.find(name) != -1 if is_similar or is_sub_string: distance_by_name[actual_name] = ( distance, actual_name.find(name) if is_sub_string else float("inf"), ) # Only keep results with a distance below the threshold distance_by_name = { k: v for k, v in distance_by_name.items() if v[0] < 2 * threshold } # Display results with shortest distance first for k, v in sorted(distance_by_name.items(), key=lambda i: (i[1][0], i[1][1])): if k not in suggested_names: suggested_names.append(k) return suggested_names clikit-0.6.2/src/clikit/utils/string.py000066400000000000000000000046121366776574300201170ustar00rootroot00000000000000import re from typing import Any from typing import Optional from ._compat import basestring def parse_string(value, nullable=True): # type: (Any, bool) -> Optional[str] if nullable and (value is None or value == "null"): return if value is None: return "null" if isinstance(value, bool): return str(value).lower() return str(value) def parse_boolean(value, nullable=True): # type: (Any, bool) -> Optional[bool] if nullable and (value is None or value == "null"): return if isinstance(value, bool): return value if isinstance(value, int): value = str(value) if isinstance(value, basestring): if not value: return False if value in {"false", "0", "no", "off"}: return False if value in {"true", "1", "yes", "on"}: return True raise ValueError('The value "{}" cannot be parsed as boolean.'.format(value)) def parse_int(value, nullable=True): # type: (Any, bool) -> Optional[int] if nullable and (value is None or value == "null"): return try: return int(value) except ValueError: raise ValueError('The value "{}" cannot be parsed as integer.'.format(value)) def parse_float(value, nullable=True): # type: (Any, bool) -> Optional[float] if nullable and (value is None or value == "null"): return try: return float(value) except ValueError: raise ValueError('The value "{}" cannot be parsed as float.'.format(value)) def get_string_length( string, formatter=None ): # type: (str, Optional[Formatter]) -> int if formatter is not None: string = formatter.remove_format(string) return len(string) def get_max_word_length( string, formatter=None ): # type: (str, Optional[Formatter]) -> int if formatter is not None: string = formatter.remove_format(string) max_length = 0 words = re.split("\s+", string) for word in words: max_length = max(max_length, get_string_length(word)) return max_length def get_max_line_length( string, formatter=None ): # type: (str, Optional[Formatter]) -> int if formatter is not None: string = formatter.remove_format(string) max_length = 0 words = re.split("\n", string) for word in words: max_length = max(max_length, get_string_length(word)) return max_length clikit-0.6.2/src/clikit/utils/terminal.py000066400000000000000000000071601366776574300204250ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import platform import struct import subprocess import shlex class Terminal(object): """ Represents the current terminal. """ def __init__(self): self._width = None self._height = None @property def width(self): width = os.getenv("COLUMNS", "").strip() if width: return int(width) if self._width is None: self._init_dimensions() return self._width @property def height(self): height = os.getenv("LINES", "").strip() if height: return int(height) if self._height is None: self._init_dimensions() return self._height def _init_dimensions(self): current_os = platform.system().lower() dimensions = None if current_os.lower() == "windows": dimensions = self._get_terminal_size_windows() if dimensions is None: dimensions = self._get_terminal_size_tput() elif current_os.lower() in ["linux", "darwin"] or current_os.startswith( "cygwin" ): dimensions = self._get_terminal_size_linux() if dimensions is None: dimensions = 80, 25 # Ensure we have a valid width if dimensions[0] <= 0: dimensions = 80, dimensions[1] self._width, self._height = dimensions def _get_terminal_size_windows(self): try: from ctypes import windll, create_string_buffer # stdin handle is -10 # stdout handle is -11 # stderr handle is -12 h = windll.kernel32.GetStdHandle(-12) csbi = create_string_buffer(22) res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) if res: ( bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy, ) = struct.unpack("hhhhHhhhhhh", csbi.raw) sizex = right - left + 1 sizey = bottom - top + 1 return sizex, sizey except: pass def _get_terminal_size_tput(self): # get terminal width # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window try: cols = int( subprocess.check_output( shlex.split("tput cols"), stderr=subprocess.STDOUT ) ) rows = int( subprocess.check_output( shlex.split("tput lines"), stderr=subprocess.STDOUT ) ) return (cols, rows) except: pass def _get_terminal_size_linux(self): def ioctl_GWINSZ(fd): try: import fcntl import termios cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")) return cr except: pass cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) if not cr: try: fd = os.open(os.ctermid(), os.O_RDONLY) cr = ioctl_GWINSZ(fd) os.close(fd) except: pass if not cr: try: cr = (os.environ["LINES"], os.environ["COLUMNS"]) except: return None return int(cr[1]), int(cr[0]) clikit-0.6.2/src/clikit/utils/time.py000066400000000000000000000007341366776574300175500ustar00rootroot00000000000000import math _TIME_FORMATS = [ (0, "< 1 sec"), (2, "1 sec"), (59, "secs", 1), (60, "1 min"), (3600, "mins", 60), (5400, "1 hr"), (86400, "hrs", 3600), (129600, "1 day"), (604800, "days", 86400), ] def format_time(secs): # type: (int) -> str for fmt in _TIME_FORMATS: if secs > fmt[0]: continue if len(fmt) == 2: return fmt[1] return "{} {}".format(math.ceil(secs / fmt[2]), fmt[1]) clikit-0.6.2/tests/000077500000000000000000000000001366776574300141705ustar00rootroot00000000000000clikit-0.6.2/tests/__init__.py000066400000000000000000000000001366776574300162670ustar00rootroot00000000000000clikit-0.6.2/tests/api/000077500000000000000000000000001366776574300147415ustar00rootroot00000000000000clikit-0.6.2/tests/api/__init__.py000066400000000000000000000000001366776574300170400ustar00rootroot00000000000000clikit-0.6.2/tests/api/args/000077500000000000000000000000001366776574300156755ustar00rootroot00000000000000clikit-0.6.2/tests/api/args/__init__.py000066400000000000000000000000001366776574300177740ustar00rootroot00000000000000clikit-0.6.2/tests/api/args/format/000077500000000000000000000000001366776574300171655ustar00rootroot00000000000000clikit-0.6.2/tests/api/args/format/__init__.py000066400000000000000000000000001366776574300212640ustar00rootroot00000000000000clikit-0.6.2/tests/api/args/format/test_args_format_builder.py000066400000000000000000000522741366776574300246220ustar00rootroot00000000000000import pytest from clikit.api.args.exceptions import CannotAddArgumentException from clikit.api.args.exceptions import CannotAddOptionException from clikit.api.args.exceptions import NoSuchArgumentException from clikit.api.args.exceptions import NoSuchOptionException from clikit.api.args.format import ArgsFormat from clikit.api.args.format import ArgsFormatBuilder from clikit.api.args.format import Argument from clikit.api.args.format import Option from clikit.api.args.format.command_name import CommandName from clikit.api.args.format.command_option import CommandOption @pytest.fixture() def base_format_builder(): return ArgsFormatBuilder() @pytest.fixture() def base_format(): return ArgsFormat() @pytest.fixture() def builder(base_format): return ArgsFormatBuilder(base_format) def test_add_command_name(builder): server = CommandName("server") add = CommandName("add") builder.add_command_name(server) builder.add_command_name(add) assert [server, add] == builder.get_command_names() def test_add_command_names(builder): server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) assert [server, add] == builder.get_command_names() def test_set_command_names(builder): builder.add_command_name(CommandName("cluster")) server = CommandName("server") add = CommandName("add") builder.set_command_names(server, add) assert [server, add] == builder.get_command_names() def test_has_command_names(builder): assert not builder.has_command_names() assert not builder.has_command_names(False) builder.add_command_name(CommandName("cluster")) assert builder.has_command_names() assert builder.has_command_names(False) def test_has_command_names_with_base_definition(base_format_builder): base_format_builder.add_command_name(CommandName("server")) builder = ArgsFormatBuilder(base_format_builder.format) assert builder.has_command_names() assert not builder.has_command_names(False) builder.add_command_name(CommandName("add")) assert builder.has_command_names() assert builder.has_command_names(False) def test_get_command_names(builder): server = CommandName("server") add = CommandName("add") builder.add_command_name(server) builder.add_command_name(add) assert [server, add] == builder.get_command_names() assert [server, add] == builder.get_command_names(False) def test_get_command_names_with_base_definition(base_format_builder): server = CommandName("server") add = CommandName("add") base_format_builder.add_command_name(server) builder = ArgsFormatBuilder(base_format_builder.format) builder.add_command_name(add) assert [server, add] == builder.get_command_names() assert [add] == builder.get_command_names(False) def test_add_command_option(builder): option = CommandOption("option") builder.add_command_option(option) assert [option] == builder.get_command_options() def test_add_command_option_preserves_existing_options(builder): option1 = CommandOption("option1") option2 = CommandOption("option2") builder.add_command_option(option1) builder.add_command_option(option2) assert [option1, option2] == builder.get_command_options() def test_add_command_fails_if_option_with_same_long_name_as_other_command_option( builder, ): builder.add_option(CommandOption("option", "a")) with pytest.raises(CannotAddOptionException): builder.add_command_option(CommandOption("option", "b")) def test_add_command_fails_if_option_with_same_long_name_as_other_option(builder): builder.add_option(Option("option", "a")) with pytest.raises(CannotAddOptionException): builder.add_command_option(CommandOption("option", "b")) def test_add_command_fails_if_option_with_same_long_name_as_option_in_base_format( base_format_builder, ): base_format_builder.add_option(Option("option", "a")) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(CannotAddOptionException): builder.add_command_option(CommandOption("option", "b")) def test_add_command_fails_if_option_with_same_short_name_as_other_command_option( builder, ): builder.add_option(CommandOption("option", "a")) with pytest.raises(CannotAddOptionException): builder.add_command_option(CommandOption("option2", "a")) def test_add_command_fails_if_option_with_same_short_name_as_other_option(builder): builder.add_option(CommandOption("option", "a")) with pytest.raises(CannotAddOptionException): builder.add_command_option(CommandOption("option2", "a")) def test_add_command_fails_if_option_with_same_short_name_as_alias_of_other_option( builder, ): builder.add_option(CommandOption("option1", "a")) with pytest.raises(CannotAddOptionException): builder.add_command_option(CommandOption("option2", "b", ["a"])) def test_add_command_fails_if_option_with_same_short_name_as_option_in_base_format( base_format_builder, ): base_format_builder.add_option(Option("option", "a")) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(CannotAddOptionException): builder.add_command_option(CommandOption("option2", "a")) def test_add_optional_argument(builder): arg = Argument("argument") builder.add_argument(arg) assert {"argument": arg} == builder.get_arguments() def test_add_required_argument(builder): arg = Argument("argument", Argument.REQUIRED) builder.add_argument(arg) assert {"argument": arg} == builder.get_arguments() def test_argument_preserves_existing_arguments(builder): arg1 = Argument("argument1") arg2 = Argument("argument2") builder.add_argument(arg1) builder.add_argument(arg2) assert {"argument1": arg1, "argument2": arg2} == builder.get_arguments() def test_fail_if_adding_required_argument_after_optional_argument(builder): builder.add_argument(Argument("argument1")) with pytest.raises(CannotAddArgumentException): builder.add_argument(Argument("argument2", Argument.REQUIRED)) def test_fail_if_adding_required_argument_after_optional_argument_in_base_definition( base_format_builder, ): base_format_builder.add_argument(Argument("argument1")) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(CannotAddArgumentException): builder.add_argument(Argument("argument2", Argument.REQUIRED)) def test_fail_if_adding_required_argument_after_multi_valued_argument(builder): builder.add_argument(Argument("argument1", Argument.MULTI_VALUED)) with pytest.raises(CannotAddArgumentException): builder.add_argument(Argument("argument2", Argument.REQUIRED)) def test_fail_if_adding_required_argument_after_multi_valued_argument_in_base_definition( base_format_builder, ): base_format_builder.add_argument(Argument("argument1", Argument.MULTI_VALUED)) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(CannotAddArgumentException): builder.add_argument(Argument("argument2", Argument.REQUIRED)) def test_fail_if_adding_optional_argument_after_multi_valued_argument(builder): builder.add_argument(Argument("argument1", Argument.MULTI_VALUED)) with pytest.raises(CannotAddArgumentException): builder.add_argument(Argument("argument2")) def test_fail_if_adding_optional_argument_after_multi_valued_argument_in_base_definition( base_format_builder, ): base_format_builder.add_argument(Argument("argument1", Argument.MULTI_VALUED)) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(CannotAddArgumentException): builder.add_argument(Argument("argument2")) def test_fail_if_adding_argument_with_existing_name(builder): builder.add_argument(Argument("argument", Argument.REQUIRED)) with pytest.raises(CannotAddArgumentException): builder.add_argument(Argument("argument", Argument.OPTIONAL)) def test_fail_if_adding_argument_with_existing_name_in_base_definition( base_format_builder, ): base_format_builder.add_argument(Argument("argument", Argument.REQUIRED)) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(CannotAddArgumentException): builder.add_argument(Argument("argument", Argument.OPTIONAL)) def test_add_arguments(builder): arg1 = Argument("argument1") arg2 = Argument("argument2") arg3 = Argument("argument3") builder.add_argument(arg1) builder.add_arguments(arg2, arg3) assert { "argument1": arg1, "argument2": arg2, "argument3": arg3, } == builder.get_arguments() def test_set_arguments(builder): arg1 = Argument("argument1") arg2 = Argument("argument2") arg3 = Argument("argument3") builder.add_argument(arg1) builder.set_arguments(arg2, arg3) assert {"argument2": arg2, "argument3": arg3} == builder.get_arguments() def test_get_argument(builder): arg1 = Argument("argument1") arg2 = Argument("argument2") builder.add_arguments(arg1, arg2) assert arg1 == builder.get_argument("argument1") assert arg2 == builder.get_argument("argument2") def test_get_argument_from_base_definition(base_format_builder): arg = Argument("argument") base_format_builder.add_argument(arg) builder = ArgsFormatBuilder(base_format_builder.format) assert arg == builder.get_argument("argument") def test_get_argument_fails_if_unknown_name(builder): with pytest.raises(NoSuchArgumentException): builder.get_argument("foo") def test_get_argument_by_position(builder): arg1 = Argument("argument1") arg2 = Argument("argument2") builder.add_arguments(arg1, arg2) assert arg1 == builder.get_argument(0) assert arg2 == builder.get_argument(1) def test_get_argument_by_position_from_base_definition(base_format_builder): arg = Argument("argument") base_format_builder.add_argument(arg) builder = ArgsFormatBuilder(base_format_builder.format) assert arg == builder.get_argument(0) def test_get_argument_by_position_fails_if_unknown_position(builder): with pytest.raises(NoSuchArgumentException): builder.get_argument(0) def test_get_argument_fails_if_in_base_definition_but_include_base_disabled( base_format_builder, ): arg = Argument("argument") base_format_builder.add_argument(arg) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(NoSuchArgumentException): builder.get_argument("argument", False) def test_get_arguments(builder): arg1 = Argument("argument1") arg2 = Argument("argument2") builder.add_arguments(arg1, arg2) assert {"argument1": arg1, "argument2": arg2} == builder.get_arguments() def test_get_arguments_with_base_arguments(base_format_builder): arg1 = Argument("argument1") arg2 = Argument("argument2") base_format_builder.add_argument(arg1) builder = ArgsFormatBuilder(base_format_builder.format) builder.add_argument(arg2) assert {"argument1": arg1, "argument2": arg2} == builder.get_arguments() assert {"argument2": arg2} == builder.get_arguments(False) def test_has_argument(builder): arg1 = Argument("argument1") arg2 = Argument("argument2") builder.add_arguments(arg1, arg2) assert builder.has_argument("argument1") assert builder.has_argument("argument2") def test_has_argument_from_base_definition(base_format_builder): arg1 = Argument("argument1") arg2 = Argument("argument2") base_format_builder.add_argument(arg1) builder = ArgsFormatBuilder(base_format_builder.format) builder.add_argument(arg2) assert builder.has_argument("argument1") assert builder.has_argument("argument2") assert builder.has_argument("argument2", False) assert not builder.has_argument("argument1", False) def test_has_argument_at_position(builder): arg1 = Argument("argument1") arg2 = Argument("argument2") builder.add_arguments(arg1, arg2) assert builder.has_argument(0) assert builder.has_argument(1) def test_has_argument_at_position_from_base_definition(base_format_builder): arg1 = Argument("argument1") arg2 = Argument("argument2") base_format_builder.add_argument(arg1) builder = ArgsFormatBuilder(base_format_builder.format) builder.add_argument(arg2) assert builder.has_argument(0) assert builder.has_argument(1) assert not builder.has_argument(1, False) assert builder.has_argument(0, False) def test_has_arguments(builder): arg1 = Argument("argument1") arg2 = Argument("argument2") builder.add_arguments(arg1, arg2) assert builder.has_arguments() assert builder.has_arguments() def test_has_arguments_with_base_definition(base_format_builder): arg1 = Argument("argument1") arg2 = Argument("argument2") base_format_builder.add_argument(arg1) builder = ArgsFormatBuilder(base_format_builder.format) assert builder.has_arguments() assert not builder.has_arguments(False) builder.add_argument(arg2) assert builder.has_arguments() assert builder.has_arguments(False) def test_has_multi_valued_argument(builder): arg1 = Argument("argument1") arg2 = Argument("argument2", Argument.MULTI_VALUED) builder.add_argument(arg1) assert not builder.has_multi_valued_argument() builder.add_argument(arg2) assert builder.has_multi_valued_argument() def test_has_multi_valued_argument_with_base_definition(base_format_builder): base_format_builder.add_argument(Argument("argument", Argument.MULTI_VALUED)) builder = ArgsFormatBuilder(base_format_builder.format) assert builder.has_multi_valued_argument() assert not builder.has_multi_valued_argument(False) def test_has_optional_argument(builder): arg1 = Argument("argument1", Argument.REQUIRED) arg2 = Argument("argument2", Argument.OPTIONAL) builder.add_argument(arg1) assert not builder.has_optional_argument() builder.add_argument(arg2) assert builder.has_optional_argument() def test_has_optional_argument_with_base_definition(base_format_builder): base_format_builder.add_argument(Argument("argument", Argument.OPTIONAL)) builder = ArgsFormatBuilder(base_format_builder.format) assert builder.has_optional_argument() assert not builder.has_optional_argument(False) def test_has_required_argument(builder): arg = Argument("argument", Argument.REQUIRED) assert not builder.has_required_argument() builder.add_argument(arg) assert builder.has_required_argument() def test_has_required_argument_with_base_definition(base_format_builder): base_format_builder.add_argument(Argument("argument", Argument.REQUIRED)) builder = ArgsFormatBuilder(base_format_builder.format) assert builder.has_required_argument() assert not builder.has_required_argument(False) def test_add_option(builder): opt = Option("option") builder.add_option(opt) assert {"option": opt} == builder.get_options() def test_add_option_preserve_existing_options(builder): opt1 = Option("option1") opt2 = Option("option2") builder.add_option(opt1) builder.add_option(opt2) assert {"option1": opt1, "option2": opt2} == builder.get_options() def test_add_option_fails_if_same_long_name(builder): builder.add_option(Option("option", "a")) with pytest.raises(CannotAddOptionException): builder.add_option(Option("option", "b")) def test_add_option_fails_if_same_long_name_as_command_option(builder): builder.add_option(CommandOption("option", "a")) with pytest.raises(CannotAddOptionException): builder.add_option(Option("option", "b")) def test_add_option_fails_if_same_long_name_as_command_option_alias(builder): builder.add_command_option(CommandOption("option", "a", ["alias"])) with pytest.raises(CannotAddOptionException): builder.add_option(Option("alias", "b")) def test_add_option_fails_if_same_long_name_in_base_format(base_format_builder): base_format_builder.add_option(Option("option", "a")) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(CannotAddOptionException): builder.add_option(Option("option", "b")) def test_add_option_fails_if_same_long_name_as_command_option_in_base_format( base_format_builder, ): base_format_builder.add_option(Option("option", "a")) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(CannotAddOptionException): builder.add_option(Option("option", "b")) def test_add_option_fails_if_same_long_name_as_command_option_alias_in_base_format( base_format_builder, ): base_format_builder.add_command_option(CommandOption("option", "a", ["alias"])) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(CannotAddOptionException): builder.add_option(Option("alias", "b")) def test_add_option_fails_if_same_short_name(builder): builder.add_option(Option("option1", "a")) with pytest.raises(CannotAddOptionException): builder.add_option(Option("option2", "a")) def test_add_option_fails_if_same_short_name_as_command_option(builder): builder.add_option(CommandOption("option1", "a")) with pytest.raises(CannotAddOptionException): builder.add_option(Option("option1", "a")) def test_add_option_fails_if_same_short_name_in_base_format(base_format_builder): base_format_builder.add_option(Option("option1", "a")) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(CannotAddOptionException): builder.add_option(Option("option2", "a")) def test_add_options(builder): opt1 = Option("option1") opt2 = Option("option2") opt3 = Option("option3") builder.add_option(opt1) builder.add_options(opt2, opt3) assert {"option1": opt1, "option2": opt2, "option3": opt3} == builder.get_options() def test_set_options(builder): opt1 = Option("option1") opt2 = Option("option2") opt3 = Option("option3") builder.add_option(opt1) builder.set_options(opt2, opt3) assert {"option2": opt2, "option3": opt3} == builder.get_options() def test_get_options(builder): opt1 = Option("option1") opt2 = Option("option2") builder.add_options(opt1, opt2) assert {"option1": opt1, "option2": opt2} == builder.get_options() def test_get_options_with_base_format(base_format_builder): opt1 = Option("option1") opt2 = Option("option2") base_format_builder.add_option(opt1) builder = ArgsFormatBuilder(base_format_builder.format) builder.add_option(opt2) assert {"option1": opt1, "option2": opt2} == builder.get_options() assert {"option2": opt2} == builder.get_options(False) def test_get_option(builder): opt = Option("option") builder.add_option(opt) assert opt == builder.get_option("option") def test_get_option_from_base_format(base_format_builder): opt = Option("option") base_format_builder.add_option(opt) builder = ArgsFormatBuilder(base_format_builder.format) assert opt == builder.get_option("option") def test_get_option_by_short_name(builder): opt = Option("option", "o") builder.add_option(opt) assert opt == builder.get_option("o") def test_get_option_by_short_name_from_base_format(base_format_builder): opt = Option("option", "o") base_format_builder.add_option(opt) builder = ArgsFormatBuilder(base_format_builder.format) assert opt == builder.get_option("o") def test_get_option_fails_with_unknown_name(builder): with pytest.raises(NoSuchOptionException): builder.get_option("foo") def test_get_option_fails_if_in_base_format_but_include_base_disabled( base_format_builder, ): base_format_builder.add_option(Option("option")) builder = ArgsFormatBuilder(base_format_builder.format) with pytest.raises(NoSuchOptionException): builder.get_option("option", False) def test_has_option(builder): opt = Option("option") builder.add_option(opt) assert builder.has_option("option") def test_has_option_from_base_format(base_format_builder): opt = Option("option") base_format_builder.add_option(opt) builder = ArgsFormatBuilder(base_format_builder.format) assert builder.has_option("option") assert not builder.has_option("option", False) def test_has_option_by_short_name(builder): opt = Option("option", "o") builder.add_option(opt) assert builder.has_option("o") def test_has_option_by_short_name_from_base_format(base_format_builder): opt = Option("option", "o") base_format_builder.add_option(opt) builder = ArgsFormatBuilder(base_format_builder.format) assert builder.has_option("o") assert not builder.has_option("o", False) def test_has_options(builder): assert not builder.has_options() assert not builder.has_options(False) builder.add_option(Option("option")) assert builder.has_options() assert builder.has_options(False) def test_has_options_with_base_format(base_format_builder): base_format_builder.add_option(Option("option")) builder = ArgsFormatBuilder(base_format_builder.format) assert builder.has_options() assert not builder.has_options(False) builder.add_option(Option("option2")) assert builder.has_options() assert builder.has_options(False) clikit-0.6.2/tests/api/args/format/test_argument.py000066400000000000000000000125361366776574300224270ustar00rootroot00000000000000import pytest from clikit.api.args.format import Argument def test_create(): arg = Argument("argument") assert "argument" == arg.name assert not arg.is_required() assert arg.is_optional() assert not arg.is_multi_valued() assert arg.default is None assert arg.description is None def test_fail_if_name_is_null(): with pytest.raises(ValueError): Argument(None) def test_fail_if_name_is_empty(): with pytest.raises(ValueError): Argument("") def test_fail_if_name_is_not_a_string(): with pytest.raises(ValueError): Argument(1234) def test_fail_if_name_contains_spaces(): with pytest.raises(ValueError): Argument("foo bar") def test_fail_if_name_starts_with_hyphen(): with pytest.raises(ValueError): Argument("-argument") def test_fail_if_name_starts_with_number(): with pytest.raises(ValueError): Argument("1argument") @pytest.mark.parametrize( "flags", [ Argument.REQUIRED | Argument.OPTIONAL, Argument.STRING | Argument.BOOLEAN, Argument.STRING | Argument.INTEGER, Argument.STRING | Argument.FLOAT, Argument.BOOLEAN | Argument.INTEGER, Argument.BOOLEAN | Argument.FLOAT, Argument.INTEGER | Argument.FLOAT, ], ) def test_fail_if_invalid_flags_combination(flags): with pytest.raises(ValueError): Argument("argument", flags) def test_required_argument(): arg = Argument("argument", Argument.REQUIRED) assert "argument" == arg.name assert arg.is_required() assert not arg.is_optional() assert not arg.is_multi_valued() assert arg.default is None assert arg.description is None def test_fail_if_required_argument_and_default_value(): with pytest.raises(ValueError): Argument("argument", Argument.REQUIRED, default="Default") def test_optional_argument(): arg = Argument("argument", Argument.OPTIONAL) assert "argument" == arg.name assert not arg.is_required() assert arg.is_optional() assert not arg.is_multi_valued() assert arg.default is None assert arg.description is None def test_optional_argument_with_default_value(): arg = Argument("argument", Argument.OPTIONAL, default="Default") assert "argument" == arg.name assert not arg.is_required() assert arg.is_optional() assert not arg.is_multi_valued() assert "Default" == arg.default assert arg.description is None def test_multi_valued_argument(): arg = Argument("argument", Argument.MULTI_VALUED) assert "argument" == arg.name assert not arg.is_required() assert arg.is_optional() assert arg.is_multi_valued() assert arg.default == [] assert arg.description is None def test_required_multi_valued_argument(): arg = Argument("argument", Argument.REQUIRED | Argument.MULTI_VALUED) assert "argument" == arg.name assert arg.is_required() assert not arg.is_optional() assert arg.is_multi_valued() assert arg.default == [] assert arg.description is None def test_optional_multi_valued_argument(): arg = Argument("argument", Argument.OPTIONAL | Argument.MULTI_VALUED) assert "argument" == arg.name assert not arg.is_required() assert arg.is_optional() assert arg.is_multi_valued() assert arg.default == [] assert arg.description is None def test_optional_multi_valued_argument_with_default(): arg = Argument( "argument", Argument.OPTIONAL | Argument.MULTI_VALUED, default=["foo", "bar"] ) assert "argument" == arg.name assert not arg.is_required() assert arg.is_optional() assert arg.is_multi_valued() assert ["foo", "bar"] == arg.default assert arg.description is None def test_fail_if_multi_values_argument_and_default_value_is_not_list(): with pytest.raises(ValueError): Argument("argument", Argument.MULTI_VALUED, default="Default") @pytest.mark.parametrize( "flags, string, output", [ (0, "", ""), (0, "string", "string"), (0, "1", "1"), (0, "1.23", "1.23"), (0, "null", "null"), (Argument.NULLABLE, "null", None), (0, "true", "true"), (0, "false", "false"), (Argument.STRING, "", ""), (Argument.STRING, "string", "string"), (Argument.STRING, "1", "1"), (Argument.STRING, "1.23", "1.23"), (Argument.STRING, "null", "null"), (Argument.STRING | Argument.NULLABLE, "null", None), (Argument.STRING, "true", "true"), (Argument.STRING, "false", "false"), (Argument.BOOLEAN, "true", True), (Argument.BOOLEAN, "false", False), (Argument.BOOLEAN | Argument.NULLABLE, "null", None), (Argument.INTEGER, "1", 1), (Argument.INTEGER, "0", 0), (Argument.INTEGER | Argument.NULLABLE, "null", None), (Argument.FLOAT, "1", 1.0), (Argument.FLOAT, "1.23", 1.23), (Argument.FLOAT, "0", 0.0), (Argument.FLOAT | Argument.NULLABLE, "null", None), ], ) def test_parse_value(flags, string, output): arg = Argument("argument", flags) assert output == arg.parse(string) @pytest.mark.parametrize( "flags, string", [(Argument.BOOLEAN, "null"), (Argument.INTEGER, "null"), (Argument.FLOAT, "null")], ) def test_parse_value_fails_if_invalid(flags, string): arg = Argument("argument", flags) with pytest.raises(ValueError): arg.parse(string) clikit-0.6.2/tests/api/args/format/test_option.py000066400000000000000000000142551366776574300221150ustar00rootroot00000000000000import pytest from clikit.api.args.format import Option def test_create(): opt = Option("option") assert "option" == opt.long_name assert opt.short_name is None assert opt.is_long_name_preferred() assert not opt.is_short_name_preferred() assert not opt.accepts_value() assert not opt.is_value_required() assert not opt.is_value_optional() assert not opt.is_multi_valued() assert opt.default is None assert "..." == opt.value_name def test_dashed_long_name(): opt = Option("--option") assert "option" == opt.long_name def test_fail_if_long_name_is_null(): with pytest.raises(ValueError): Option(None) def test_fail_if_long_name_is_empty(): with pytest.raises(ValueError): Option("") def test_fail_if_long_name_is_not_a_string(): with pytest.raises(ValueError): Option(1234) def test_fail_if_long_name_contains_spaces(): with pytest.raises(ValueError): Option("foo bar") def test_fail_if_long_name_starts_with_number(): with pytest.raises(ValueError): Option("1option") def test_fail_if_long_name_is_one_character(): with pytest.raises(ValueError): Option("o") def test_fail_if_long_name_starts_with_single_hyphen(): with pytest.raises(ValueError): Option("-option") def test_fail_if_long_name_starts_with_three_hyphens(): with pytest.raises(ValueError): Option("-option") @pytest.mark.parametrize( "flags", [ Option.NO_VALUE | Option.OPTIONAL_VALUE, Option.NO_VALUE | Option.REQUIRED_VALUE, Option.NO_VALUE | Option.MULTI_VALUED, Option.PREFER_LONG_NAME | Option.PREFER_SHORT_NAME, Option.STRING | Option.BOOLEAN, Option.STRING | Option.INTEGER, Option.STRING | Option.FLOAT, Option.BOOLEAN | Option.INTEGER, Option.BOOLEAN | Option.FLOAT, Option.INTEGER | Option.FLOAT, ], ) def test_fail_if_invalid_flag_combination(flags): with pytest.raises(ValueError): Option("option", "o", flags) def test_short_name(): opt = Option("option", "o") assert "o" == opt.short_name def test_dashed_short_name(): opt = Option("option", "-o") assert "o" == opt.short_name def test_fail_if_short_name_is_empty(): with pytest.raises(ValueError): Option("option", "") def test_fail_if_short_name_is_not_string(): with pytest.raises(ValueError): Option("option", 1234) def test_fail_if_short_name_is_longer_than_one_letter(): with pytest.raises(ValueError): Option("option", "ab") def test_no_value(): opt = Option("option", flags=Option.NO_VALUE) assert not opt.accepts_value() assert not opt.is_value_required() assert not opt.is_value_optional() assert not opt.is_multi_valued() assert opt.default is None def test_fail_if_no_value_and_default_value(): with pytest.raises(ValueError): Option("option", flags=Option.NO_VALUE, default="Default") def test_optional_value(): opt = Option("option", flags=Option.OPTIONAL_VALUE) assert opt.accepts_value() assert not opt.is_value_required() assert opt.is_value_optional() assert not opt.is_multi_valued() assert opt.default is None def test_optional_value_with_default(): opt = Option("option", flags=Option.OPTIONAL_VALUE, default="Default") assert opt.accepts_value() assert not opt.is_value_required() assert opt.is_value_optional() assert not opt.is_multi_valued() assert "Default" == opt.default def test_required_value(): opt = Option("option", flags=Option.REQUIRED_VALUE) assert opt.accepts_value() assert opt.is_value_required() assert not opt.is_value_optional() assert not opt.is_multi_valued() assert opt.default is None def test_required_value_with_default(): opt = Option("option", flags=Option.REQUIRED_VALUE, default="Default") assert opt.accepts_value() assert opt.is_value_required() assert not opt.is_value_optional() assert not opt.is_multi_valued() assert "Default" == opt.default def test_multi_valued(): opt = Option("option", flags=Option.MULTI_VALUED) assert opt.accepts_value() assert opt.is_value_required() assert not opt.is_value_optional() assert opt.is_multi_valued() assert [] == opt.default def test_multi_valued_with_default(): opt = Option("option", flags=Option.MULTI_VALUED, default=["foo", "bar"]) assert opt.accepts_value() assert opt.is_value_required() assert not opt.is_value_optional() assert opt.is_multi_valued() assert ["foo", "bar"] == opt.default def test_fail_if_multi_valued_with_default_not_list(): with pytest.raises(ValueError): Option("option", flags=Option.MULTI_VALUED, default="Default") @pytest.mark.parametrize( "flags, string, output", [ (0, "", ""), (0, "string", "string"), (0, "1", "1"), (0, "1.23", "1.23"), (0, "null", "null"), (Option.NULLABLE, "null", None), (0, "true", "true"), (0, "false", "false"), (Option.STRING, "", ""), (Option.STRING, "string", "string"), (Option.STRING, "1", "1"), (Option.STRING, "1.23", "1.23"), (Option.STRING, "null", "null"), (Option.STRING | Option.NULLABLE, "null", None), (Option.STRING, "true", "true"), (Option.STRING, "false", "false"), (Option.BOOLEAN, "true", True), (Option.BOOLEAN, "false", False), (Option.BOOLEAN | Option.NULLABLE, "null", None), (Option.INTEGER, "1", 1), (Option.INTEGER, "0", 0), (Option.INTEGER | Option.NULLABLE, "null", None), (Option.FLOAT, "1", 1.0), (Option.FLOAT, "1.23", 1.23), (Option.FLOAT, "0", 0.0), (Option.FLOAT | Option.NULLABLE, "null", None), ], ) def test_parse_value(flags, string, output): arg = Option("option", flags=flags) assert output == arg.parse(string) @pytest.mark.parametrize( "flags, string", [(Option.BOOLEAN, "null"), (Option.INTEGER, "null"), (Option.FLOAT, "null")], ) def test_parse_value_fails_if_invalid(flags, string): arg = Option("argument", flags=flags) with pytest.raises(ValueError): arg.parse(string) clikit-0.6.2/tests/api/event/000077500000000000000000000000001366776574300160625ustar00rootroot00000000000000clikit-0.6.2/tests/api/event/__init__.py000066400000000000000000000000001366776574300201610ustar00rootroot00000000000000clikit-0.6.2/tests/api/event/test_event.py000066400000000000000000000004151366776574300206140ustar00rootroot00000000000000from clikit.api.event import Event def test_is_propagation_stopped(): e = Event() assert not e.is_propagation_stopped() def test_stop_propagation_and_is_propagation_stopped(): e = Event() e.stop_propagation() assert e.is_propagation_stopped() clikit-0.6.2/tests/api/event/test_event_dispatcher.py000066400000000000000000000067461366776574300230370ustar00rootroot00000000000000import pytest from clikit.api.event import EventDispatcher @pytest.fixture() def dispatcher(): return EventDispatcher() @pytest.fixture() def listener(): return EventListener() PRE_FOO = "pre.foo" POST_FOO = "post.foo" PRE_BAR = "pre.bar" POST_BAR = "post.bar" class EventListener: def __init__(self): self.pre_foo_invoked = False self.post_foo_invoked = False def pre_foo(self, *_): self.pre_foo_invoked = True def post_foo(self, e, *_): self.post_foo_invoked = True e.stop_propagation() def test_initial_state(dispatcher): assert {} == dispatcher.get_listeners() assert not dispatcher.has_listeners(PRE_FOO) assert not dispatcher.has_listeners(POST_FOO) def test_add_listener(dispatcher, listener): dispatcher.add_listener(PRE_FOO, listener.pre_foo) dispatcher.add_listener(POST_FOO, listener.post_foo) assert dispatcher.has_listeners() assert dispatcher.has_listeners(PRE_FOO) assert dispatcher.has_listeners(POST_FOO) assert 1 == len(dispatcher.get_listeners(PRE_FOO)) assert 1 == len(dispatcher.get_listeners(POST_FOO)) assert 2 == len(dispatcher.get_listeners()) def test_get_listeners_sorts_by_priority(dispatcher): listener1 = EventListener() listener2 = EventListener() listener3 = EventListener() dispatcher.add_listener(PRE_FOO, listener1.pre_foo, -10) dispatcher.add_listener(PRE_FOO, listener2.pre_foo, 10) dispatcher.add_listener(PRE_FOO, listener3.pre_foo) expected = [listener2.pre_foo, listener3.pre_foo, listener1.pre_foo] assert expected == dispatcher.get_listeners(PRE_FOO) def test_get_all_listeners_sorts_by_priority(dispatcher): listener1 = EventListener() listener2 = EventListener() listener3 = EventListener() listener4 = EventListener() listener5 = EventListener() listener6 = EventListener() dispatcher.add_listener(PRE_FOO, listener1.pre_foo, -10) dispatcher.add_listener(PRE_FOO, listener2.pre_foo) dispatcher.add_listener(PRE_FOO, listener3.pre_foo, 10) dispatcher.add_listener(POST_FOO, listener4.pre_foo, -10) dispatcher.add_listener(POST_FOO, listener5.pre_foo) dispatcher.add_listener(POST_FOO, listener6.pre_foo, 10) expected = { PRE_FOO: [listener3.pre_foo, listener2.pre_foo, listener1.pre_foo], POST_FOO: [listener6.pre_foo, listener5.pre_foo, listener4.pre_foo], } assert expected == dispatcher.get_listeners() def test_get_listener_priority(dispatcher): listener1 = EventListener() listener2 = EventListener() dispatcher.add_listener(PRE_FOO, listener1.pre_foo, -10) dispatcher.add_listener(PRE_FOO, listener2.pre_foo) assert -10 == dispatcher.get_listener_priority(PRE_FOO, listener1.pre_foo) assert 0 == dispatcher.get_listener_priority(PRE_FOO, listener2.pre_foo) assert dispatcher.get_listener_priority(PRE_BAR, listener2.pre_foo) is None def test_dispatch(dispatcher, listener): dispatcher.add_listener(PRE_FOO, listener.pre_foo) dispatcher.add_listener(POST_FOO, listener.post_foo) dispatcher.dispatch(PRE_FOO) assert listener.pre_foo_invoked assert not listener.post_foo_invoked def test_stop_event_propagation(dispatcher, listener): other_listener = EventListener() dispatcher.add_listener(POST_FOO, listener.post_foo, 10) dispatcher.add_listener(POST_FOO, other_listener.post_foo) dispatcher.dispatch(POST_FOO) assert listener.post_foo_invoked assert not other_listener.post_foo_invoked clikit-0.6.2/tests/api/io/000077500000000000000000000000001366776574300153505ustar00rootroot00000000000000clikit-0.6.2/tests/api/io/__init__.py000066400000000000000000000000001366776574300174470ustar00rootroot00000000000000clikit-0.6.2/tests/api/io/test_input.py000066400000000000000000000000011366776574300201070ustar00rootroot00000000000000 clikit-0.6.2/tests/api/io/test_section_output.py000066400000000000000000000041501366776574300220450ustar00rootroot00000000000000import pytest from clikit.api.io.section_output import SectionOutput from clikit.formatter.ansi_formatter import AnsiFormatter from clikit.io.output_stream.buffered_output_stream import BufferedOutputStream @pytest.fixture() def stream(): return BufferedOutputStream() @pytest.fixture() def sections(): return [] @pytest.fixture() def output(stream, sections): return SectionOutput(stream, sections, AnsiFormatter(forced=True)) @pytest.fixture() def output2(stream, sections): return SectionOutput(stream, sections, AnsiFormatter(forced=True)) def test_clear_all(output, stream): output.write_line("Foo\nBar") output.clear() assert "Foo\nBar\n\x1b[2A\x1b[0J" == stream.fetch() def test_clear_with_number_of_lines(output, stream): output.write_line("Foo\nBar\nBaz\nFooBar") output.clear(2) assert "Foo\nBar\nBaz\nFooBar\n\x1b[2A\x1b[0J" == stream.fetch() def test_clear_with_number_of_lines_and_multiple_sections(output, output2, stream): output2.write_line("Foo") output2.write_line("Bar") output2.clear(1) output.write_line("Baz") assert "Foo\nBar\n\x1b[1A\x1b[0J\x1b[1A\x1b[0JBaz\nFoo\n" == stream.fetch() def test_clear_preserves_empty_lines(output, output2, stream): output2.write_line("\nFoo") output2.clear(1) output.write_line("Bar") assert "\nFoo\n\x1b[1A\x1b[0J\x1b[1A\x1b[0JBar\n\n" == stream.fetch() def test_overwrite(output, stream): output.write_line("Foo") output.overwrite("Bar") assert "Foo\n\x1b[1A\x1b[0JBar\n" == stream.fetch() def test_overwrite_multiple_lines(output, stream): output.write_line("Foo\nBar\nBaz") output.overwrite("Bar") assert "Foo\nBar\nBaz\n\x1b[3A\x1b[0JBar\n" == stream.fetch() def test_add_multiple_sections(output, output2, sections): assert 2 == len(sections) def test_multiple_sections_output(output, output2, stream): output.write_line("Foo") output2.write_line("Bar") output.overwrite("Baz") output2.overwrite("Foobar") assert ( "Foo\nBar\n\x1b[2A\x1b[0JBar\n\x1b[1A\x1b[0JBaz\nBar\n\x1b[1A\x1b[0JFoobar\n" == stream.fetch() ) clikit-0.6.2/tests/args/000077500000000000000000000000001366776574300151245ustar00rootroot00000000000000clikit-0.6.2/tests/args/__init__.py000066400000000000000000000000001366776574300172230ustar00rootroot00000000000000clikit-0.6.2/tests/args/test_argv_args.py000066400000000000000000000040151366776574300205100ustar00rootroot00000000000000import sys import pytest from clikit.args import ArgvArgs @pytest.fixture() def argv(): original_argv = sys.argv yield sys.argv = original_argv def test_create(argv): sys.argv = ("console", "server", "add", "--port", "80", "localhost") args = ArgvArgs() assert args.script_name == "console" assert ["server", "add", "--port", "80", "localhost"] == args.tokens def test_create_with_custom_tokens(argv): sys.argv = ("console", "server", "add", "localhost") args = ArgvArgs(["console", "server", "add", "--port", "80", "localhost"]) assert args.script_name == "console" assert ["server", "add", "--port", "80", "localhost"] == args.tokens def test_has_token(): args = ArgvArgs(["console", "server", "add", "--port", "80", "localhost"]) assert args.has_token("server") assert args.has_token("add") assert args.has_token("--port") assert args.has_token("80") assert args.has_token("localhost") assert not args.has_token("console") assert not args.has_token("foo") def test_has_option_token(): args = ArgvArgs( [ "console", "server", "add", "--port", "80", "localhost", "--", "-h", "--test", "remainder", ] ) assert args.has_option_token("server") assert args.has_option_token("add") assert args.has_option_token("--port") assert args.has_option_token("80") assert args.has_option_token("localhost") assert not args.has_option_token("console") assert not args.has_option_token("-h") assert not args.has_option_token("--test") assert not args.has_option_token("remainder") def test_to_string(): args = ArgvArgs(["console", "server", "add", "--port", "80", "localhost"]) assert "console server add --port 80 localhost" == args.to_string() assert "console server add --port 80 localhost" == args.to_string(True) assert "server add --port 80 localhost" == args.to_string(False) clikit-0.6.2/tests/args/test_default_args_parser.py000066400000000000000000000317361366776574300225630ustar00rootroot00000000000000import pytest from clikit.api.args.exceptions import CannotParseArgsException from clikit.api.args.exceptions import NoSuchOptionException from clikit.api.args.format import ArgsFormatBuilder from clikit.api.args.format import Argument from clikit.api.args.format import CommandName from clikit.api.args.format import Option from clikit.args import DefaultArgsParser from clikit.args import StringArgs @pytest.fixture() def parser(): return DefaultArgsParser() def test_parse_command_names(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) fmt = builder.format args = parser.parse(StringArgs("server add"), fmt) assert [server, add] == args.command_names assert {} == args.arguments(False) assert {} == args.options(False) def test_parse_command_name_aliases(parser): builder = ArgsFormatBuilder() server = CommandName("server", ["server-alias"]) add = CommandName("add", ["add-alias"]) builder.add_command_names(server, add) fmt = builder.format args = parser.parse(StringArgs("server-alias add-alias"), fmt) assert [server, add] == args.command_names assert {} == args.arguments(False) assert {} == args.options(False) def test_parse_ignores_missing_command_names(parser): builder = ArgsFormatBuilder() server = CommandName("server", ["server-alias"]) add = CommandName("add", ["add-alias"]) builder.add_command_names(server, add) fmt = builder.format args = parser.parse(StringArgs(""), fmt) assert [server, add] == args.command_names assert {} == args.arguments(False) assert {} == args.options(False) def test_parse_arguments(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument1")) builder.add_argument(Argument("argument2")) fmt = builder.format args = parser.parse(StringArgs("server add foo bar"), fmt) assert {"argument1": "foo", "argument2": "bar"} == args.arguments(False) assert {} == args.options(False) def test_parse_arguments_ignores_missing_command_names(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument1")) builder.add_argument(Argument("argument2")) fmt = builder.format args = parser.parse(StringArgs("server foo bar"), fmt) assert {"argument1": "foo", "argument2": "bar"} == args.arguments(False) assert {} == args.options(False) def test_parse_arguments_ignores_missing_command_name_aliases(parser): builder = ArgsFormatBuilder() server = CommandName("server", ["server-alias"]) add = CommandName("add", ["add-alias"]) builder.add_command_names(server, add) builder.add_argument(Argument("argument1")) builder.add_argument(Argument("argument2")) fmt = builder.format args = parser.parse(StringArgs("server-alias foo bar"), fmt) assert {"argument1": "foo", "argument2": "bar"} == args.arguments(False) assert {} == args.options(False) def test_parse_arguments_ignores_missing_optional_arguments(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument1")) builder.add_argument(Argument("argument2")) fmt = builder.format args = parser.parse(StringArgs("server add foo"), fmt) assert {"argument1": "foo"} == args.arguments(False) assert {} == args.options(False) def test_parse_fails_if_missing_required_argument(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument1", Argument.REQUIRED)) builder.add_argument(Argument("argument2", Argument.REQUIRED)) fmt = builder.format with pytest.raises(CannotParseArgsException): parser.parse(StringArgs("server add foo"), fmt) def test_parse_does_not_fail_if_missing_required_argument_and_lenient(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument1", Argument.REQUIRED)) builder.add_argument(Argument("argument2", Argument.REQUIRED)) fmt = builder.format args = parser.parse(StringArgs("server add foo"), fmt, True) assert {"argument1": "foo"} == args.arguments(False) assert {} == args.options(False) def test_parse_fails_if_missing_required_argument_with_missing_command_name(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument1", Argument.REQUIRED)) builder.add_argument(Argument("argument2", Argument.REQUIRED)) fmt = builder.format with pytest.raises(CannotParseArgsException): parser.parse(StringArgs("server foo"), fmt) def test_parse_does_not_fail_if_missing_required_argument_with_missing_command_name_and_lenient( parser, ): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument1", Argument.REQUIRED)) builder.add_argument(Argument("argument2", Argument.REQUIRED)) fmt = builder.format args = parser.parse(StringArgs("server foo"), fmt, True) assert {"argument1": "foo"} == args.arguments(False) assert {} == args.options(False) def test_parse_fail_if_too_many_arguments(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument1")) fmt = builder.format with pytest.raises(CannotParseArgsException): parser.parse(StringArgs("server add foo bar"), fmt) def test_parse_does_not_fail_if_too_many_arguments_and_lenient(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument1")) fmt = builder.format args = parser.parse(StringArgs("server add foo bar"), fmt, True) assert {"argument1": "foo"} == args.arguments(False) assert {} == args.options(False) def test_parse_fail_if_too_many_arguments_with_missing_command_name(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument1")) fmt = builder.format with pytest.raises(CannotParseArgsException): parser.parse(StringArgs("server foo bar"), fmt) def test_parse_does_not_fail_if_too_many_arguments_with_missing_command_name_and_lenient( parser, ): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument1")) fmt = builder.format args = parser.parse(StringArgs("server foo bar"), fmt, True) assert {"argument1": "foo"} == args.arguments(False) assert {} == args.options(False) def test_parse_multi_valued_arguments(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("multi", Argument.MULTI_VALUED)) fmt = builder.format args = parser.parse(StringArgs("server add one two three"), fmt) assert {"multi": ["one", "two", "three"]} == args.arguments(False) assert {} == args.options(False) def test_parse_multi_valued_arguments_ignores_missing_command_names(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("multi", Argument.MULTI_VALUED)) fmt = builder.format args = parser.parse(StringArgs("server one two three"), fmt) assert {"multi": ["one", "two", "three"]} == args.arguments(False) assert {} == args.options(False) def test_parse_long_option_without_value(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_option(Option("option")) fmt = builder.format args = parser.parse(StringArgs("server add --option"), fmt) assert {} == args.arguments(False) assert {"option": True} == args.options(False) def test_parse_long_option_with_value(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_option(Option("option", flags=Option.OPTIONAL_VALUE)) fmt = builder.format args = parser.parse(StringArgs("server add --option foo"), fmt) assert {} == args.arguments(False) assert {"option": "foo"} == args.options(False) def test_parse_long_option_with_value2(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_option(Option("option", flags=Option.OPTIONAL_VALUE)) fmt = builder.format args = parser.parse(StringArgs("server add --option=foo"), fmt) assert {} == args.arguments(False) assert {"option": "foo"} == args.options(False) def test_parse_long_option_fails_if_missing_value(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_option(Option("option", flags=Option.REQUIRED_VALUE)) fmt = builder.format with pytest.raises(CannotParseArgsException) as e: parser.parse(StringArgs("server add --option"), fmt) assert 'The "--option" option requires a value.' == str(e.value) def test_parse_short_option_without_value(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_option(Option("option", "o")) fmt = builder.format args = parser.parse(StringArgs("server add -o"), fmt) assert {} == args.arguments(False) assert {"option": True} == args.options(False) def test_parse_short_option_with_value(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_option(Option("option", "o", flags=Option.OPTIONAL_VALUE)) fmt = builder.format args = parser.parse(StringArgs("server add -o foo"), fmt) assert {} == args.arguments(False) assert {"option": "foo"} == args.options(False) def test_parse_short_option_with_value2(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_option(Option("option", "o", flags=Option.OPTIONAL_VALUE)) fmt = builder.format args = parser.parse(StringArgs("server add -ofoo"), fmt) assert {} == args.arguments(False) assert {"option": "foo"} == args.options(False) def test_parse_short_option_fails_if_missing_value(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_option(Option("option", "o", flags=Option.REQUIRED_VALUE)) fmt = builder.format with pytest.raises(CannotParseArgsException) as e: parser.parse(StringArgs("server add -o"), fmt) assert 'The "--option" option requires a value.' == str(e.value) def test_option_fails_if_invalid_option(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) fmt = builder.format with pytest.raises(NoSuchOptionException) as e: parser.parse(StringArgs("server add --foo"), fmt) assert 'The "--foo" option does not exist.' == str(e.value) def test_parse_option_stops_parsing_if_invalid_option_and_lenient(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) fmt = builder.format args = parser.parse(StringArgs("server add --foo bar"), fmt, True) assert {} == args.arguments(False) assert {} == args.options(False) def test_parse_option_up_to_invalid_option_if_lenient(parser): builder = ArgsFormatBuilder() server = CommandName("server") add = CommandName("add") builder.add_command_names(server, add) builder.add_argument(Argument("argument")) fmt = builder.format args = parser.parse(StringArgs("server add bar --foo"), fmt, True) assert {"argument": "bar"} == args.arguments(False) assert {} == args.options(False) clikit-0.6.2/tests/args/test_string_args.py000066400000000000000000000032301366776574300210550ustar00rootroot00000000000000import pytest from clikit.args import StringArgs @pytest.mark.parametrize( "string, tokens", [ ("", []), ("foo", ["foo"]), (" foo bar ", ["foo", "bar"]), ('"quoted"', ["quoted"]), ("'quoted'", ["quoted"]), ("'a\rb\nc\td'", ["a\rb\nc\td"]), ("'a'\r'b'\n'c'\t'd'", ["a", "b", "c", "d"]), ("\"quoted 'twice'\"", ["quoted 'twice'"]), ("'quoted \"twice\"'", ['quoted "twice"']), ("\\'escaped\\'", ["'escaped'"]), ('\\"escaped\\"', ['"escaped"']), ("\\'escaped more\\'", ["'escaped", "more'"]), ('\\"escaped more\\"', ['"escaped', 'more"']), ("-a", ["-a"]), ("-azc", ["-azc"]), ("-awithavalue", ["-awithavalue"]), ('-a"foo bar"', ["-afoo bar"]), ('-a"foo bar""foo bar"', ["-afoo barfoo bar"]), ("-a'foo bar'", ["-afoo bar"]), ("-a'foo bar''foo bar'", ["-afoo barfoo bar"]), ("-a'foo bar'\"foo bar\"", ["-afoo barfoo bar"]), ("--long-option", ["--long-option"]), ("--long-option=foo", ["--long-option=foo"]), ('--long-option="foo bar"', ["--long-option=foo bar"]), ('--long-option="foo bar""another"', ["--long-option=foo baranother"]), ("--long-option='foo bar'", ["--long-option=foo bar"]), ("--long-option='foo bar''another'", ["--long-option=foo baranother"]), ("--long-option='foo bar'\"another\"", ["--long-option=foo baranother"]), ("foo -a -ffoo --long bar", ["foo", "-a", "-ffoo", "--long", "bar"]), ("\\' \\\"", ["'", '"']), ], ) def test_create(string, tokens): args = StringArgs(string) assert tokens == args.tokens clikit-0.6.2/tests/conftest.py000066400000000000000000000003601366776574300163660ustar00rootroot00000000000000import pytest from clikit.formatter import AnsiFormatter from clikit.io import BufferedIO @pytest.fixture() def io(): return BufferedIO() @pytest.fixture() def ansi_io(): return BufferedIO(formatter=AnsiFormatter(forced=True)) clikit-0.6.2/tests/handler/000077500000000000000000000000001366776574300156055ustar00rootroot00000000000000clikit-0.6.2/tests/handler/__init__.py000066400000000000000000000000001366776574300177040ustar00rootroot00000000000000clikit-0.6.2/tests/handler/help/000077500000000000000000000000001366776574300165355ustar00rootroot00000000000000clikit-0.6.2/tests/handler/help/__init__.py000066400000000000000000000000001366776574300206340ustar00rootroot00000000000000clikit-0.6.2/tests/handler/help/test_help_option.py000066400000000000000000000044121366776574300224670ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from contextlib import contextmanager import pytest from clikit.api.args import Args from clikit.api.args.format import Argument from clikit.args.string_args import StringArgs from clikit.config.default_application_config import DefaultApplicationConfig from clikit.console_application import ConsoleApplication from clikit.io.output_stream import BufferedOutputStream @pytest.fixture() def app(): config = DefaultApplicationConfig() config.set_catch_exceptions(True) config.set_terminate_after_run(False) config.set_display_name("The Application") config.set_version("1.2.3") with config.command("command") as c: with c.sub_command("run") as sc: sc.set_description('Description of "run"') sc.add_argument("args", Argument.MULTI_VALUED, 'Description of "argument"') application = ConsoleApplication(config) return application def func_spy(): def decorator(func): def wrapper(*args, **kwargs): decorator.called = True return func(*args, **kwargs) return wrapper decorator.called = False return decorator @contextmanager def help_spy(help_command): spy = func_spy() original = help_command.config.handler.handle try: help_command.config.handler.handle = spy(original) yield spy finally: help_command.config.handler.handle = original @pytest.mark.parametrize( "args", [ "command run --help", "command run --help --another", "command run --help -- whatever", "command run -h", "command run -h --another", "command run -h -- whatever", ], ) def test_help_option(app, args): help_command = app.get_command("help") with help_spy(help_command) as spy: app.run(StringArgs(args)) assert spy.called, "help command not called" @pytest.mark.parametrize( "args", [ "command run -- whatever --help", "command run -- whatever -h", "command run -- --help", ], ) def test_help_option_ignored(app, args): help_command = app.get_command("help") with help_spy(help_command) as spy: app.run(StringArgs(args)) assert not spy.called, "help command called" clikit-0.6.2/tests/handler/help/test_help_text_handler.py000066400000000000000000000200711366776574300236370ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import pytest from clikit.api.args import Args from clikit.args.string_args import StringArgs from clikit.config.default_application_config import DefaultApplicationConfig from clikit.console_application import ConsoleApplication @pytest.fixture() def app(): config = DefaultApplicationConfig() config.set_display_name("The Application") config.set_version("1.2.3") with config.command("command") as c: with c.sub_command("add") as sc1: sc1.set_description('Description of "add"') sc1.add_argument("argument", 0, 'Description of "argument"') with c.sub_command("delete") as sc2: sc2.set_description('Description of "delete"') sc2.add_option("opt", "o", description='Description of "opt"') sc2.add_argument("sub-arg", description='Description of "sub-arg"') application = ConsoleApplication(config) return application def test_render_for_application(app, io): help_command = app.get_command("help") handler = help_command.config.handler raw_args = StringArgs("") args = Args(help_command.args_format, raw_args) status = handler.handle(args, io, app.get_command("command")) expected = """\ The Application version 1.2.3 USAGE console [-h] [-q] [-v [<...>]] [-V] [--ansi] [--no-ansi] [-n] [] ... [] ARGUMENTS The command to execute The arguments of the command GLOBAL OPTIONS -h (--help) Display this help message -q (--quiet) Do not output any message -v (--verbose) Increase the verbosity of messages: "-v" for normal output, "-vv" for more verbose output and "-vvv" for debug -V (--version) Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n (--no-interaction) Do not ask any interactive question AVAILABLE COMMANDS command help Display the manual of a command """ assert 0 == status assert expected == io.fetch_output() def test_render_for_application_does_not_display_hidden_commands(app, io): app.get_command("command").config.hide() help_command = app.get_command("help") handler = help_command.config.handler raw_args = StringArgs("") args = Args(help_command.args_format, raw_args) status = handler.handle(args, io, app.get_command("command")) expected = """\ The Application version 1.2.3 USAGE console [-h] [-q] [-v [<...>]] [-V] [--ansi] [--no-ansi] [-n] [] ... [] ARGUMENTS The command to execute The arguments of the command GLOBAL OPTIONS -h (--help) Display this help message -q (--quiet) Do not output any message -v (--verbose) Increase the verbosity of messages: "-v" for normal output, "-vv" for more verbose output and "-vvv" for debug -V (--version) Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n (--no-interaction) Do not ask any interactive question AVAILABLE COMMANDS help Display the manual of a command """ assert 0 == status assert expected == io.fetch_output() def test_render_sub_command(app, io): help_command = app.get_command("help") handler = help_command.config.handler raw_args = StringArgs("command delete") args = Args(help_command.args_format, raw_args) args.set_argument("command", "command") status = handler.handle(args, io, app.get_command("command")) expected = """\ USAGE console command delete [-o] [] ARGUMENTS Description of "sub-arg" OPTIONS -o (--opt) Description of "opt" GLOBAL OPTIONS -h (--help) Display this help message -q (--quiet) Do not output any message -v (--verbose) Increase the verbosity of messages: "-v" for normal output, "-vv" for more verbose output and "-vvv" for debug -V (--version) Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n (--no-interaction) Do not ask any interactive question """ assert 0 == status assert expected == io.fetch_output() def test_render_parent_command_with_help_command_argument(app, io): help_command = app.get_command("help") handler = help_command.config.handler raw_args = StringArgs("help command") args = Args(help_command.args_format, raw_args) args.set_argument("command", "command") status = handler.handle(args, io, app.get_command("command")) expected = """\ USAGE console command or: console command add [] or: console command delete [-o] [] COMMANDS add Description of "add" Description of "argument" delete Description of "delete" Description of "sub-arg" -o (--opt) Description of "opt" GLOBAL OPTIONS -h (--help) Display this help message -q (--quiet) Do not output any message -v (--verbose) Increase the verbosity of messages: "-v" for normal output, "-vv" for more verbose output and "-vvv" for debug -V (--version) Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n (--no-interaction) Do not ask any interactive question """ assert 0 == status assert expected == io.fetch_output() def test_render_sub_command_with_help_command_argument(app, io): help_command = app.get_command("help") handler = help_command.config.handler raw_args = StringArgs("help command delete") args = Args(help_command.args_format, raw_args) args.set_argument("command", "command") status = handler.handle(args, io, app.get_command("command")) expected = """\ USAGE console command delete [-o] [] ARGUMENTS Description of "sub-arg" OPTIONS -o (--opt) Description of "opt" GLOBAL OPTIONS -h (--help) Display this help message -q (--quiet) Do not output any message -v (--verbose) Increase the verbosity of messages: "-v" for normal output, "-vv" for more verbose output and "-vvv" for debug -V (--version) Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n (--no-interaction) Do not ask any interactive question """ assert 0 == status assert expected == io.fetch_output() def test_render_parent_command_does_not_display_hidden_sub_commands(app, io): app.get_command("command").config.get_sub_command_config("delete").hide() help_command = app.get_command("help") handler = help_command.config.handler raw_args = StringArgs("help command") args = Args(help_command.args_format, raw_args) args.set_argument("command", "command") status = handler.handle(args, io, app.get_command("command")) expected = """\ USAGE console command or: console command add [] COMMANDS add Description of "add" Description of "argument" GLOBAL OPTIONS -h (--help) Display this help message -q (--quiet) Do not output any message -v (--verbose) Increase the verbosity of messages: "-v" for normal output, "-vv" for more verbose output and "-vvv" for debug -V (--version) Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n (--no-interaction) Do not ask any interactive question """ assert 0 == status assert expected == io.fetch_output() clikit-0.6.2/tests/test_console_aplication.py000066400000000000000000000163711366776574300214560ustar00rootroot00000000000000from typing import Generator import pytest from clikit import ConsoleApplication from clikit.api.command.exceptions import CannotAddCommandException from clikit.api.command.exceptions import NoSuchCommandException from clikit.api.config import ApplicationConfig as BaseApplicationConfig from clikit.api.config import CommandConfig from clikit.api.io import IO from clikit.api.io import Input from clikit.api.io import Output from clikit.args import StringArgs from clikit.handler.callback_handler import CallbackHandler from clikit.io.input_stream import StringInputStream from clikit.io.output_stream import BufferedOutputStream from clikit.resolver import DefaultResolver class ApplicationConfig(BaseApplicationConfig): @property def default_command_resolver(self): return DefaultResolver() @pytest.fixture() def config(): # type: () -> Generator[ApplicationConfig] config = ApplicationConfig() config.set_catch_exceptions(False) config.set_terminate_after_run(False) config.set_io_factory( lambda app, args, input_stream, output_stream, error_stream: IO( Input(input_stream), Output(output_stream), Output(error_stream) ) ) yield config def test_create(config): config.add_argument("argument") config.add_option("option", "o") app = ConsoleApplication(config) assert config == app.config fmt = app.global_args_format assert len(fmt.get_arguments()) == 1 arg = fmt.get_argument("argument") assert arg.name == "argument" opt = fmt.get_option("option") assert opt.long_name == "option" assert opt.short_name == "o" def test_get_commands(config): config1 = CommandConfig("command1") config2 = CommandConfig("command2") config.add_command_config(config1) config.add_command_config(config2) app = ConsoleApplication(config) assert len(app.commands) == 2 assert app.commands.get("command1").name == config1.name assert app.commands.get("command2").name == config2.name def test_get_commands_excludes_disabled_commands(config): enabled = CommandConfig("command1").enable() disabled = CommandConfig("command2").disable() config.add_command_config(enabled) config.add_command_config(disabled) app = ConsoleApplication(config) assert len(app.commands) == 1 assert app.commands.get("command1").name == enabled.name def test_get_command(config): config1 = CommandConfig("command1") config.add_command_config(config1) app = ConsoleApplication(config) command = app.get_command("command1") assert command.name == config1.name def test_get_command_fails_if_command_not_found(config): app = ConsoleApplication(config) with pytest.raises(NoSuchCommandException): app.get_command("foobar") def test_has_command(config): config1 = CommandConfig("command1") config.add_command_config(config1) app = ConsoleApplication(config) assert app.has_command(config1.name) def test_has_commands(config): config1 = CommandConfig("command1") config.add_command_config(config1) app = ConsoleApplication(config) assert app.has_commands() def test_has_no_commands(config): app = ConsoleApplication(config) assert not app.has_commands() def test_get_named_commands(config): config1 = CommandConfig("command1") config2 = CommandConfig("command2") config3 = CommandConfig("command3") config2.anonymous() config3.default() config.add_command_config(config1) config.add_command_config(config2) config.add_command_config(config3) app = ConsoleApplication(config) assert len(app.named_commands) == 2 def test_has_named_commands(config): config1 = CommandConfig("command1") config.add_command_config(config1) app = ConsoleApplication(config) assert app.has_named_commands() def test_has_no_named_commands(config): config1 = CommandConfig("command1") config1.anonymous() config.add_command_config(config1) app = ConsoleApplication(config) assert not app.has_named_commands() def test_get_default_commands(config): config1 = CommandConfig("command1") config2 = CommandConfig("command2") config3 = CommandConfig("command3") config2.default() config3.default() config.add_command_config(config1) config.add_command_config(config2) config.add_command_config(config3) app = ConsoleApplication(config) assert len(app.default_commands) == 2 def test_has_no_default_commands(config): config1 = CommandConfig("command1") config.add_command_config(config1) app = ConsoleApplication(config) assert not app.has_default_commands() def test_has_default_commands(config): config1 = CommandConfig("command1") config1.default() config.add_command_config(config1) app = ConsoleApplication(config) assert app.has_default_commands() def test_fails_if_no_command_name(config): config.add_command_config(CommandConfig()) with pytest.raises(CannotAddCommandException): ConsoleApplication(config) def test_fails_if_duplicate_command_name(config): config.add_command_config(CommandConfig("command")) config.add_command_config(CommandConfig("command")) with pytest.raises(CannotAddCommandException): ConsoleApplication(config) @pytest.mark.parametrize( "arg_string, config_callback", [ # Simple command ( "list", lambda config, callback: config.create_command("list").set_handler( CallbackHandler(callback) ), ), # Default command ( "", lambda config, callback: config.create_command("list") .default() .set_handler(CallbackHandler(callback)), ), # Sub command ( "server add", lambda config, callback: config.create_command("server") .create_sub_command("add") .set_handler(CallbackHandler(callback)), ), # Default sub command ( "server", lambda config, callback: config.create_command("server") .create_sub_command("add") .default() .set_handler(CallbackHandler(callback)), ), ], ) def test_run_command(config, arg_string, config_callback): def callback(_, io): io.write(io.read_line()) io.error(io.read_line()) return 123 config_callback(config, callback) args = StringArgs(arg_string) input = StringInputStream("line1\nline2") output = BufferedOutputStream() error_output = BufferedOutputStream() app = ConsoleApplication(config) assert 123 == app.run(args, input, output, error_output) assert "line1\n" == output.fetch() assert "line2" == error_output.fetch() def test_run_with_keyboard_interrupt(config): # type: (ApplicationConfig) -> None def callback(_, io): raise KeyboardInterrupt() config.create_command("interrupted").set_handler(CallbackHandler(callback)) app = ConsoleApplication(config) output = BufferedOutputStream() error_output = BufferedOutputStream() assert 1 == app.run( StringArgs("interrupted"), StringInputStream(""), output, error_output ) assert "" == output.fetch() assert "" == error_output.fetch() clikit-0.6.2/tests/ui/000077500000000000000000000000001366776574300146055ustar00rootroot00000000000000clikit-0.6.2/tests/ui/__init__.py000066400000000000000000000000001366776574300167040ustar00rootroot00000000000000clikit-0.6.2/tests/ui/components/000077500000000000000000000000001366776574300167725ustar00rootroot00000000000000clikit-0.6.2/tests/ui/components/__init__.py000066400000000000000000000000001366776574300210710ustar00rootroot00000000000000clikit-0.6.2/tests/ui/components/helpers.py000066400000000000000000000001121366776574300210000ustar00rootroot00000000000000def outer(): def inner(): raise Exception("Foo") inner() clikit-0.6.2/tests/ui/components/test_choice_question.py000066400000000000000000000046141366776574300235710ustar00rootroot00000000000000import pytest from clikit.ui.components import ChoiceQuestion def test_ask_choice(io): io.set_input( "\n" "1\n" " 1 \n" "John\n" "1\n" "John\n" "1\n" "0,2\n" " 0 , 2 \n" "\n" "\n" "4\n" "0\n" "-2\n" ) heroes = ["Superman", "Batman", "Spiderman"] question = ChoiceQuestion("What is your favorite superhero?", heroes, "2") question.set_max_attempts(1) # First answer is an empty answer, we're supposed to receive the default value assert "Spiderman" == question.ask(io) question = ChoiceQuestion("What is your favorite superhero?", heroes) question.set_max_attempts(1) assert "Batman" == question.ask(io) assert "Batman" == question.ask(io) question = ChoiceQuestion("What is your favorite superhero?", heroes) question.set_error_message('Input "{}" is not a superhero!') question.set_max_attempts(2) io.clear_error() assert "Batman" == question.ask(io) assert 'Input "John" is not a superhero!' in io.fetch_error() question = ChoiceQuestion("What is your favorite superhero?", heroes, "1") question.set_max_attempts(1) with pytest.raises(Exception) as e: question.ask(io) assert 'Value "John" is invalid' == str(e.value) question = ChoiceQuestion("What is your favorite superhero?", heroes) question.set_max_attempts(1) question.set_multi_select(True) assert ["Batman"] == question.ask(io) assert ["Superman", "Spiderman"] == question.ask(io) assert ["Superman", "Spiderman"] == question.ask(io) question = ChoiceQuestion("What is your favorite superhero?", heroes, "0,1") question.set_max_attempts(1) question.set_multi_select(True) assert ["Superman", "Batman"] == question.ask(io) question = ChoiceQuestion("What is your favorite superhero?", heroes, " 0 , 1 ") question.set_max_attempts(1) question.set_multi_select(True) assert ["Superman", "Batman"] == question.ask(io) question = ChoiceQuestion("What is your favourite superhero?", heroes) question.set_max_attempts(1) with pytest.raises(ValueError) as e: question.ask(io) assert 'Value "4" is invalid' == str(e.value) assert "Superman" == question.ask(io) with pytest.raises(ValueError) as e: question.ask(io) assert 'Value "-2" is invalid' == str(e.value) clikit-0.6.2/tests/ui/components/test_confirmation_question.py000066400000000000000000000013511366776574300250220ustar00rootroot00000000000000from clikit.ui.components import ConfirmationQuestion def test_ask(io): data = [ ("", True), ("", False, False), ("y", True), ("yes", True), ("n", False), ("no", False), ] for d in data: io.set_input(d[0] + "\n") default = d[2] if len(d) > 2 else True question = ConfirmationQuestion("Do you like French fries?", default) assert d[1] == question.ask(io) def test_ask_with_custom_answer(io): io.set_input("j\ny\n") question = ConfirmationQuestion("Do you like French fries?", False, "(?i)^(j|y)") assert question.ask(io) question = ConfirmationQuestion("Do you like French fries?", False, "(?i)^(j|y)") assert question.ask(io) clikit-0.6.2/tests/ui/components/test_exception_trace.py000066400000000000000000000253201366776574300235610ustar00rootroot00000000000000# -*- coding: utf-8 -*- import re import pytest from clikit.api.io.flags import DEBUG from clikit.api.io.flags import VERBOSE from clikit.io.buffered_io import BufferedIO from clikit.ui.components.exception_trace import ExceptionTrace from clikit.utils._compat import PY36 from clikit.utils._compat import PY38 def fail(): raise Exception("Failed") @pytest.mark.skipif(PY36, reason="Legacy error messages are Python <3.6 only") def test_render_legacy_error_message(): io = BufferedIO() try: raise Exception("Failed") except Exception as e: trace = ExceptionTrace(e) trace.render(io) expected = """\ Exception Failed """ assert expected == io.fetch_output() @pytest.mark.skipif( not PY36, reason="Better error messages are only available for Python ^3.6" ) def test_render_better_error_message(): io = BufferedIO() try: raise Exception("Failed") except Exception as e: trace = ExceptionTrace(e) trace.render(io) expected = """\ Exception Failed at {}:44 in test_render_better_error_message 40│ def test_render_better_error_message(): 41│ io = BufferedIO() 42│ 43│ try: → 44│ raise Exception("Failed") 45│ except Exception as e: 46│ trace = ExceptionTrace(e) 47│ 48│ trace.render(io) """.format( trace._get_relative_file_path(__file__) ) assert expected == io.fetch_output() @pytest.mark.skipif(PY36, reason="Legacy error messages are Python <3.6 only") def test_render_verbose_legacy(): io = BufferedIO() io.set_verbosity(VERBOSE) try: raise Exception("Failed") except Exception as e: trace = ExceptionTrace(e) trace.render(io) msg = "'Failed'" if PY38: msg = '"Failed"' expected = """\ Exception Failed Traceback (most recent call last): File "{}", line 78, in test_render_verbose_legacy raise Exception({}) """.format( __file__, msg ) assert expected == io.fetch_output() @pytest.mark.skipif( not PY36, reason="Better error messages are only available for Python ^3.6" ) def test_render_debug_better_error_message(): io = BufferedIO() io.set_verbosity(DEBUG) try: fail() except Exception as e: # Exception trace = ExceptionTrace(e) trace.render(io) expected = r"""^ Stack trace: 1 {}:112 in test_render_debug_better_error_message 110\│ 111\│ try: → 112\│ fail\(\) 113\│ except Exception as e: # Exception 114\│ trace = ExceptionTrace\(e\) Exception Failed at {}:14 in fail 10\│ from clikit.utils._compat import PY38 11\│ 12\│ 13\│ def fail\(\): → 14\│ raise Exception\("Failed"\) 15\│ 16\│ 17\│ @pytest.mark.skipif\(PY36, reason="Legacy error messages are Python <3.6 only"\) 18\│ def test_render_legacy_error_message\(\): """.format( re.escape(trace._get_relative_file_path(__file__)), re.escape(trace._get_relative_file_path(__file__)), ) assert re.match(expected, io.fetch_output()) is not None def recursion_error(): recursion_error() @pytest.mark.skipif( not PY36, reason="Better error messages are only available for Python ^3.6" ) def test_render_debug_better_error_message_recursion_error(): io = BufferedIO() io.set_verbosity(DEBUG) try: recursion_error() except RecursionError as e: trace = ExceptionTrace(e) trace.render(io) expected = r"""^ Stack trace: \d+ {}:162 in test_render_debug_better_error_message_recursion_error 160\│ 161\│ try: → 162\│ recursion_error\(\) 163\│ except RecursionError as e: 164\│ trace = ExceptionTrace\(e\) ... Previous frame repeated \d+ times \s*\d+ {}:151 in recursion_error 149\│ 150\│ def recursion_error\(\): → 151\│ recursion_error\(\) 152\│ 153\│ RecursionError maximum recursion depth exceeded at {}:151 in recursion_error 147\│ assert re.match\(expected, io.fetch_output\(\)\) is not None 148\│ 149\│ 150\│ def recursion_error\(\): → 151\│ recursion_error\(\) 152\│ 153\│ 154\│ @pytest.mark.skipif\( 155\│ not PY36, reason="Better error messages are only available for Python \^3\.6" """.format( re.escape(trace._get_relative_file_path(__file__)), re.escape(trace._get_relative_file_path(__file__)), re.escape(trace._get_relative_file_path(__file__)), ) assert re.match(expected, io.fetch_output()) is not None @pytest.mark.skipif( not PY36, reason="Better error messages are only available for Python ^3.6" ) def test_render_verbose_better_error_message(): io = BufferedIO() io.set_verbosity(VERBOSE) try: fail() except Exception as e: # Exception trace = ExceptionTrace(e) trace.render(io) expected = r"""^ Stack trace: 1 {}:218 in test_render_verbose_better_error_message fail\(\) Exception Failed at {}:14 in fail 10\│ from clikit.utils._compat import PY38 11\│ 12\│ 13\│ def fail\(\): → 14\│ raise Exception\("Failed"\) 15\│ 16\│ 17\│ @pytest.mark.skipif\(PY36, reason="Legacy error messages are Python <3.6 only"\) 18\│ def test_render_legacy_error_message\(\): """.format( re.escape(trace._get_relative_file_path(__file__)), re.escape(trace._get_relative_file_path(__file__)), ) assert re.match(expected, io.fetch_output()) is not None def first(): def second(): first() second() @pytest.mark.skipif( not PY36, reason="Better error messages are only available for Python ^3.6" ) def test_render_debug_better_error_message_recursion_error_with_multiple_duplicated_frames(): io = BufferedIO() io.set_verbosity(VERBOSE) with pytest.raises(RecursionError) as e: first() trace = ExceptionTrace(e.value) trace.render(io) expected = r"... Previous 2 frames repeated \d+ times".format( filename=re.escape(trace._get_relative_file_path(__file__)), ) assert re.search(expected, io.fetch_output()) is not None @pytest.mark.skipif( not PY36, reason="Better error messages are only available for Python ^3.6" ) def test_render_can_ignore_given_files(): import os from .helpers import outer io = BufferedIO() io.set_verbosity(VERBOSE) def call(): def run(): outer() run() with pytest.raises(Exception) as e: call() trace = ExceptionTrace(e.value) helpers_file = os.path.join(os.path.dirname(__file__), "helpers.py") trace.ignore_files_in("^{}$".format(re.escape(helpers_file))) trace.render(io) expected = """ Stack trace: 2 {}:297 in test_render_can_ignore_given_files call() 1 {}:294 in call run() Exception Foo at {}:3 in inner 1│ def outer(): 2│ def inner(): → 3│ raise Exception("Foo") 4│ 5│ inner() 6│ """.format( trace._get_relative_file_path(__file__), trace._get_relative_file_path(__file__), trace._get_relative_file_path(helpers_file), ) assert expected == io.fetch_output() @pytest.mark.skipif( not PY36, reason="Better error messages are only available for Python ^3.6" ) def test_render_shows_ignored_files_if_in_debug_mode(): import os from .helpers import outer io = BufferedIO() io.set_verbosity(DEBUG) def call(): def run(): outer() run() with pytest.raises(Exception) as e: call() trace = ExceptionTrace(e.value) helpers_file = os.path.join(os.path.dirname(__file__), "helpers.py") trace.ignore_files_in("^{}$".format(re.escape(helpers_file))) trace.render(io) expected = """ Stack trace: 4 {}:351 in test_render_shows_ignored_files_if_in_debug_mode 349│ 350│ with pytest.raises(Exception) as e: → 351│ call() 352│ 353│ trace = ExceptionTrace(e.value) 3 {}:348 in call 346│ outer() 347│ → 348│ run() 349│ 350│ with pytest.raises(Exception) as e: 2 {}:346 in run 344│ def call(): 345│ def run(): → 346│ outer() 347│ 348│ run() 1 {}:5 in outer 3│ raise Exception("Foo") 4│ → 5│ inner() 6│ Exception Foo at {}:3 in inner 1│ def outer(): 2│ def inner(): → 3│ raise Exception("Foo") 4│ 5│ inner() 6│ """.format( trace._get_relative_file_path(__file__), trace._get_relative_file_path(__file__), trace._get_relative_file_path(__file__), trace._get_relative_file_path(helpers_file), trace._get_relative_file_path(helpers_file), ) assert expected == io.fetch_output() @pytest.mark.skipif( not PY36, reason="Better error messages are only available for Python ^3.6" ) def test_render_supports_solutions(): from crashtest.contracts.provides_solution import ProvidesSolution from crashtest.contracts.base_solution import BaseSolution from crashtest.solution_providers.solution_provider_repository import ( SolutionProviderRepository, ) class CustomError(ProvidesSolution, Exception): @property def solution(self): solution = BaseSolution("Solution Title.", "Solution Description") solution.documentation_links.append("https://example.com") solution.documentation_links.append("https://example2.com") return solution io = BufferedIO() def call(): raise CustomError("Error with solution") with pytest.raises(CustomError) as e: call() trace = ExceptionTrace( e.value, solution_provider_repository=SolutionProviderRepository() ) trace.render(io) expected = """ CustomError Error with solution at {}:433 in call 429│ 430│ io = BufferedIO() 431│ 432│ def call(): → 433│ raise CustomError("Error with solution") 434│ 435│ with pytest.raises(CustomError) as e: 436│ call() 437│ • Solution Title: Solution Description https://example.com, https://example2.com """.format( trace._get_relative_file_path(__file__), ) assert expected == io.fetch_output() clikit-0.6.2/tests/ui/components/test_progress_bar.py000066400000000000000000000277331366776574300231070ustar00rootroot00000000000000# -*- coding: utf-8 -*- import time import pytest from clikit.ui.components import ProgressBar from clikit.utils._compat import decode @pytest.fixture() def bar(io): return ProgressBar(io, min_seconds_between_redraws=0) @pytest.fixture() def ansi_bar(ansi_io): return ProgressBar(ansi_io, min_seconds_between_redraws=0) def test_multiple_start(ansi_bar, ansi_io): ansi_bar.start() ansi_bar.advance() ansi_bar.start() output = [ " 0 [>---------------------------]", " 1 [->--------------------------]", " 0 [>---------------------------]", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_advance(ansi_bar, ansi_io): ansi_bar.start() ansi_bar.advance() output = [ " 0 [>---------------------------]", " 1 [->--------------------------]", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_advance_with_step(ansi_bar, ansi_io): ansi_bar.start() ansi_bar.advance(5) output = [ " 0 [>---------------------------]", " 5 [----->----------------------]", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_advance_multiple_times(ansi_bar, ansi_io): ansi_bar.start() ansi_bar.advance(3) ansi_bar.advance(2) output = [ " 0 [>---------------------------]", " 3 [--->------------------------]", " 5 [----->----------------------]", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_advance_over_max(ansi_io): bar = ProgressBar(ansi_io, 10) bar.set_progress(9) bar.advance() bar.advance() output = [ " 9/10 [=========================>--] 90%", " 10/10 [============================] 100%", " 11/11 [============================] 100%", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_format(ansi_io): output = [ " 0/10 [>---------------------------] 0%", " 10/10 [============================] 100%", " 10/10 [============================] 100%", ] expected = "\x0D" + "\x0D".join(output) # max in construct, no format ansi_io.clear_error() bar = ProgressBar(ansi_io, 10) bar.start() bar.advance(10) bar.finish() assert expected == ansi_io.fetch_error() # max in start, no format ansi_io.clear_error() bar = ProgressBar(ansi_io) bar.start(10) bar.advance(10) bar.finish() assert expected == ansi_io.fetch_error() # max in construct, explicit format before ansi_io.clear_error() bar = ProgressBar(ansi_io, 10) bar.set_format("normal") bar.start() bar.advance(10) bar.finish() assert expected == ansi_io.fetch_error() # max in start, explicit format before ansi_io.clear_error() bar = ProgressBar(ansi_io) bar.set_format("normal") bar.start(10) bar.advance(10) bar.finish() assert expected == ansi_io.fetch_error() def test_customizations(ansi_io): bar = ProgressBar(ansi_io, 10, 0) bar.set_bar_width(10) bar.set_bar_character("_") bar.set_empty_bar_character(" ") bar.set_progress_character("/") bar.set_format(" %current%/%max% [%bar%] %percent:3s%%") bar.start() bar.advance() output = [" 0/10 [/ ] 0%", " 1/10 [_/ ] 10%"] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_display_without_start(ansi_io): bar = ProgressBar(ansi_io, 50, 0) bar.display() expected = "\x0D 0/50 [>---------------------------] 0%" assert expected == ansi_io.fetch_error() def test_display_with_quiet_verbosity(ansi_io): ansi_io.set_quiet(True) bar = ProgressBar(ansi_io, 50, 0) bar.display() assert "" == ansi_io.fetch_error() def test_finish_without_start(ansi_io): bar = ProgressBar(ansi_io, 50, 0) bar.finish() expected = "\x0D 50/50 [============================] 100%" assert expected == ansi_io.fetch_error() def test_percent(ansi_io): bar = ProgressBar(ansi_io, 50, 0) bar.start() bar.display() bar.advance() bar.advance() output = [ " 0/50 [>---------------------------] 0%", " 0/50 [>---------------------------] 0%", " 1/50 [>---------------------------] 2%", " 2/50 [=>--------------------------] 4%", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_overwrite_with_shorter_line(ansi_io): bar = ProgressBar(ansi_io, 50, 0) bar.set_format(" %current%/%max% [%bar%] %percent:3s%%") bar.start() bar.display() bar.advance() # Set shorter format bar.set_format(" %current%/%max% [%bar%]") bar.advance() output = [ " 0/50 [>---------------------------] 0%", " 0/50 [>---------------------------] 0%", " 1/50 [>---------------------------] 2%", " 2/50 [=>--------------------------] ", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_set_current_progress(ansi_io): bar = ProgressBar(ansi_io, 50, 0) bar.start() bar.display() bar.advance() bar.set_progress(15) bar.set_progress(25) output = [ " 0/50 [>---------------------------] 0%", " 0/50 [>---------------------------] 0%", " 1/50 [>---------------------------] 2%", " 15/50 [========>-------------------] 30%", " 25/50 [==============>-------------] 50%", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_multibyte_support(ansi_bar, ansi_io): ansi_bar.start() ansi_bar.set_bar_character("■") ansi_bar.advance(3) output = [ " 0 [>---------------------------]", " 3 [■■■>------------------------]", ] expected = "\x0D" + "\x0D".join(output) assert decode(expected) == ansi_io.fetch_error() def test_clear(ansi_io): bar = ProgressBar(ansi_io, 50, 0) bar.start() bar.set_progress(25) bar.clear() output = [ " 0/50 [>---------------------------] 0%", " 25/50 [==============>-------------] 50%", " ", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_percent_not_hundred_before_complete(ansi_io): bar = ProgressBar(ansi_io, 200, 0) bar.start() bar.display() bar.advance(199) bar.advance() output = [ " 0/200 [>---------------------------] 0%", " 0/200 [>---------------------------] 0%", " 199/200 [===========================>] 99%", " 200/200 [============================] 100%", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_non_decorated_output(io): bar = ProgressBar(io, 200, 0) bar.start() for i in range(200): bar.advance() bar.finish() expected = "\n".join( [ " 0/200 [>---------------------------] 0%", " 20/200 [==>-------------------------] 10%", " 40/200 [=====>----------------------] 20%", " 60/200 [========>-------------------] 30%", " 80/200 [===========>----------------] 40%", " 100/200 [==============>-------------] 50%", " 120/200 [================>-----------] 60%", " 140/200 [===================>--------] 70%", " 160/200 [======================>-----] 80%", " 180/200 [=========================>--] 90%", " 200/200 [============================] 100%", ] ) assert expected == io.fetch_error() def test_multiline_format(ansi_io): bar = ProgressBar(ansi_io, 3, 0) bar.set_format("%bar%\nfoobar") bar.start() bar.advance() bar.clear() bar.finish() output = [ "\033[1A>---------------------------\nfoobar", "\033[1A=========>------------------\nfoobar ", "\033[1A \n ", "\033[1A============================\nfoobar ", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_regress(ansi_bar, ansi_io): ansi_bar.start() ansi_bar.advance() ansi_bar.advance() ansi_bar.advance(-1) output = [ " 0 [>---------------------------]", " 1 [->--------------------------]", " 2 [-->-------------------------]", " 1 [->--------------------------]", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_regress_with_steps(ansi_bar, ansi_io): ansi_bar.start() ansi_bar.advance(4) ansi_bar.advance(4) ansi_bar.advance(-2) output = [ " 0 [>---------------------------]", " 4 [---->-----------------------]", " 8 [-------->-------------------]", " 6 [------>---------------------]", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_regress_multiple_times(ansi_bar, ansi_io): ansi_bar.start() ansi_bar.advance(3) ansi_bar.advance(3) ansi_bar.advance(-1) ansi_bar.advance(-2) output = [ " 0 [>---------------------------]", " 3 [--->------------------------]", " 6 [------>---------------------]", " 5 [----->----------------------]", " 3 [--->------------------------]", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_regress_below_min(ansi_io): bar = ProgressBar(ansi_io, 10, 0) bar.set_progress(1) bar.advance(-1) bar.advance(-1) output = [ " 1/10 [==>-------------------------] 10%", " 0/10 [>---------------------------] 0%", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() def test_overwrite_with_section_output(ansi_io): bar = ProgressBar(ansi_io.section(), 50, 0) bar.start() bar.display() bar.advance() bar.advance() output = [ " 0/50 [>---------------------------] 0%", " 0/50 [>---------------------------] 0%", " 1/50 [>---------------------------] 2%", " 2/50 [=>--------------------------] 4%", ] expected = "\n\x1b[1A\x1b[0J".join(output) + "\n" assert expected == ansi_io.fetch_error() def test_overwrite_multiple_progress_bars_with_section_outputs(ansi_io): output1 = ansi_io.section() output2 = ansi_io.section() bar1 = ProgressBar(output1, 50, 0) bar2 = ProgressBar(output2, 50, 0) bar1.start() bar2.start() bar2.advance() bar1.advance() output = [ " 0/50 [>---------------------------] 0%", " 0/50 [>---------------------------] 0%", "\x1b[1A\x1b[0J 1/50 [>---------------------------] 2%", "\x1b[2A\x1b[0J 1/50 [>---------------------------] 2%", "\x1b[1A\x1b[0J 1/50 [>---------------------------] 2%", " 1/50 [>---------------------------] 2%", ] expected = "\n".join(output) + "\n" assert expected == ansi_io.fetch_error() def test_min_and_max_seconds_between_redraws(ansi_bar, ansi_io): ansi_bar.min_seconds_between_redraws(0.5) ansi_bar.max_seconds_between_redraws(2 - 1) ansi_bar.start() ansi_bar.set_progress(1) time.sleep(1) ansi_bar.set_progress(2) time.sleep(2) ansi_bar.set_progress(3) output = [ " 0 [>---------------------------]", " 2 [->--------------------------]", " 3 [--->------------------------]", ] expected = "\x0D" + "\x0D".join(output) assert expected == ansi_io.fetch_error() clikit-0.6.2/tests/ui/components/test_progress_indicator.py000066400000000000000000000055051366776574300243100ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import time from clikit.formatter import AnsiFormatter from clikit.ui.components import ProgressIndicator def test_default_indicator(ansi_io): bar = ProgressIndicator(ansi_io) bar.start("Starting...") time.sleep(0.101) bar.advance() time.sleep(0.101) bar.advance() time.sleep(0.101) bar.advance() time.sleep(0.101) bar.advance() time.sleep(0.101) bar.advance() time.sleep(0.101) bar.set_message("Advancing...") bar.advance() bar.finish("Done...") bar.start("Starting Again...") time.sleep(0.101) bar.advance() bar.finish("Done Again...") bar.start("Starting Again...") time.sleep(0.101) bar.advance() bar.finish("Done Again...", reset_indicator=True) output = [ " - Starting...", " \\ Starting...", " | Starting...", " / Starting...", " - Starting...", " \\ Starting...", " \\ Advancing...", " | Advancing...", " | Done...", ] expected = "\x0D\x1B[2K" + "\x0D\x1B[2K".join(output) expected += "\n" output = [" - Starting Again...", " \\ Starting Again...", " \\ Done Again..."] expected += "\x0D\x1B[2K" + "\x0D\x1B[2K".join(output) expected += "\n" output = [" - Starting Again...", " \\ Starting Again...", " - Done Again..."] expected += "\x0D\x1B[2K" + "\x0D\x1B[2K".join(output) expected += "\n" assert expected == ansi_io.fetch_error() def test_explicit_format(ansi_io): bar = ProgressIndicator(ansi_io, ProgressIndicator.NORMAL) bar.start("Starting...") time.sleep(0.101) bar.advance() time.sleep(0.101) bar.advance() time.sleep(0.101) bar.advance() time.sleep(0.101) bar.advance() time.sleep(0.101) bar.advance() time.sleep(0.101) bar.set_message("Advancing...") bar.advance() bar.finish("Done...") bar.start("Starting Again...") time.sleep(0.101) bar.advance() bar.finish("Done Again...") bar.start("Starting Again...") time.sleep(0.101) bar.advance() bar.finish("Done Again...", reset_indicator=True) output = [ " - Starting...", " \\ Starting...", " | Starting...", " / Starting...", " - Starting...", " \\ Starting...", " \\ Advancing...", " | Advancing...", " | Done...", ] expected = "\x0D\x1B[2K" + "\x0D\x1B[2K".join(output) expected += "\n" output = [" - Starting Again...", " \\ Starting Again...", " \\ Done Again..."] expected += "\x0D\x1B[2K" + "\x0D\x1B[2K".join(output) expected += "\n" output = [" - Starting Again...", " \\ Starting Again...", " - Done Again..."] expected += "\x0D\x1B[2K" + "\x0D\x1B[2K".join(output) expected += "\n" assert expected == ansi_io.fetch_error() clikit-0.6.2/tests/ui/components/test_question.py000066400000000000000000000036321366776574300222560ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import os import subprocess import pytest from clikit.ui.components import Question def has_tty_available(): devnull = open(os.devnull, "w") exit_code = subprocess.call(["stty", "2"], stdout=devnull, stderr=devnull) return exit_code == 0 TTY_AVAILABLE = has_tty_available() def test_ask(io): question = Question("What time is it?", "2PM") io.set_input("\n8AM\n") assert "2PM" == question.ask(io) io.clear_error() assert "8AM" == question.ask(io) assert "What time is it? " == io.fetch_error() @pytest.mark.skipif( not TTY_AVAILABLE, reason="`stty` is required to test hidden response functionality" ) def test_ask_hidden_response(io): question = Question("What time is it?", "2PM") question.hide() io.set_input("8AM\n") assert "8AM" == question.ask(io) assert "What time is it? " == io.fetch_error() def test_ask_and_validate(io): error = "This is not a color!" def validator(color): if color not in ["white", "black"]: raise Exception(error) return color question = Question("What color was the white horse of Henry IV?", "white") question.set_validator(validator) question.set_max_attempts(2) io.set_input("\nblack\n") assert "white" == question.ask(io) assert "black" == question.ask(io) io.set_input("green\nyellow\norange\n") with pytest.raises(Exception) as e: question.ask(io) assert error == str(e.value) def test_no_interaction(io): io.set_interactive(False) question = Question("Do you have a job?", "not yet") assert "not yet" == question.ask(io) def test_ask_question_with_special_characters(io): question = Question("What time is it, Sébastien?", "2PMë") io.set_input("\n") assert "2PMë" == question.ask(io) assert "What time is it, Sébastien? " == io.fetch_error() clikit-0.6.2/tests/ui/components/test_table.py000066400000000000000000000371151366776574300215010ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import pytest from clikit.ui.components import Table from clikit.ui.style import TableStyle from clikit.ui.style.alignment import Alignment def test_render_ascii_border(io): table = Table(TableStyle.ascii()) table.set_header_row(["ISBN", "Title", "Author"]) table.add_rows( [ ["99921-58-10-7", "Divine Comedy", "Dante Alighieri"], ["9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens"], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ] ) table.render(io) expected = """\ +---------------+--------------------------+------------------+ | ISBN | Title | Author | +---------------+--------------------------+------------------+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------------------------+------------------+ """ assert expected == io.fetch_output() def test_render_solid_border(io): table = Table(TableStyle.solid()) table.set_header_row(["ISBN", "Title", "Author"]) table.add_rows( [ ["99921-58-10-7", "Divine Comedy", "Dante Alighieri"], ["9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens"], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ] ) table.render(io) expected = """\ ┌───────────────┬──────────────────────────┬──────────────────┐ │ ISBN │ Title │ Author │ ├───────────────┼──────────────────────────┼──────────────────┤ │ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri │ │ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens │ │ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien │ │ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │ └───────────────┴──────────────────────────┴──────────────────┘ """ assert expected == io.fetch_output() def test_render_no_border(io): table = Table(TableStyle.borderless()) table.set_header_row(["ISBN", "Title", "Author"]) table.add_rows( [ ["99921-58-10-7", "Divine Comedy", "Dante Alighieri"], ["9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens"], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ] ) table.render(io) expected = """\ ISBN Title Author ============= ======================== ================ 99921-58-10-7 Divine Comedy Dante Alighieri 9971-5-0210-0 A Tale of Two Cities Charles Dickens 960-425-059-0 The Lord of the Rings J. R. R. Tolkien 80-902734-1-6 And Then There Were None Agatha Christie """ assert expected == io.fetch_output() def test_render_empty(io): table = Table(TableStyle.ascii()) table.render(io) assert "" == io.fetch_output() def test_render_alignment(io): style = TableStyle.ascii() style.set_column_alignment(1, Alignment.CENTER) style.set_column_alignment(2, Alignment.RIGHT) table = Table(style) table.set_header_row(["ISBN", "Title", "Author"]) table.add_rows( [ ["99921-58-10-7", "Divine Comedy", "Dante Alighieri"], ["9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens"], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ] ) table.render(io) expected = """\ +---------------+--------------------------+------------------+ | ISBN | Title | Author | +---------------+--------------------------+------------------+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------------------------+------------------+ """ assert expected == io.fetch_output() def test_render_with_word_wrapping(io): style = TableStyle.ascii() table = Table(style) table.set_header_row(["ISBN", "Title", "Author"]) table.add_rows( [ [ "99921-58-10-7", "Divine Comedy Divine Comedy Divine Comedy Divine Comedy Divine Comedy ", "Dante Alighieri", ], [ "9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens Charles Dickens Charles Dickens", ], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ] ) table.render(io) expected = """\ +---------------+------------------------------------+-------------------------+ | ISBN | Title | Author | +---------------+------------------------------------+-------------------------+ | 99921-58-10-7 | Divine Comedy Divine Comedy Divine | Dante Alighieri | | | Comedy Divine Comedy Divine Comedy | | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens Charles | | | | Dickens Charles Dickens | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+------------------------------------+-------------------------+ """ assert expected == io.fetch_output() def test_render_with_word_wrapping_utf8(io): style = TableStyle.ascii() table = Table(style) table.set_header_row(["ISBN", "Title", "Author"]) table.add_rows( [ [ "99921-58-10-7", "Diviné Cömédy Diviné Cömédy Diviné Cömédy Diviné Cömédy Diviné Cömédy ", "Dante Alighieri", ], [ "9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens Charles Dickens Charles Dickens", ], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ] ) table.render(io) expected = """\ +---------------+------------------------------------+-------------------------+ | ISBN | Title | Author | +---------------+------------------------------------+-------------------------+ | 99921-58-10-7 | Diviné Cömédy Diviné Cömédy Diviné | Dante Alighieri | | | Cömédy Diviné Cömédy Diviné Cömédy | | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens Charles | | | | Dickens Charles Dickens | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+------------------------------------+-------------------------+ """ assert expected == io.fetch_output() def test_render_with_more_word_wrapping(io): style = TableStyle.ascii() table = Table(style) table.set_header_row(["ISBN", "Title", "Author"]) table.add_rows( [ [ "99921-58-10-7 99921-58-10-7 99921-58-10-7 99921-58-10-7", "Divine Comedy Divine Comedy Divine Comedy Divine Comedy Divine Comedy ", "Dante Alighieri", ], [ "9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens Charles Dickens Charles Dickens", ], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ] ) table.render(io) expected = """\ +---------------+------------------------------------+-------------------------+ | ISBN | Title | Author | +---------------+------------------------------------+-------------------------+ | 99921-58-10-7 | Divine Comedy Divine Comedy Divine | Dante Alighieri | | 99921-58-10-7 | Comedy Divine Comedy Divine Comedy | | | 99921-58-10-7 | | | | 99921-58-10-7 | | | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens Charles | | | | Dickens Charles Dickens | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+------------------------------------+-------------------------+ """ assert expected == io.fetch_output() def test_render_with_word_cuts(io): style = TableStyle.ascii() table = Table(style) table.set_header_row(["ISBN", "Title", "Author"]) table.add_rows( [ [ "99921-58-10-7", "DivineComedyDivineComedyDivineComedyDivineComedyDivineComedy ", "Dante Alighieri", ], [ "9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens Charles Dickens Charles Dickens", ], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ] ) table.render(io) expected = """\ +---------------+----------------------------------+-------------------------+ | ISBN | Title | Author | +---------------+----------------------------------+-------------------------+ | 99921-58-10-7 | DivineComedyDivineComedyDivineCo | Dante Alighieri | | | medyDivineComedyDivineComedy | | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens Charles | | | | Dickens Charles Dickens | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+----------------------------------+-------------------------+ """ assert expected == io.fetch_output() def test_render_with_word_wrapping_and_indentation(io): style = TableStyle.ascii() table = Table(style) table.set_header_row(["ISBN", "Title", "Author"]) table.add_rows( [ [ "99921-58-10-7", "Divine Comedy Divine Comedy Divine Comedy Divine Comedy Divine Comedy ", "Dante Alighieri", ], [ "9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens Charles Dickens Charles Dickens", ], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ] ) table.render(io, indentation=4) expected = """\ +---------------+-----------------------------+-------------------------+ | ISBN | Title | Author | +---------------+-----------------------------+-------------------------+ | 99921-58-10-7 | Divine Comedy Divine Comedy | Dante Alighieri | | | Divine Comedy Divine Comedy | | | | Divine Comedy | | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens Charles | | | | Dickens Charles Dickens | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+-----------------------------+-------------------------+ """ assert expected == io.fetch_output() def test_render_formatted_cells(io): table = Table(TableStyle.ascii()) table.set_header_row(["ISBN", "Title", "Author"]) table.add_rows( [ ["99921-58-10-7", "Divine Comedy", "Dante Alighieri"], ["9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens"], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ] ) table.render(io) expected = """\ +---------------+--------------------------+------------------+ | ISBN | Title | Author | +---------------+--------------------------+------------------+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------------------------+------------------+ """ assert expected == io.fetch_output() def test_set_header_row_fails_if_too_many_cells(): table = Table() table.add_row(["a", "b", "c"]) with pytest.raises(ValueError): table.set_header_row(["a", "b", "c", "d"]) def test_set_header_row_fails_if_too_missing_cells(): table = Table() table.add_row(["a", "b", "c"]) with pytest.raises(ValueError): table.set_header_row(["a", "b"]) def test_add_row_fails_if_too_many_cells(): table = Table() table.set_header_row(["a", "b", "c"]) with pytest.raises(ValueError): table.add_row(["a", "b", "c", "d"]) def test_add_row_fails_if_too_missing_cells(): table = Table() table.set_header_row(["a", "b", "c"]) with pytest.raises(ValueError): table.add_row(["a", "b"]) def test_set_row_fails_if_too_many_cells(): table = Table() table.add_row(["a", "b", "c"]) table.add_row(["a", "b", "c"]) with pytest.raises(ValueError): table.set_row(1, ["a", "b", "c", "d"]) def test_set_row_fails_if_too_missing_cells(): table = Table() table.add_row(["a", "b", "c"]) table.add_row(["a", "b", "c"]) with pytest.raises(ValueError): table.set_row(1, ["a", "b"]) def test_set_rows(io): table = Table() table.set_rows([["a", "b", "c"], ["d", "e", "f"]]) table.render(io) table.set_row(1, ["g", "h", "i"]) table.render(io) expected = """\ +---+---+---+ | a | b | c | | d | e | f | +---+---+---+ +---+---+---+ | a | b | c | | g | h | i | +---+---+---+ """ assert expected == io.fetch_output() clikit-0.6.2/tests/ui/help/000077500000000000000000000000001366776574300155355ustar00rootroot00000000000000clikit-0.6.2/tests/ui/help/__init__.py000066400000000000000000000000001366776574300176340ustar00rootroot00000000000000clikit-0.6.2/tests/ui/help/test_application_help.py000066400000000000000000000117001366776574300224600ustar00rootroot00000000000000from clikit import ConsoleApplication from clikit.api.args import Args from clikit.api.args.format import ArgsFormat from clikit.api.args.format import Option from clikit.api.config import ApplicationConfig from clikit.args import ArgvArgs from clikit.ui.help import ApplicationHelp def test_render(io): config = ApplicationConfig("test-bin") config.set_display_name("The Application") config.add_argument( "global-argument", description='Description of "global-argument"' ) config.add_option("global-option", description='Description of "global-option"') with config.command("command1") as c: c.set_description('Description of "command1"') with config.command("command2") as c: c.set_description('Description of "command2"') with config.command("longer-command3") as c: c.set_description('Description of "longer-command3"') app = ConsoleApplication(config) help = ApplicationHelp(app) help.render(io) expected = """\ The Application USAGE test-bin [--global-option] [] ... [] ARGUMENTS The command to execute The arguments of the command GLOBAL OPTIONS --global-option Description of "global-option" AVAILABLE COMMANDS command1 Description of "command1" command2 Description of "command2" longer-command3 Description of "longer-command3" """ assert expected == io.fetch_output() def test_sort_commands(io): config = ApplicationConfig("test-bin") config.set_display_name("The Application") config.create_command("command3") config.create_command("command1") config.create_command("command2") app = ConsoleApplication(config) help = ApplicationHelp(app) help.render(io) expected = """\ The Application USAGE test-bin [] ... [] ARGUMENTS The command to execute The arguments of the command AVAILABLE COMMANDS command1 command2 command3 """ assert expected == io.fetch_output() def test_render_version(io): config = ApplicationConfig("test-bin", "1.2.3") config.set_display_name("The Application") app = ConsoleApplication(config) help = ApplicationHelp(app) help.render(io) expected = """\ The Application version 1.2.3 USAGE test-bin [] ... [] ARGUMENTS The command to execute The arguments of the command """ assert expected == io.fetch_output() def test_render_default_display_name(io): config = ApplicationConfig("test-bin") app = ConsoleApplication(config) help = ApplicationHelp(app) help.render(io) expected = """\ Test Bin USAGE test-bin [] ... [] ARGUMENTS The command to execute The arguments of the command """ assert expected == io.fetch_output() def test_render_default_no_name(io): config = ApplicationConfig() app = ConsoleApplication(config) help = ApplicationHelp(app) help.render(io) expected = """\ Console Tool USAGE console [] ... [] ARGUMENTS The command to execute The arguments of the command """ assert expected == io.fetch_output() def test_render_global_options_with_preferred_short_name(io): config = ApplicationConfig() config.add_option( "global-option", "g", Option.PREFER_SHORT_NAME, 'Description of "global-option"' ) app = ConsoleApplication(config) help = ApplicationHelp(app) help.render(io) expected = """\ Console Tool USAGE console [-g] [] ... [] ARGUMENTS The command to execute The arguments of the command GLOBAL OPTIONS -g (--global-option) Description of "global-option" """ assert expected == io.fetch_output() def test_render_global_options_with_preferred_long_name(io): config = ApplicationConfig() config.add_option( "global-option", "g", Option.PREFER_LONG_NAME, 'Description of "global-option"' ) app = ConsoleApplication(config) help = ApplicationHelp(app) help.render(io) expected = """\ Console Tool USAGE console [--global-option] [] ... [] ARGUMENTS The command to execute The arguments of the command GLOBAL OPTIONS --global-option (-g) Description of "global-option" """ assert expected == io.fetch_output() def test_render_description(io): config = ApplicationConfig() config.set_help("The help for {script_name}\n\nSecond paragraph") app = ConsoleApplication(config) help = ApplicationHelp(app) help.render(io) expected = """\ Console Tool USAGE console [] ... [] ARGUMENTS The command to execute The arguments of the command DESCRIPTION The help for console Second paragraph """ assert expected == io.fetch_output() clikit-0.6.2/tests/ui/help/test_command_help.py000066400000000000000000000223761366776574300216060ustar00rootroot00000000000000from __future__ import unicode_literals from clikit import ConsoleApplication from clikit.api.args.format import Argument from clikit.api.args.format import Option from clikit.api.config import ApplicationConfig from clikit.ui.help import CommandHelp nbsp = "\u00A0" def test_render(io): config = ApplicationConfig() config.set_name("test-bin") config.add_argument("global-argument", 0, 'Description of "global-argument"') config.add_option("global-option", None, 0, 'Description of "global-option"') with config.command("command") as c: c.set_description('Description of "command"') c.set_help( 'Help of "command {command_name} of {script_name}"\n\nSecond paragraph' ) c.add_alias("command-alias") c.add_argument("argument", 0, 'Description of "argument"') c.add_option("option", None, 0, 'Description of "option"') app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command [--option] [] [] aliases: command-alias ARGUMENTS Description of "global-argument" Description of "argument" OPTIONS --option Description of "option" GLOBAL OPTIONS --global-option Description of "global-option" DESCRIPTION Help of "command command of test-bin" Second paragraph """ assert expected == io.fetch_output() def test_render_required_argument(io): config = ApplicationConfig() config.set_name("test-bin") with config.command("command") as c: c.add_argument("argument", Argument.REQUIRED, 'Description of "argument"') app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command ARGUMENTS Description of "argument" """ assert expected == io.fetch_output() def test_render_option_with_optional_value(io): config = ApplicationConfig() config.set_name("test-bin") with config.command("command") as c: c.add_option("option", None, Option.OPTIONAL_VALUE, 'Description of "option"') app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command [--option{}[<...>]] OPTIONS --option Description of "option" """.format( nbsp ) assert expected == io.fetch_output() def test_render_option_with_optional_value_short_name_preferred(io): config = ApplicationConfig() config.set_name("test-bin") with config.command("command") as c: c.add_option("option", "o", Option.OPTIONAL_VALUE, 'Description of "option"') app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command [-o{}[<...>]] OPTIONS -o (--option) Description of "option" """.format( nbsp ) assert expected == io.fetch_output() def test_render_option_with_optional_value_long_name_preferred(io): config = ApplicationConfig() config.set_name("test-bin") with config.command("command") as c: c.add_option( "option", "o", Option.OPTIONAL_VALUE | Option.PREFER_LONG_NAME, 'Description of "option"', ) app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command [--option{}[<...>]] OPTIONS --option (-o) Description of "option" """.format( nbsp ) assert expected == io.fetch_output() def test_render_option_with_required_value(io): config = ApplicationConfig() config.set_name("test-bin") with config.command("command") as c: c.add_option("option", None, Option.REQUIRED_VALUE, 'Description of "option"') app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command [--option{}<...>] OPTIONS --option Description of "option" """.format( nbsp ) assert expected == io.fetch_output() def test_render_option_with_default_value(io): config = ApplicationConfig() config.set_name("test-bin") with config.command("command") as c: c.add_option( "option", None, Option.OPTIONAL_VALUE, 'Description of "option"', "Default" ) app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command [--option{}[<...>]] OPTIONS --option Description of "option" (default: "Default") """.format( nbsp ) assert expected == io.fetch_output() def test_render_option_with_named_value(io): config = ApplicationConfig() config.set_name("test-bin") with config.command("command") as c: c.add_option( "option", None, Option.OPTIONAL_VALUE, 'Description of "option"', value_name="value", ) app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command [--option{}[]] OPTIONS --option Description of "option" """.format( nbsp ) assert expected == io.fetch_output() def test_render_command_with_sub_commands(io): config = ApplicationConfig() config.set_name("test-bin") config.add_option( "global-option", "g", description='Description of "global-option"' ) with config.command("command") as c: c.add_argument("argument", 0, 'Description of "argument"') c.add_option("option", None, 0, 'Description of "option"') with c.sub_command("add") as sc1: sc1.set_description('Description of "add"') sc1.add_argument("sub-argument1", 0, 'Description of "sub-argument1"') sc1.add_argument("sub-argument2", 0, 'Description of "sub-argument2"') sc1.add_option("sub-option1", "o", 0, 'Description of "sub-option1"') sc1.add_option("sub-option2", None, 0, 'Description of "sub-option2"') with c.sub_command("delete") as sc2: sc2.set_description('Description of "delete"') app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command [--option] [] or: test-bin command add [-o] [--sub-option2] [] [] [] or: test-bin command delete [] ARGUMENTS Description of "argument" COMMANDS add Description of "add" Description of "sub-argument1" Description of "sub-argument2" -o (--sub-option1) Description of "sub-option1" --sub-option2 Description of "sub-option2" delete Description of "delete" OPTIONS --option Description of "option" GLOBAL OPTIONS -g (--global-option) Description of "global-option" """ assert expected == io.fetch_output() def test_sort_sub_commands(io): config = ApplicationConfig() config.set_name("test-bin") with config.command("command") as c: c.create_sub_command("sub3") c.create_sub_command("sub1") c.create_sub_command("sub2") app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command or: test-bin command sub3 or: test-bin command sub1 or: test-bin command sub2 COMMANDS sub1 sub2 sub3 """ assert expected == io.fetch_output() def test_render_command_with_default_sub_command(io): config = ApplicationConfig() config.set_name("test-bin") with config.command("command") as c: with c.sub_command("add") as sc1: sc1.default() sc1.set_description('Description of "add"') sc1.add_argument("argument", 0, 'Description of "argument"') with c.sub_command("delete") as sc2: sc2.set_description('Description of "delete"') app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command [add] [] or: test-bin command delete COMMANDS add Description of "add" Description of "argument" delete Description of "delete" """ assert expected == io.fetch_output() def test_render_command_with_anonymous_sub_command(io): config = ApplicationConfig() config.set_name("test-bin") with config.command("command") as c: with c.sub_command("add") as sc1: sc1.anonymous() sc1.set_description('Description of "add"') sc1.add_argument("argument", 0, 'Description of "argument"') with c.sub_command("delete") as sc2: sc2.set_description('Description of "delete"') app = ConsoleApplication(config) help = CommandHelp(app.get_command("command")) help.render(io) expected = """\ USAGE test-bin command [] or: test-bin command delete COMMANDS delete Description of "delete" """ assert expected == io.fetch_output() clikit-0.6.2/tests/utils/000077500000000000000000000000001366776574300153305ustar00rootroot00000000000000clikit-0.6.2/tests/utils/__init__.py000066400000000000000000000000001366776574300174270ustar00rootroot00000000000000clikit-0.6.2/tests/utils/test_command.py000066400000000000000000000013521366776574300203600ustar00rootroot00000000000000from clikit.api.command import Command from clikit.api.command import CommandCollection from clikit.api.config.command_config import CommandConfig from clikit.utils.command import find_similar_command_names def test_find_similar_command_names(): foobar_config = CommandConfig("foobar") bar_config = CommandConfig("bar") barfoo_config = CommandConfig("barfoo") fooo_config = CommandConfig("fooo") names = find_similar_command_names( "foo", CommandCollection( [ Command(foobar_config), Command(bar_config), Command(barfoo_config), Command(fooo_config), ] ), ) assert ["fooo", "foobar", "barfoo"] == names clikit-0.6.2/tests/utils/test_terminal.py000066400000000000000000000010301366776574300205460ustar00rootroot00000000000000from clikit.utils.terminal import Terminal def test_terminal_always_has_a_valid_width(mocker): mocker.patch( "clikit.utils.terminal.Terminal._get_terminal_size_windows", return_value=(0, 42), ) mocker.patch( "clikit.utils.terminal.Terminal._get_terminal_size_tput", return_value=(0, 42) ) mocker.patch( "clikit.utils.terminal.Terminal._get_terminal_size_linux", return_value=(0, 42) ) terminal = Terminal() assert 80 == terminal.width assert 42 == terminal.height clikit-0.6.2/tox.ini000066400000000000000000000003251366776574300143410ustar00rootroot00000000000000[tox] isolated_build = true envlist = py27, py35, py36, py37, py38 [testenv] whitelist_externals = poetry commands = poetry run pip install -U pip poetry install -v --no-root poetry run pytest tests/