pax_global_header00006660000000000000000000000064145177754730014536gustar00rootroot0000000000000052 comment=f695cd3de3de03dc617dcb198d26ba2a02081008 cleo-2.1.0/000077500000000000000000000000001451777547300124605ustar00rootroot00000000000000cleo-2.1.0/.github/000077500000000000000000000000001451777547300140205ustar00rootroot00000000000000cleo-2.1.0/.github/FUNDING.yml000066400000000000000000000000241451777547300156310ustar00rootroot00000000000000github: [sdispater] cleo-2.1.0/.github/dependabot.yml000066400000000000000000000002671451777547300166550ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: monthly - package-ecosystem: github-actions directory: "/" schedule: interval: monthly cleo-2.1.0/.github/workflows/000077500000000000000000000000001451777547300160555ustar00rootroot00000000000000cleo-2.1.0/.github/workflows/news.yaml000066400000000000000000000011151451777547300177130ustar00rootroot00000000000000name: Check news file on: pull_request: types: [labeled, unlabeled, opened, reopened, synchronize] jobs: check-news-entry: name: news entry runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 with: # `towncrier check` runs `git diff --name-only origin/main...`, which # needs a non-shallow clone. fetch-depth: 0 - name: Check news entry if: "!contains(github.event.pull_request.labels.*.name, 'skip news')" run: | pipx run towncrier check --compare-with origin/${{ github.base_ref }} cleo-2.1.0/.github/workflows/release.yml000066400000000000000000000037071451777547300202270ustar00rootroot00000000000000name: Release on: push: tags: - '*.*.*' jobs: Build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Get tag id: tag run: | echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_OUTPUT - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: "3.10" - name: Install and set up Poetry run: | curl -sL https://install.python-poetry.org | python - -y - name: Update PATH shell: bash run: echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Build distributions run: | poetry build -vvv - name: Upload distribution artifacts uses: actions/upload-artifact@v3 with: name: project-dist path: dist Publish: needs: [Build] runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Get tag id: tag run: | echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_OUTPUT - name: Download distribution artifact uses: actions/download-artifact@master with: name: project-dist path: dist - name: Install and set up Poetry run: | curl -sL https://install.python-poetry.org | python - -y - name: Update PATH shell: bash run: echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Check distributions run: | ls -la dist - name: Publish to PyPI env: POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} run: | poetry publish - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} with: tag_name: ${{ steps.tag.outputs.tag }} release_name: ${{ steps.tag.outputs.tag }} draft: false prerelease: false cleo-2.1.0/.github/workflows/tests.yml000066400000000000000000000017561451777547300177530ustar00rootroot00000000000000name: Tests on: pull_request: push: branches: [main] jobs: Tests: name: ${{ matrix.os }} / ${{ matrix.python-version }} runs-on: ${{ matrix.image }} strategy: matrix: os: [Ubuntu, macOS, Windows] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] include: - os: Ubuntu image: ubuntu-22.04 - os: Windows image: windows-2022 - os: macOS image: macos-12 defaults: run: shell: bash steps: - uses: actions/checkout@v4 - name: Install Poetry run: pipx install poetry - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: poetry - name: Install dependencies run: poetry install - name: Run typechecking run: poetry run mypy - name: Run tests run: poetry run pytest cleo-2.1.0/.gitignore000066400000000000000000000003401451777547300144450ustar00rootroot00000000000000*.pyc # Packages *.egg *.egg-info dist build .cache # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml .DS_Store .idea/* .python-version test.py /test .pytest_cache .vscode *.patch cleo-2.1.0/.pre-commit-config.yaml000066400000000000000000000003321451777547300167370ustar00rootroot00000000000000ci: autofix_prs: false repos: - repo: https://github.com/psf/black rev: 23.10.1 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.1.1 hooks: - id: ruff cleo-2.1.0/CHANGELOG.md000066400000000000000000000231031451777547300142700ustar00rootroot00000000000000# Changelog ## [2.1.0] - 2023-10-30 ### Features & Improvements - Added support for Python 3.12 [#379](https://github.com/python-poetry/cleo/pull/379) - Added `CONTRIBUTING.md` document [#331](https://github.com/python-poetry/cleo/pull/331) - Added `tests/` directory to sdist artifact [#327](https://github.com/python-poetry/cleo/pull/327) ### Bug fixes - Fixed subcommand completions for Fish [#359](https://github.com/python-poetry/cleo/pull/359) - Removed deprecated `-A` option from Fish completions [#366](https://github.com/python-poetry/cleo/pull/366) - Fixed program name discovery in completions script when running as module [#231](https://github.com/python-poetry/cleo/pull/231) - Fixed ANSI coloring detection in virtual terminal environments (Windows, PyCharm) [#104](https://github.com/python-poetry/cleo/pull/104) - Fixed terminal size detection [#299](https://github.com/python-poetry/cleo/pull/299) ## [2.0.1] - 2022-11-23 - Relax `poetry-core` requirement for PEP 517 builds ([#291](https://github.com/python-poetry/cleo/pull/291)). ## [2.0.0] - 2022-11-21 No source code changes. This is a version-only release to replace `1.0.0`, which was yanked on the grounds that it was incompatible with real dependents (i.e. Poetry) based on their version specifiers, which explicitly included `1.0.0` pre-releases. ## [1.0.0] - 2022-11-21 ### Key points - Supported Python versions are now 3.7 up to 3.11. - `cleo` is now fully type-checked. - `cleo` no longer depends on `clikit`. ### Changed - Replaced `Terminal` class with `shutil.get_terminal_size()` from standard library ([#175](https://github.com/python-poetry/cleo/pull/175)). - Exceptions are now Errors ([#179](https://github.com/python-poetry/cleo/pull/179)). - `pylev` was dropped in favor of much faster `rapidfuzz` ([#173](https://github.com/python-poetry/cleo/pull/173)). - Default error verbosity was reduced ([#132](https://github.com/python-poetry/cleo/pull/132) & [#166](https://github.com/python-poetry/cleo/pull/166)). ### Removed - Removed doc comment-based command configuration notation ([#239](https://github.com/python-poetry/cleo/pull/239)). ### Fixed - `--no-interaction` is now automatically set when running in non-TTY terminals ([#245](https://github.com/python-poetry/cleo/pull/245)). - Generated completions will no longer cause shell errors for namespaced commands ([#247](https://github.com/python-poetry/cleo/pull/247)). - Using `^C` while autocompleting `Question` answer will no longer break terminal ([#240](https://github.com/python-poetry/cleo/pull/240)). - Namespaced commands no longer reset interactive state ([#234](https://github.com/python-poetry/cleo/pull/234)). - Fixed underlying regex that caused CVE-2022-42966 ([#285](https://github.com/python-poetry/cleo/pull/285)). ## [0.8.1] - 2020-04-17 ### Changed - Upgraded `clikit` to version `^0.6.0`. ## [0.8.0] - 2020-03-26 ### Added - Errors are now rendered in a nicer way for Python 3.6+. ## [0.7.6] - 2019-10-25 ### Fixed - Upgraded `clikit` to fix issues in option parsing. ## [0.7.5] - 2019-06-28 ### Fixed - Upgraded dependency requirements for bug fixes. ## [0.7.4] - 2019-05-15 ### Fixed - Fixed command construction with the `argument` and `option` helpers. ## [0.7.3] - 2019-05-12 ### Added - Added the `argument` and `option` helpers. ### Fixed - Fixed the `decorated` option for the command tester. - Fixed tested applications being terminated after execution. ## [0.7.2] - 2018-12-08 ### Fixed - Fixed invalid combination of OPTIONAL_VALUE and MULTI_VALUED flags for options. ## [0.7.1] - 2018-12-07 ### Fixed - Fixed parser not setting proper flags. ## [0.7.0] - 2018-12-07 This version breaks backwards compatibility and caution is advised when updating. While the public API of the `Command` class is mostly the same, a lot of the internals has changed or has been removed. Cleo is now mostly a higher level wrapper for [CliKit](https://github.com/sdispater/clikit) which is more flexible. ### Added - Added a sub command system via CliKit. - Added an event system via CliKit. ### Changed - All helper classes have been removed. If you use the `Command` methods this should not affect you. - The testers `get_display()` method has been removed. Use `tester.io.fetch_output()`. - The testers `execute()` method no longer requires the command name and requires a string as arguments instead of a list. - The testers `execute()` method now accepts a `inputs` keyword argument to pass user inputs. - The `call()` method no longer requires the command name and requires a string as arguments instead of a list. - The tables now automatically wraps the cells based on the available width. - The table separators and table cells elements have been removed. - The look and feel of the `help` command has changed. - Namespace commands are no longer supported and will be treated as standard commands. - The `list` command has been removed and merged with `help`. ## [0.6.8] - 2018-06-25 ### Changed - Testers (application and command) now automatically sets `decorated` to `False`. ### Fixed - Fixed numeric values appearing when getting terminal size on Windows. ## [0.6.7] - 2018-06-25 ### Fixed - Fixed verbosity option behavior. ## [0.6.6] - 2018-05-21 ### Fixed - Fixed an error for choice questions with only one choice. ## [0.6.5] - 2018-04-04 ### Fixed - Fixed handling of KeyboardInterrupt. ## [0.6.4] - 2018-03-15 ### Fixed - Fixed bad python version requirements. ## [0.6.3] - 2018-03-15 ### Fixed - Fixed bad python version requirements. ## [0.6.2] - 2018-03-15 ### Changed - Removed the memory formatter in progress bars and indicators ### Fixed - Fixed an error in the `call()` method. ## [0.6.1] - 2017-08-07 ### Changed - `psutil` is now opt-in to avoid failed compilations. ## [0.6.0] - 2017-04-21 ### Added - Added a new `completions` command to generate autocompletion scripts. - Added support for command signature inheritance. - Added a new `spin()` helper to display a spinner. ### Changed - Removed the `_completion` command. - Removes ability to choose when a command name is ambiguous. ## [0.5.0] - 2016-09-21 ### Added - Improves terminal handling by adding the `Terminal` class. - Adds methods to write to stderr. - Adds `write()` and `overwrite()` method to commands. - Adds ability to regress progress bar. - Adds ability to choose when a command name is ambiguous. ### Changed - Removes support for decorators and dictionaries declarations. - Simplifies public API for creating arguments and options. - Improves string formatting. - Hides `_completion` command from list. - Improves aliases display. - Displays errors even in quiet mode - Changes console header format - Simplifies the way to create single command application. - Simplifies command testing with user inputs. ## [0.4.1] - 2016-02-09 ### Added - Adding support for Windows ## [0.4] - 2016-01-11 This is a major release with some API changes. ### Added - Commands definition can now be specified with the class docstring (support for string signature) - Two other levels of verbosity (`-vv` and `-vvv`) have been added - Commands description can now be output as json and markdown ### Changed - The `Command` class is now more high-level with a single `handle()` method to override and useful helper methods - The ``ProgressHelper`` has been removed and the ``ProgressBar`` class must be used - The `TableHelper` has largely been improved - `DialogHelper` has been replaced by a more robust `QuestionHelper` - `Command.set_code()` logic has changed to accept a `Command` instance to be able to use the new helper methods - Autocompletion has been improved ### Fixed - Values are now properly cast by validators - Fixing "flag" not being set properly - Progress bar now behaves properly (Fixes [#37](https://github.com/python-poetry/cleo/issues/37)) - The `-n|--no-interaction` option behaves properly (Fixes [#38](https://github.com/python-poetry/cleo/issues/39) and [#39](https://github.com/python-poetry/cleo/issues/39)) [unreleased]: https://github.com/python-poetry/cleo/compare/2.0.1...main [2.0.1]: https://github.com/python-poetry/cleo/releases/tag/2.0.1 [2.0.0]: https://github.com/python-poetry/cleo/releases/tag/2.0.0 [1.0.0]: https://github.com/python-poetry/cleo/releases/tag/1.0.0 [0.8.1]: https://github.com/python-poetry/cleo/releases/tag/0.8.1 [0.8.0]: https://github.com/python-poetry/cleo/releases/tag/0.8.0 [0.7.6]: https://github.com/python-poetry/cleo/releases/tag/0.7.6 [0.7.5]: https://github.com/python-poetry/cleo/releases/tag/0.7.5 [0.7.4]: https://github.com/python-poetry/cleo/releases/tag/0.7.4 [0.7.3]: https://github.com/python-poetry/cleo/releases/tag/0.7.3 [0.7.2]: https://github.com/python-poetry/cleo/releases/tag/0.7.2 [0.7.1]: https://github.com/python-poetry/cleo/releases/tag/0.7.1 [0.7.0]: https://github.com/python-poetry/cleo/releases/tag/0.7.0 [0.6.8]: https://github.com/python-poetry/cleo/releases/tag/0.6.8 [0.6.7]: https://github.com/python-poetry/cleo/releases/tag/0.6.7 [0.6.6]: https://github.com/python-poetry/cleo/releases/tag/0.6.6 [0.6.5]: https://github.com/python-poetry/cleo/releases/tag/0.6.5 [0.6.4]: https://github.com/python-poetry/cleo/releases/tag/0.6.4 [0.6.3]: https://github.com/python-poetry/cleo/releases/tag/0.6.3 [0.6.2]: https://github.com/python-poetry/cleo/releases/tag/0.6.2 [0.6.1]: https://github.com/python-poetry/cleo/releases/tag/0.6.1 [0.6.0]: https://github.com/python-poetry/cleo/releases/tag/0.6.0 [0.5.0]: https://github.com/python-poetry/cleo/releases/tag/0.5.0 [0.4.1]: https://github.com/python-poetry/cleo/releases/tag/0.4.1 [0.4]: https://github.com/python-poetry/cleo/releases/tag/0.4 cleo-2.1.0/CONTRIBUTING.md000066400000000000000000000034761451777547300147230ustar00rootroot00000000000000# Contribute to Cleo ## Project Management The Cleo project is managed by [Poetry](https://python-poetry.org/). You need to install it first: ```bash pipx install poetry ``` For other installation methods, please refer to [Poetry's documentation](https://python-poetry.org/docs/#installation). After the installation, install the project and dependencies: ```bash poetry install ``` ## Run the Tests ```bash poetry run pytest ``` ## Code Style and Linters We use [Black](https://github.com/psf/black) to format the code and [Ruff](https://github.com/charliermarsh/ruff) as the linter. They are all integrated into [pre-commit](https://pre-commit.com/). You can enable it by: > NOTICE: `pre-commit` is declared as one of development dependencies of Cleo. If you don't have `pre-commit` installed globally, prepend the commands in this section with `poetry run` ```bash pre-commit install ``` and run the checks by: ```bash pre-commit run --all-files ``` ### News fragments When you make changes such as fixing a bug or adding a feature, you must add a news fragment describing your change. News fragments are placed in the `news/` directory, and should be named according to this pattern: `..md` (e.g., `566.bugfix.md`). > NOTICE: If your change doesn't have an issue, please use PR number in place of `` #### Issue Types - `break`: Breaking changes - `feat`: Features & Improvements - `bugfix`: Bug fixes - `docs`: Changes to documentation - `deps`: Changes to dependencies - `removal`: Removals or deprecations in the API - `misc`: Miscellaneous changes that don't fit any of the other categories The contents of the file should be a single sentence in past tense that describes your changes (e.g., `Added CONTRIBUTING.md file.`). See entries in the [Change Log](/CHANGELOG.md) for more examples. cleo-2.1.0/LICENSE000066400000000000000000000020451451777547300134660ustar00rootroot00000000000000Copyright (c) 2013 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.cleo-2.1.0/README.md000066400000000000000000000262201451777547300137410ustar00rootroot00000000000000# Cleo [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) [![Tests](https://github.com/python-poetry/cleo/actions/workflows/tests.yml/badge.svg)](https://github.com/python-poetry/cleo/actions/workflows/tests.yml) [![PyPI version](https://img.shields.io/pypi/v/cleo)](https://pypi.org/project/cleo/) Create beautiful and testable command-line interfaces. ## Resources - [Documentation](http://cleo.readthedocs.io) - [Issue Tracker](https://github.com/python-poetry/cleo/issues) ## Usage To make a command that greets you from the command line, create `greet_command.py` and add the following to it: ```python from cleo.commands.command import Command from cleo.helpers import argument, option class GreetCommand(Command): name = "greet" description = "Greets someone" arguments = [ argument( "name", description="Who do you want to greet?", optional=True ) ] options = [ option( "yell", "y", description="If set, the task will yell in uppercase letters", flag=True ) ] def handle(self): name = self.argument("name") if name: text = f"Hello {name}" else: text = "Hello" if self.option("yell"): text = text.upper() self.line(text) ``` You also need to create the file `application.py` to run at the command line which creates an `Application` and adds commands to it: ```python #!/usr/bin/env python from greet_command import GreetCommand from cleo.application import Application application = Application() application.add(GreetCommand()) if __name__ == "__main__": application.run() ``` Test the new command by running the following ```bash $ python application.py greet John ``` This will print the following to the command line: ```text Hello John ``` You can also use the `--yell` option to make everything uppercase: ```bash $ python application.py greet John --yell ``` This prints: ```text HELLO JOHN ``` ### Coloring the Output Whenever you output text, you can surround the text with tags to color its output. For example: ```python # blue text self.line("foo") # green text self.line("foo") # cyan text self.line("foo") # bold red text self.line("foo") ``` The closing tag can be replaced by ``, which revokes all formatting options established by the last opened tag. It is possible to define your own styles using the `add_style()` method: ```python self.add_style("fire", fg="red", bg="yellow", options=["bold", "blink"]) self.line("foo") ``` Available foreground and background colors are: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan` and `white`. And available options are: `bold`, `underscore`, `blink`, `reverse` and `conceal`. You can also set these colors and options inside the tag name: ```python # green text self.line("foo") # black text on a cyan background self.line("foo") # bold text on a yellow background self.line("foo") ``` ### Verbosity Levels Cleo has four verbosity levels. These are defined in the `Output` class: | Mode | Meaning | Console option | | ------------------------ | ---------------------------------- | ----------------- | | `Verbosity.QUIET` | Do not output any messages | `-q` or `--quiet` | | `Verbosity.NORMAL` | The default verbosity level | (none) | | `Verbosity.VERBOSE` | Increased verbosity of messages | `-v` | | `Verbosity.VERY_VERBOSE` | Informative non essential messages | `-vv` | | `Verbosity.DEBUG` | Debug messages | `-vvv` | It is possible to print a message in a command for only a specific verbosity level. For example: ```python if Verbosity.VERBOSE <= self.io.verbosity: self.line(...) ``` There are also more semantic methods you can use to test for each of the verbosity levels: ```python if self.output.is_quiet(): # ... if self.output.is_verbose(): # ... ``` You can also pass the verbosity flag directly to `line()`. ```python self.line("", verbosity=Verbosity.VERBOSE) ``` When the quiet level is used, all output is suppressed. ### Using Arguments The most interesting part of the commands are the arguments and options that you can make available. Arguments are the strings - separated by spaces - that come after the command name itself. They are ordered, and can be optional or required. For example, add an optional `last_name` argument to the command and make the `name` argument required: ```python class GreetCommand(Command): name = "greet" description = "Greets someone" arguments = [ argument( "name", description="Who do you want to greet?", ), argument( "last_name", description="Your last name?", optional=True ) ] options = [ option( "yell", "y", description="If set, the task will yell in uppercase letters", flag=True ) ] ``` You now have access to a `last_name` argument in your command: ```python last_name = self.argument("last_name") if last_name: text += f" {last_name}" ``` The command can now be used in either of the following ways: ```bash $ python application.py greet John $ python application.py greet John Doe ``` It is also possible to let an argument take a list of values (imagine you want to greet all your friends). For this it must be specified at the end of the argument list: ```python class GreetCommand(Command): name = "greet" description = "Greets someone" arguments = [ argument( "names", description="Who do you want to greet?", multiple=True ) ] options = [ option( "yell", "y", description="If set, the task will yell in uppercase letters", flag=True ) ] ``` To use this, just specify as many names as you want: ```bash $ python application.py greet John Jane ``` You can access the `names` argument as a list: ```python names = self.argument("names") if names: text = "Hello " + ", ".join(names) ``` ### Using Options Unlike arguments, options are not ordered (meaning you can specify them in any order) and are specified with two dashes (e.g. `--yell` - you can also declare a one-letter shortcut that you can call with a single dash like `-y`). Options are _always_ optional, and can be setup to accept a value (e.g. `--dir=src`) or simply as a boolean flag without a value (e.g. `--yell`). > _Tip_: It is also possible to make an option _optionally_ accept a value (so > that `--yell` or `--yell=loud` work). Options can also be configured to > accept a list of values. For example, add a new option to the command that can be used to specify how many times in a row the message should be printed: ```python class GreetCommand(Command): name = "greet" description = "Greets someone" arguments = [ argument( "name", description="Who do you want to greet?", optional=True ) ] options = [ option( "yell", "y", description="If set, the task will yell in uppercase letters", flag=True ), option( "iterations", description="How many times should the message be printed?", default=1 ) ] ``` Next, use this in the command to print the message multiple times: ```python for _ in range(int(self.option("iterations"))): self.line(text) ``` Now, when you run the task, you can optionally specify a `--iterations` flag: ```bash $ python application.py greet John $ python application.py greet John --iterations=5 ``` The first example will only print once, since `iterations` is empty and defaults to `1`. The second example will print five times. Recall that options don\'t care about their order. So, either of the following will work: ```bash $ python application.py greet John --iterations=5 --yell $ python application.py greet John --yell --iterations=5 ``` ### Testing Commands Cleo provides several tools to help you test your commands. The most useful one is the `CommandTester` class. It uses a special IO class to ease testing without a real console: ```python from greet_command import GreetCommand from cleo.application import Application from cleo.testers.command_tester import CommandTester def test_execute(): application = Application() application.add(GreetCommand()) command = application.find("greet") command_tester = CommandTester(command) command_tester.execute() assert "..." == command_tester.io.fetch_output() ``` The `CommandTester.io.fetch_output()` method returns what would have been displayed during a normal call from the console. `CommandTester.io.fetch_error()` is also available to get what you have been written to the stderr. You can test sending arguments and options to the command by passing them as a string to the `CommandTester.execute()` method: ```python from greet_command import GreetCommand from cleo.application import Application from cleo.testers.command_tester import CommandTester def test_execute(): application = Application() application.add(GreetCommand()) command = application.find("greet") command_tester = CommandTester(command) command_tester.execute("John") assert "John" in command_tester.io.fetch_output() ``` You can also test a whole console application by using the `ApplicationTester` class. ### Calling an existing Command If a command depends on another one being run before it, instead of asking the user to remember the order of execution, you can call it directly yourself. This is also useful if you want to create a \"meta\" command that just runs a bunch of other commands. Calling a command from another one is straightforward: ```python def handle(self): return_code = self.call("greet", "John --yell") return return_code ``` If you want to suppress the output of the executed command, you can use the `call_silent()` method instead. ### Autocompletion Cleo supports automatic (tab) completion in `bash`, `zsh` and `fish`. By default, your application will have a `completions` command. To register these completions for your application, run one of the following in a terminal (replacing `[program]` with the command you use to run your application): ```bash # Bash [program] completions bash | sudo tee /etc/bash_completion.d/[program].bash-completion # Bash - macOS/Homebrew (requires `brew install bash-completion`) [program] completions bash > $(brew --prefix)/etc/bash_completion.d/[program].bash-completion # Zsh mkdir ~/.zfunc echo "fpath+=~/.zfunc" >> ~/.zshrc [program] completions zsh > ~/.zfunc/_[program] # Zsh - macOS/Homebrew [program] completions zsh > $(brew --prefix)/share/zsh/site-functions/_[program] # Fish [program] completions fish > ~/.config/fish/completions/[program].fish ``` cleo-2.1.0/docs/000077500000000000000000000000001451777547300134105ustar00rootroot00000000000000cleo-2.1.0/docs/.gitignore000066400000000000000000000000071451777547300153750ustar00rootroot00000000000000_build cleo-2.1.0/docs/Makefile000066400000000000000000000011721451777547300150510ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) cleo-2.1.0/docs/_static/000077500000000000000000000000001451777547300150365ustar00rootroot00000000000000cleo-2.1.0/docs/_static/progress.gif000066400000000000000000000370721451777547300174020ustar00rootroot00000000000000GIF89a@!1 HFHQOQRQSX\b^decefgjnknpprtrstu{uwyzz}~~Ā̀ӀӑƓeꖦߘ՝oƪ֪߫󮰸ǯͱҴ˸нþč˿ĩ˨˹̥ηϡϩϿŢɻٳٿ߳ȳ㣩㮰㹁㽊䡣朚ֿغͻϮ좢x캂ƜテѣmŔǶϭԲÊؾqڹוڰ֡! NETSCAPE2.0!,@! H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ }УHܶdFJʓ)G j‹q(2K~YKۦ?ra5دߵjծ;ӪDZ>TUkO1:3w`8TdAҞ$I[f11af `7JBaj7jĢe65qjS<(͟cM'M(QK\cm (S"DD)r-7ƬU 5é2"0-15$vpLT{l@Efbc"YFa?hY7PT%TX=XUh]Z2Fc8c/ b+bY\Ex8)PT:B5?BH$GFAD5P1;J& V(@[ ~,8-tq0  ލB!, m޸ -^ =Ex * z*hjOEZ(ˠ"&h`a]PJfS#;a|xU!#b>{` ~? m>MX1LAM\A/VDMԱM?,aRſW4Qw6r,ܱqXسJ 7?\pD!Q0iM(4Tpq ,D}01"S2'-̵3 9=?3L023B? jq BXH`WO9&^LakUe)ۏ&8C!ڃ*Aٝ0 T V1T\SXmlVT#LUt=+ 1EKVTU"uXG򝅁\@M8s!bڻ(&pKN/n[I 8/0U ZGҗ;aW8F}QXZhA!k"uG88iL# T81B*6`TDC*X<% A0P `j(KJT+\%DRRY?ǕrRC  [T=Mo١Cƈ`f<6 a2U0 !-(^;"80~*`?lj8 0+p9ч2@e ưGS졍 rT@Ir G=Ct#7pdʩzL8)z3+QQu^g6ҩ~ԞoYO ?!u“oy(@9Ї^e7bmسH>"݂ ($b>51PQKl>S5!m3,i O0砃9M378# MQ]L6I? @ D?Y2B?K7>FPe0 '`I#pcG+LB4AAphN&CB=W P4-6oS4hLD ? E@<Y6+9;W.(#H@W DjTEl}8&(zK tM5ӇXk@J 0(YQO`a)0"61m $hUS CǞ+Ds ^@T3FTj K)P0-KuhRT- JeH'=_SuPC!,? !&\s`$XB ;!,? !,? !,'HП?*\ȰÇ#JHŋ3jϡ={4ɓ(S\b~(mkI͛8sLs G֬m8@ҦIJ*F{P &Tƈ aZ,rPxkK^jJ B $-hoS߽G; :\=x> gԈ$z˷ #jӌ%0+Y #{kAf=qX͝pވ=:Tk74㿏SPa(*@a@5@E= SAנW2AO=ܔM0lCbO;L;LQ10SSO?Ө6 u dE n|=G i> _IXsM>ɘC98Λ#M'渉y4s9p357r#Pd&:= # f%~ υE~3Ё(]s@$ H&L2 %C,N pJ+w0#(<5?B%0>4@(5#P8(<|S/9ջZkr?h zb[e d LQp1 R6^h\|G"ȇ!#,smAp@@d](," *^O&p20@)˕5PE2ޖlU, Ң\i/pTdL%3[ $m I%Ї|xQбv?W#$V tP?ʪ4t?dD!, %H*\ȰÇ#JHŋȱǏ CIɓ(S\IrBm3͛8sꜨlgIx 糨QDQoyX'О 2pG5!,U0U\ EKhR_] j4n|Gt 2Pٔ*{ ,eҢeM@L[i/c@5ϲN`=n@ z^4D)NguntimV?`$0o/ !,? !,? !,'H?*\ȰÇكHŋ3jȱE籤ɓ(S\2Ȅ!3M۶yϟ7%7'EpG(ƙ0˦TsʵәGR/F(Qg@~^M DHZ˷ljn!\H0*T_m;µ9w WX(cӉC/ލP"M(Z^^<۞ ~ ̛;dΛ7^3:YsΚMQQk>81$"n t\ElDM@QUq#XJ!LcLXams3~b57,PH*8M6 3k16, ͐I-M?Eפf-edmx|/ TS߀R@!,V 38*\@k [&HAx $!,? !,? !,)H*\ȰÇ#JHŋȱǏ CI2F'WR+[,I͛7jt_?3L81h^ҧP9˂CڶCJlmŽչӠqƮ )$PS۬9Z5|]z\{&v0E か9m|+bC7n2 \qⴣ)W0#]Rup~D!?{{PyΜN~@uz (L/)5˷D•Aq5(PPAK8N= ӎO}tO8:9AOZ5Ss0FRp`#ָ(B W TcU@A>C>A %vqwq dw&ű6%Ao=qt@oD<y 4V>8,`#N4PHX 8_3 !=Dx A/! $H!(,1A[M?ĤØA(S5 1ܔM/㴃=S FP P8dEpLEP/#0J5BKnU@G (9 t9 O'/ e-8Q\(:x ͤc7LN1? BV@V6(aAg"(_,@hq k'Y$18֨1,-8XA8J#j6H |z'6K+0# 1 4eWzM?H55զo %$2L @ }-<,r SFP,l's@A5 {x$hJ 3,h%2c`pF1$`gaX4dQ7 d%n9 "TtP AX6%,QEڲ FP* GSSR "Ô&Sa#@i= TX{((D00àe/{#L3 -c?V_|)j@/M Lأ?kD K! d>O +Qb .*1E9* AT!IG-2yV"(XfOR<юz{B?JҒ:g&MJAz{Vt0iH)Ӛ4"!,? !,? !,? !,#*\ȰÇ#JHŋ3jѡmɓ!Dɲ˗ ͆2e yϞ2򜩐ΟH*]"щA 2ڴkD׮m R؅F]5!?pC7c C5l!Ohb!qKa^XAUQc MО}A5S_2+̴ ݷwn<9plfT #IvTYеj; QIh԰e 7ּٿf:fG5C iE߄X#Lm=30 q@N@pAA %\A[zS iO0;xc0Cܜ25GOeTx)*P , m(HL}e38\#O蘃D#.9砃;֤#':̘ 3}=$`h#X!"4TA´#80a]&DZB?0b9ՌuMB`2 %Č# &LrG:dI42ّ!4eZO#qx=p" C((v-Rs@<#3N8 **GS@-rL4c2&@@SxXr81 'R HOAijە#rp۴ :褓~砉N0S=P]r|q{4P)HAm5 0ÁY=~F5१d`b7v$&brG; Zs֦  À(h'PGpM=s"֨셣Q8ddFUPcnbD\?=iX4XZAFl0hl|ho1 c!:5C6D?pEX "TPF|@tkh1_ CB|+|L]SC; C{bfuK!TFG},S?xGC@`\I̱AUwHAo$S86[*߀? @QS'xP0?..(SngH !,H*\ȰÇ83jȱBmCIrd8Fsx˗0'p\E۪$3ϟ>+[ &УH7BF_wn u%Leb6iYϠ5i@ߣD\#`hqcp0  H)Fg[ћM!,? !,? !, ? \ȰÇ#JHŋ3jȱ;LУɓ(S\ɲeGPӥ͛8s܉dȉ.ɳѣk"]*a~ӦelXBqᶆ$ړզ=mQ}Mu߿c9Ȋ;8`߿ M0_N8"-Z`pWdӇJ0[LgvՃw7pXk?p߽ ɃKwTkA$J*"B4&8] rfcΛ8{5jfM#{_%ܸqS-HW O*u@!JlC 7 r5b0턣L5ZAR=سr)2PIq@>DSOPn@9s:cO:͜%7c?t2\VE [Hd@O"yP/&>,3V(E%$ 'r3I+`BG8C$أӃ=dԀ 90R@-44(0b6 ª,Dsn9 Nm7osC8"8L!-n tr*\Y !64}£=O ?O"L364@)oTcr\@XXlgPV?l# ǔXC2Y#TS":b[ltP?E1mK!, x H*\ȰC#PxLj wcƏ ?8P"_Hˮ l͛t Ǹj\V(ч47rt`2a  `$ף`0r˸bmGpݰx3;܂~Ԗhk6e=ؔg`Đ#+ٔi˘Vκ4 !,? !,? !,*H*\ȰÇ#JHŋsQ#Ə CIɏ<ϟ+Oʜ#͋ݜҠM*7| H*]ʴb˞J𩿟ABڴכX]ՠ0 v*}[mۮmSRlc_wxXg{zexeOc4T4gM.XffLҦQb6lxoCIG`Yv7b0F8j)WzQk7oFktH2ltfdwrpk j igi~cơoы=&k1=찂8WP8`0*)W /YPP5Ÿ@!0P@} W?D58F[PjySPo2E @u@ e7 JLnA9 q 5܄c7 Cl1 s b-!M":U2QU ԅNScWLP= bMZ1@&P 2?_zYSM{j5s@ n}A-9^-:d;AK6tl5=g 9 9kc9vl98:ČC?ӄ1ǫ$D`@@e] 9,AA_ D?I Tc^RCjz P54IB(,Ot?1޶"P:>&| ,cPD0V"b|#H@~؈`q@bILhE,Z;D,01Np %h ,$'9W8:A a>`N3B-0d฀Yc=iDVA.xX ^4q|Fk F!8q CcHACq )!Q}Nv'H9*,U-|$p"zJ@x}k]p߽Wۻv&DanCز eFrl}8Ś+w{̼y8bYOߚkl7"u˒6%p[}.`%,Tr1| 7 r4 rL8a43N4Ep,-]% $7S]$16L])C@0pL3栓NiMsN1рI5س '8X!P֤=@8 C,]"(5h1>t̐:P+d:v$ ' JG;Q$s?ml(6q+s?0G 8A(L@ }0l!6]p=Ai&T$aWmi8s9pO2¢s.L3Ħ^D(h lBB!s z!"A }|K\Qqb+p"7 &$,ؓ 'I#pc,L" a^5B4_/ӌ"!"@V0= d*5Fa@>ʢ68C9S8] Xg`S "#1. #Jp@ubim\X NGO?dWVa%XA=\aAI:B O=:(LS?0Ӌ(\}^2?k;5|A^02*ϕZAqIU.ltBT%Cy18F/(b ͉!@yPrp H (a Wm#S gHC*wH$ !,@ H*I'PbE 3j} I_0* LX U +!,? !,? !,? !,? !,/H*\ȰÇ#JHŋaȱǏ C9П? ?0cʜ%I6I$FT(Bɴӧ-_)URU*06MS*բZ] L.W܃nn/@~ V,M-̶IVdl^x3./@UlZ'k$!f۲c9'uM1_㶉8/s8(F`M Q# zLEr7n%rwU S8%M=LqA#P 1@#IJAN;K(Ί#9UψO8'r@jD~ oB A DK mES_]mЇ 1qܱAq!f4™B@&dA~JhBBai{oة@ooiF'/ i|y@wag@rB˒"1PƑFI@?Dm\,c]c@H RU DAh;۔@$8@ ̖Iã@@'1F%Ap#@B1 tp=6Ĩļ1Ab,8*,d)AC3&d < 7~Ƶ8 oMl'( F {bM,F3NU2P9CU{[ @ ?|ti C@9,~9(@0he2{(:,Q;4svy˼0b.~{2͆h=! #(0=m#J!I0фDs@@ۺep0A=0x@&AI+xAL+ȿA4kwQ68Nb a b`3gA&mH >dJE:I b30S$NAJ$;#ȢH0 }A3dBX vM$}R=6Q@ 5IlqqF',Ꮟ}a GFKx ) $0? #bY:$<>FC\L*Hôz$lE$@$PL Y6 2\D,.V , *#< G# Jr^T! 0Z$ l )@A(ְG/aC4◢7}5Гc  Azeե.wYkU4j([CkU(j ޺*|}I[:.AfJup]j֠vY9QTlپJn+KVxB(v66Dp{m.?$׎=j!O˖~{OhbR>׺2 ~!iIR.(?I &I(Cl 46AӏH%F $~Lx |;`8#yi'`{XΰՒFx 1]2(J@!, 8c%0D(Z"i&0F@{ۖE;cleo-2.1.0/docs/_static/theme_overrides.css000066400000000000000000000666561451777547300207570ustar00rootroot00000000000000@import url(http://fonts.googleapis.com/css?family=Montserrat:400,700); @import url(http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400,500); body { font: 400 13px/26px "Open Sans",sans-serif; color: #687E95; text-rendering: optimizelegibility; background-color: #FFFFFF; } .wy-body-for-nav { background: #FFFFFF; } .wy-nav-content { background: #FFFFFF; } .wy-nav-content-wrap { background: #FFFFFF; margin-left: 300px; padding-left: 70px; padding-right: 70px; /*box-shadow: 0 0 1px rgba(0, 0, 0, 0.1);*/ max-width: 940px; } @media only screen and (max-width: 768px) { .wy-nav-content-wrap { margin-left: 0; padding-left: 0; padding-right: 0; } } .wy-nav-top { background: #1A1A1A; } .wy-plain-list-disc li, .rst-content .section ul li, .rst-content .toctree-wrapper ul li, article ul li { font: 400 13px/26px "Open Sans",sans-serif; color: #687E95; } .wy-nav-side { position: fixed; background: #1A1A1A; /*box-shadow: 0px 0 1px rgba(0, 0, 0, 0.1) inset; overflow: scroll; bottom: inherit;*/ } .wy-side-nav-search { font-family: "Open Sans", "Roboto", sans-serif; font-size: 15px; background-color: transparent; color: #7A7A7A; } .wy-menu-vertical ul { font-family: "Open Sans", "Roboto", sans-serif; margin-right: 1em; } .wy-menu-vertical > ul { padding-left: 20px; } .wy-menu-vertical li.current { background: transparent; } .wy-menu-vertical > ul > li { margin-bottom: 30px; } .wy-menu-vertical > ul > li > ul { display: block; } .wy-menu-vertical li.current > ul { background: transparent; padding: 0; } .wy-menu-vertical li.on a, .wy-menu-vertical li.current > a { background: transparent; border: 0; font-weight: 400; } .wy-menu-vertical li a { opacity: 1; } .wy-menu-vertical > ul > li > a, .wy-menu-vertical > ul > li > a:visited { color: #9A9A9A; opacity: 1; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -ms-transition: all 0.3s; -o-transition: all 0.3s; transition: all 0.3s; } .wy-menu-vertical > ul > li > a:hover { color: #FAC322; opacity: 1; } .wy-menu-vertical li a:hover { background: transparent; } .wy-menu-vertical li a, .wy-menu-vertical li.current > a { border-radius: 0 5px 5px 0; } .wy-menu-vertical li.current > ul > li > a { font-size: 13px; border-right: 0; opacity: 1; padding: 0.4045em 20px; } .wy-menu-vertical li.current > a { font-size: 13px; border-right: 0; } .wy-menu-vertical li.toctree-l2.current > a { background: none; font-size: 13px; color: #EF6155 !important; padding-left: 3.027em; } .wy-menu-vertical li.toctree-l1.current > ul > li > a::before { font-family: "FontAwesome"; content: ""; color: #EC7600; font-size: 8px; position: absolute; top: 4px; left: 3em; opacity: 0.3; display: none; } .wy-menu-vertical li.toctree-l1.current > ul > li.toctree-l2.current > a::before { content: "##"; color: #EF6155; font-size: 13px; position: absolute; top: 6px; left: 20px; opacity: 1; display: block; } .wy-menu-vertical li.toctree-l2.current ul { display: none !important; } .wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { background: none; padding: 0 4em; font-size: 12px; } .wy-menu-vertical li.toctree-l2.current li.toctree-l3.current ul { display: None; } .wy-menu-vertical a, .wy-menu-vertical a:visited { color: #7A7A7A; line-height: 20px; font-size: 13px; } .wy-menu-vertical a:hover { color: #5A5A5A; } .wy-menu-vertical > ul > li > ul > li > a, .wy-menu-vertical > ul > li > ul > li > a:visited, .wy-menu-vertical li.current a, .wy-menu-vertical li.current a:visited { color: #7A7A7A; background: transparent; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -ms-transition: all 0.3s; -o-transition: all 0.3s; transition: all 0.3s; } .wy-menu-vertical > ul > li > ul > li > a:hover, .wy-menu-vertical li.current a:hover { color: #EF6155; background: transparent; } .wy-menu-vertical li.current > a { color: #579ED1 !important; padding-left: 2.427em; } .wy-menu-vertical li.current > a:hover { } .wy-side-nav-search input[type="text"] { font-family: "Open Sans", "Roboto", sans-serif; border: 0; background: none; box-shadow: none; color: #7A7A7A; font-size: 13px; width: 90%; border-radius: 0; border-bottom: 1px solid rgba(129, 150, 154, 0.7); opacity: 1; padding: 6px 0; } .wy-side-nav-search > a, .wy-side-nav-search > a:visited { color: #9A9A9A; } .wy-side-nav-search > a:hover { background: none; } .wy-menu-vertical li { position: relative; } .wy-menu-vertical li span.toctree-expand::before, .wy-menu-vertical li.on a span.toctree-expand::before, .wy-menu-vertical li.current > a span.toctree-expand::before, .wy-menu-vertical li a span.toctree-expand, .wy-menu-vertical li a span.toctree-expand:before { display: none; } .wy-menu-vertical li.current ul li a span.toctree-expand, .wy-menu-vertical li.on a:hover span.toctree-expand, .wy-menu-vertical li.current a:hover span.toctree-expand{ display: none; } .wy-menu-vertical li.toctree-l1.current:before { content: "#"; color: #579ED1; font-size: 13px; position: absolute; top: 3px; left: 20px; z-index: 2; display: block; } .wy-menu-vertical li.toctree-l1:before { font-family: FontAwesome; content: ""; opacity: 1; color: #1999B3; font-size: 10px; top: 2px; position: absolute; left: 12px; z-index: 2; display: none; } .rst-content table.docutils, .rst-content table.field-list { border: medium none; width: 100%; } .rst-content table.docutils thead th, .rst-content table.field-list thead th { font-family: "Montserrat", serif; text-transform: uppercase; color: #7A7A7A; font-weight: 500; } .rst-content table.docutils thead th, .rst-content table.field-list thead th, .rst-content table.docutils tbody td, .rst-content table.field-list tbody td { border-top: 0; border-bottom: 1px solid rgba(230, 230, 230, 0.7); border-left: 0; border-right: 0; padding: 20px; white-space: pre-wrap; } .wy-table-odd td, .wy-table-striped tr:nth-child(2n-1) td, .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { background: none; } .wy-table td, .rst-content table.docutils td { font-size: 100% !important; } .rst-content table.docutils tbody td tt.code, .rst-content table.field-list tbody td tt.code, .rst-content table.docutils tbody td code.code, .rst-content table.field-list tbody td code.code { border: 0; background: none; padding: 0.3em 0; color: #26A6A6; } .rst-content .highlighted { border: 1px solid rgb(250, 195, 34); padding: 0 4px; border-radius: 3px; color: #BA8300; display: inline; font-weight: inherit; background: rgba(250, 195, 34, 0.6) none repeat scroll 0% 0%; } h1, h2, h3, h4, h5, h6 { color: #384E65; padding: 10px 0; font-family: "Open Sans", "Montserrat", serif; line-height: 1; margin: 40px 0 30px 0; font-weight: 300; position: relative; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; } .rst-content h1 .headerlink, .rst-content h2 .headerlink, .rst-content h3 .headerlink, .rst-content h4 .headerlink, .rst-content h5 .headerlink, .rst-content h6 .headerlink { visibility: hidden; color: inherit; font-size: inherit; margin: 0; } .rst-content h1 .headerlink:before, .rst-content h2 .headerlink:before, .rst-content h3 .headerlink:before, .rst-content h4 .headerlink:before, .rst-content h5 .headerlink:before, .rst-content h6 .headerlink:before { font-family: inherit; visibility: visible; position: absolute; left: -40px; width: 100%; content: "#"; opacity: 0; font-weight: inherit; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -ms-transition: all 0.3s; -o-transition: all 0.3s; transition: all 0.3s; } .rst-content h1 .headerlink:before { top: 20px; } .rst-content h1:hover .headerlink, .rst-content h2:hover .headerlink, .rst-content h3:hover .headerlink, .rst-content h4:hover .headerlink, .rst-content h5:hover .headerlink, .rst-content h6:hover .headerlink { display: inline; color: inherit; } .rst-content h1:hover .headerlink:before, .rst-content h2:hover .headerlink:before, .rst-content h3:hover .headerlink:before, .rst-content h4:hover .headerlink:before, .rst-content h5:hover .headerlink:before, .rst-content h6:hover .headerlink:before { opacity: 1; } .rst-content h1 .headerlink:after, .rst-content h2 .headerlink:after, .rst-content h3 .headerlink:after, .rst-content h4 .headerlink:after, .rst-content h5 .headerlink:after, .rst-content h6 .headerlink:after { visibility: hidden; content: ""; } h1 { font-size: 44px; } h2 { font-size: 38px; color: #579ED1; position: relative; } h2:before { display: none; font-family: "FontAwesome"; content: "#"; margin-left: -30px; font-size: 38px; top: 11px; color: #579ED1 !important; opacity: 0.6; position: absolute; } h3 { font-size: 24px; } h4 { font-size: 20px; } h1, h1 a, .header h1, .header h1 a { font: 300 44px/66px "Open Sans", "Roboto","Montserrat",sans-serif; margin-bottom: 30px; color: #485E75; } a, a:visited { color: #009CD4; } a:hover { color: #48B0F7; } a > em { font-style: normal; } p { font: 400 13px/26px "Open Sans",sans-serif; color: #687E95; } b, strong { font-weight: 600; } pre, pre code, .rst-content tt, code.docutils, .rst-content dl:not(.docutils) code { font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; color: #26A6A6; background-color: transparent; font-size: 13px; font-weight: 400; border: 0; padding: 0; } pre, code, .rst-content ttc { font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; font-weight: 400; border-radius: 3px; font-size: 13px; margin: 0px 2px; } div[class^="highlight"] { border: 0; background: transparent; } .higlight { background: transparent; } .highlight > pre, div[class^="highlight"] pre { font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; border-left: 1px solid #F2F2F2; font-size: 13px; padding: 2% 4%; margin-left: 1%; color: #788EA5; /*background-color: #FAFAFA;*/ } code, .rst-content tt, .rst-content code { font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; } code { color: #CB6077; background-color: transparent; font-size: 13px; font-weight: 500; } p > code, .rst-content tt, p > code.docutils { color: #26A6A6; padding: 0; display: inline; font-weight: 400; border: 0; background-color: transparent; } .rst-content tt.literal, .rst-content tt.literal, .rst-content code.literal, .rst-content dl:not(.docutils) code { color: #1976BF; } .rst-content .admonition-title { display: none; } .rst-content .note, .rst-content .warning, .rst-content .tip, .rst-content .caution, .rst-content .versionchanged, .rst-content .versionadded { padding: 4% 4%; margin: 30px 0; background: transparent; } .rst-content .note .highlight > pre, .rst-content .note div[class^="highlight"] pre, .rst-content .warning .highlight > pre, .rst-content .warning div[class^="highlight"] pre, .rst-content .versionadded .highlight > pre, .rst-content .versionadded div[class^="highlight"] pre, .rst-content .tip .highlight > pre, .rst-content .tip div[class^="highlight"] pre, .rst-content .caution .highlight > pre, .rst-content .caution div[class^="highlight"] pre { margin-left: 0; border-left: 0; } .rst-content .note { border-left: 1px dotted rgba(50, 154, 188, 0.3); border-right: 1px dotted rgba(50, 154, 188, 0.3); background: rgba(50, 154, 188, 0.02); color: #3399BB !important; box-shadow: 0 0 2px rgba(50, 154, 188, 0.1); } .rst-content .tip { border-left: 1px dotted rgba(73,182,127, 0.3); border-right: 1px dotted rgba(73,182,127, 0.3); background: rgba(73,182,127, 0.02); color: #49B67F !important; box-shadow: 0 0 2px rgba(73,182,127, 0.1); } .rst-content .tip > p { color: #49B67F !important; } .rst-content .tip > p code { color: #19864F !important; } .rst-content .warning, .rst-content .caution { border-left: 1px dotted rgba(249, 145, 87, 0.3); border-right: 1px dotted rgba(249, 145, 87, 0.3); background: rgba(249, 145, 87, 0.02); color: #F99157 !important; box-shadow: 0 0 2px rgba(249, 145, 87, 0.1); } .rst-content .note > p { color: #39B !important; } .rst-content .warning > p, .rst-content .caution > p { color: #F99157 !important; } .rst-content .versionadded, .rst-content .versionchanged { border-left: 1px dotted rgba(102, 119, 187, 0.3); border-right: 1px dotted rgba(102, 119, 187, 0.3); background: rgba(102, 119, 187, 0.02); color: #6677BB !important; margin-bottom: 20px; position: relative; box-shadow: 0 0 2px rgba(102, 119, 187, 0.1); } .rst-content .versionadded > p > span, .rst-content .versionchanged > p > span { font-family: "Montserrat", serif; display: block; font-size: 12px; margin-bottom: 20px; text-transform: uppercase; color: #36479B !important; } .rst-content .versionadded > p, .rst-content .versionchanged > p { color: #6677BB !important; } .rst-content .versionadded > p code, .rst-content .versionchanged > p code { color: #36479B !important; } .rst-content .versionadded > p, .rst-content .versionchanged > p { margin-bottom: 0; } .rst-content .note .versionchanged { padding-left: 4%; } .note code { border: 0; padding: 0; display: inline; } .note ul li, .warning ul li { color: inherit !important; } .warning code, .warning tt { border: 0; padding: 0; display: inline; color: #D9664F !important; } .btn-neutral, .btn-neutral:visited { color: #808080 !important; font: 400 12px/20px Montserrat,Arial,sans-serif; text-transform: uppercase; padding: 6px 14px; border-radius: 4px; box-shadow: none; border: 0; transition: all 0.3s; background-color: transparent !important; } .btn-neutral:hover { background-color: transparent !important; color: rgba(25, 153, 179, 0.8) !important; /*color: rgba(0, 168, 192, 1) !important;*/ } .btn-neutral:focus { background-color: transparent !important; color: rgba(25, 153, 179, 1) !important; box-shadow: none; padding: 6px 14px; } .btn-neutral span.fa { transition: all 0.3s; } .btn-neutral:hover span.fa-arrow-circle-left { padding-right: 5px; } .btn-neutral:hover span.fa-arrow-circle-right { padding-left: 5px; } .btn-neutral span.fa-arrow-circle-left:before { content: '' } .btn-neutral span.fa-arrow-circle-right:before { content: '' } /* Autodoc */ .rst-content table.field-list .field-name { text-align: left; white-space: nowrap; padding-right: 10px; width: 140px; font-weight: 600; } .rst-content table.field-list .field-body { padding: 8px 16px; text-align: left; vertical-align: top; border: 0; } .rst-content table.field-list .field-body ul { line-height: 0; } .rst-content table.field-list .field-body ul li { list-style: none; margin-left: 0; font-size: 100%; } .rst-content table.field-list td p { line-height: inherit; } .rst-content table.field-list .field-body strong { color: #F3888B; font-weight: 400; margin-top: 0; } .rst-content dl:not(.docutils).class > dt { border: 0; background: transparent; font-size: inherit; margin-bottom: 24px; color: #579ED1; font-weight: 600; } .rst-content dl:not(.docutils).class > dt > em, .rst-content dl:not(.docutils).class > dt > em.property { font-weight: 400; font-style: normal; } .rst-content dl:not(.docutils).class > dt > code { color: #F9A167; font-weight: 400; } .rst-content dl:not(.docutils) dl.classmethod > dt, .rst-content dl:not(.docutils) dl.method > dt { border-left: 1px solid #F2F2F2; background: transparent; font-size: inherit; } .rst-content dl:not(.docutils) dl.classmethod > dt > code, .rst-content dl:not(.docutils) dl.method > dt > code { color: #D9664F; font-weight: 400; } .rst-content dl:not(.docutils) dl.classmethod > dt > em, .rst-content dl:not(.docutils) dl.method > dt > em { font-weight: 400; } .rst-content dl:not(.docutils) dl.classmethod > dt > em.property, .rst-content dl:not(.docutils) dl.method > dt > em.property { font-weight: 400; color: #7AC; } /* Solarized Light For use with Jekyll and Pygments http://ethanschoonover.com/solarized SOLARIZED HEX ROLE --------- -------- ------------------------------------------ base01 #586e75 body text / default code / primary content base1 #93a1a1 comments / secondary content base3 #fdf6e3 background orange #cb4b16 constants red #dc322f regex, special keywords blue #268bd2 reserved keywords cyan #2aa198 strings, numbers green #859900 operators, other keywords */ .highlight { background-color: #fdf6e3; color: #586e75 } .highlight .c { color: #93a1a1 } /* Comment */ .highlight .err { color: #586e75 } /* Error */ .highlight .g { color: #586e75 } /* Generic */ .highlight .k { color: #859900 } /* Keyword */ .highlight .l { color: #586e75 } /* Literal */ .highlight .n { color: #586e75 } /* Name */ .highlight .o { color: #859900 } /* Operator */ .highlight .x { color: #cb4b16 } /* Other */ .highlight .p { color: #586e75 } /* Punctuation */ .highlight .cm { color: #93a1a1 } /* Comment.Multiline */ .highlight .cp { color: #859900 } /* Comment.Preproc */ .highlight .c1 { color: #93a1a1 } /* Comment.Single */ .highlight .cs { color: #859900 } /* Comment.Special */ .highlight .gd { color: #2aa198 } /* Generic.Deleted */ .highlight .ge { color: #586e75; font-style: italic } /* Generic.Emph */ .highlight .gr { color: #dc322f } /* Generic.Error */ .highlight .gh { color: #cb4b16 } /* Generic.Heading */ .highlight .gi { color: #859900 } /* Generic.Inserted */ .highlight .go { color: #586e75 } /* Generic.Output */ .highlight .gp { color: #586e75 } /* Generic.Prompt */ .highlight .gs { color: #586e75; font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #cb4b16 } /* Generic.Subheading */ .highlight .gt { color: #586e75 } /* Generic.Traceback */ .highlight .kc { color: #cb4b16 } /* Keyword.Constant */ .highlight .kd { color: #268bd2 } /* Keyword.Declaration */ .highlight .kn { color: #859900 } /* Keyword.Namespace */ .highlight .kp { color: #859900 } /* Keyword.Pseudo */ .highlight .kr { color: #268bd2 } /* Keyword.Reserved */ .highlight .kt { color: #dc322f } /* Keyword.Type */ .highlight .ld { color: #586e75 } /* Literal.Date */ .highlight .m { color: #2aa198 } /* Literal.Number */ .highlight .s { color: #2aa198 } /* Literal.String */ .highlight .na { color: #586e75 } /* Name.Attribute */ .highlight .nb { color: #B58900 } /* Name.Builtin */ .highlight .nc { color: #268bd2 } /* Name.Class */ .highlight .no { color: #cb4b16 } /* Name.Constant */ /*.highlight .nd { color: #268bd2 }*/ /* Name.Decorator */ .highlight .nd { color: #cb4b16 } .highlight .ni { color: #cb4b16 } /* Name.Entity */ .highlight .ne { color: #cb4b16 } /* Name.Exception */ .highlight .nf { color: #268bd2 } /* Name.Function */ .highlight .nl { color: #586e75 } /* Name.Label */ .highlight .nn { color: #586e75 } /* Name.Namespace */ .highlight .nx { color: #586e75 } /* Name.Other */ .highlight .py { color: #586e75 } /* Name.Property */ .highlight .nt { color: #268bd2 } /* Name.Tag */ .highlight .nv { color: #268bd2 } /* Name.Variable */ .highlight .ow { color: #859900 } /* Operator.Word */ .highlight .w { color: #586e75 } /* Text.Whitespace */ .highlight .mf { color: #ff9800 } /* Literal.Number.Float */ .highlight .mh { color: #ff9800 } /* Literal.Number.Hex */ .highlight .mi { color: #ff9800 } /* Literal.Number.Integer */ .highlight .mo { color: #ff9800 } /* Literal.Number.Oct */ .highlight .sb { color: #93a1a1 } /* Literal.String.Backtick */ .highlight .sc { color: #2aa198 } /* Literal.String.Char */ .highlight .sd { color: #586e75 } /* Literal.String.Doc */ .highlight .s2 { color: #2aa198 } /* Literal.String.Double */ .highlight .se { color: #cb4b16 } /* Literal.String.Escape */ .highlight .sh { color: #586e75 } /* Literal.String.Heredoc */ .highlight .si { color: #2aa198 } /* Literal.String.Interpol */ .highlight .sx { color: #2aa198 } /* Literal.String.Other */ .highlight .sr { color: #dc322f } /* Literal.String.Regex */ .highlight .s1 { color: #2aa198 } /* Literal.String.Single */ .highlight .ss { color: #2aa198 } /* Literal.String.Symbol */ .highlight .bp { color: #F2777A } /* Name.Builtin.Pseudo */ .highlight .vc { color: #F2777A } /* Name.Variable.Class */ .highlight .vg { color: #F2777A } /* Name.Variable.Global */ .highlight .vi { color: #F2777A } /* Name.Variable.Instance */ .highlight .il { color: #2aa198 } /* Literal.Number.Integer.Long */ /***** Mocha *****/ .highlight .s { color: #7BBDA4 } /* Literal.String */ .highlight .s1 { color: #7BBDA4 } /* Literal.String */ .highlight .si { color: #A89BB9 } /* Literal.String */ .highlight .m { color: #F4BC87 } /* Literal.Number */ .highlight .mf { color: #F4BC87 } /* Literal.Number */ .highlight .mh { color: #F4BC87 } /* Literal.Number */ .highlight .mi { color: #F4BC87 } /* Literal.Number */ .highlight .mo { color: #F4BC87 } /* Literal.Number */ .highlight .k { color: #BEB55B; font-weight: normal; } /* Keyword */ .highlight .kn { color: #BEB55B; font-weight: normal; } /* Keyword */ .highlight .ow { color: #BEB55B; font-weight: normal } /* Operator.Word */ .highlight .nc { color: #D28B71; font-weight: 500 } /* Name.Class */ .highlight .nf { color: #8AB3B5; font-weight: normal; } /* Name.Function */ .highlight .bp { color: #F3888B } /* Name.Builtin.Pseudo */ .highlight .vc { color: #F3888B } /* Name.Variable.Class */ .highlight .vg { color: #F3888B } /* Name.Variable.Global */ .highlight .vi { color: #F3888B } /* Name.Variable.Instance */ .highlight .nd { color: #BB9584 } /* Name.Decorator */ .highlight .ne { color: #CB6077; font-weight: 400 } /* Name.Exception */ .highlight .o { color: #8A8A8A } /* Operator */ .highlight .n { color: #8A8A8A } .highlight .c { color: #B8AFAD; font-style: normal; } .highlight .sd { color: #B8AFAD; font-style: normal; } .highlight .nl { color: #A89BB9 } /* Name.Label */ .highlight .nn { color: #A89BB9 } /* Name.Namespace */ .highlight .nx { color: #A89BB9 } /* Name.Other */ .highlight .py { color: #A89BB9 } /* Name.Property */ /* Inline code */ tt.code, code.code { border: 1px solid #F2F2F2; font-weight: normal; display: inline; padding: 0.3em 0.5em; background-color: #FAFAFA; } tt.code .name, code.code .name { color: #8A8A8A } tt.code .operator, code.code .operator { color: #8A8A8A } tt.code .punctuation, code.code .punctuation { color: #8A8A8A } tt.code .string, code.code .string { color: #7BBDA4 } tt.code .number, code.code .number { color: #F4BC87 } tt.code .integer, code.code .integer { color: #F4BC87 } /***** Eighties *****/ .highlight .s { color: #59B6CF } /* Literal.String */ .highlight .s1 { color: #59B6CF } /* Literal.String */ .highlight .si { color: #8986BF } /* Literal.String */ .highlight .m { color: #F9768F } /* Literal.Number */ .highlight .mf { color: #F9768F } /* Literal.Number */ .highlight .mh { color: #F9768F } /* Literal.Number */ .highlight .mi { color: #F9768F } /* Literal.Number */ .highlight .mo { color: #F9768F } /* Literal.Number */ .highlight .k { color: #579ED1; font-weight: normal; } /* Keyword */ .highlight .kn { color: #579ED1; font-weight: normal; } /* Keyword */ .highlight .ow { color: #579ED1; font-weight: normal } /* Operator.Word */ .highlight .nc { color: #69B69F; font-weight: 400 } /* Name.Class */ .highlight .nf { color: #8986BF; font-weight: normal; } /* Name.Function */ .highlight .bp { color: #69B69F } /* Name.Builtin.Pseudo */ .highlight .vc { color: #69B69F } /* Name.Variable.Class */ .highlight .vg { color: #69B69F } /* Name.Variable.Global */ .highlight .vi { color: #69B69F } /* Name.Variable.Instance */ .highlight .nd { color: #879ED1 } /* Name.Decorator */ .highlight .ne { color: #CB6077; font-weight: 400 } /* Name.Exception */ .highlight .o { color: #98AEC5 } /* Operator */ .highlight .n { color: #788EA5 } .highlight .c { color: #A8AFC2; font-style: normal; } .highlight .sd { color: #A8AFC2; font-style: normal; } .highlight .nl { color: #F9768F } /* Name.Label */ .highlight .nn { color: #8986BF } /* Name.Namespace */ .highlight .nx { color: #F9768F } /* Name.Other */ .highlight .py { color: #F9768F } /* Name.Property */ .highlight .nb { color: #63ABA3 } /* Name.Builtin */ .highlight .p { color: #98AEC5 } /* Punctuation */ /* Inline code */ tt.code, code.code { border: 1px solid #F2F2F2; font-weight: normal; display: inline; padding: 0.3em 0.5em; background-color: #FAFAFA; } tt.code .name, code.code .name { color: #788EA5 } tt.code .operator, code.code .operator { color: #98AEC5 } tt.code .punctuation, code.code .punctuation { color: #98AEC5 } tt.code .string, code.code .string { color: #59B6CF } tt.code .number, code.code .number { color: #F9768F } tt.code .integer, code.code .integer { color: #F9768F } tt.code .pseudo, code.code .pseudo { color: #69B69F } /* API */ .rst-content dl:not(.docutils) .property { font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; padding-right: 5px; } .rst-content dl:not(.docutils) em { font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; font-style: normal; color: #788EA5; } .rst-content dl.class:not(.docutils) > dt > code { font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; color: #788EA5; } .rst-content dl:not(.docutils) > dt > tt.descname, .rst-content dl:not(.docutils) > dt > tt.descname, .rst-content dl:not(.docutils) > dt > code.descname { color: #69B69F; } .rst-content dl.class:not(.docutils) > dt > .sig-paren, .rst-content dl:not(.docutils) dl.classmethod > dt > .sig-paren, .rst-content dl:not(.docutils) dl.method > dt > .sig-paren { font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; color: #98AEC5; font-weight: normal; font-style: normal; } .rst-content dl:not(.docutils) dl.classmethod > dt > code, .rst-content dl:not(.docutils) dl.method > dt > code { color: #8986BF; font-weight: 400; } .rst-content table.field-list .field-body strong { color: #F9768F; font-weight: 600; } .o { font-weight: normal; } /* Dark Orator */ div[class^="highlight"] { border: 0; background: #152B39; border-radius: 3px; } div[class^="highlight"] > pre { border: 0; color: #98AEC5; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; padding: 4% 4%; } .highlight .nn, .highlight .nc { color: #89C6BF; } .highlight .c, .highlight .c1 { color: #727995; font-style: normal; } .highlight .sd { color: #727995; font-style: normal; } .highlight .p, .highlight .o, code.code .operator { color: #687E95 } .highlight .n, code.code .name { color: #98AEC5 } .highlight .l { color: #98AEC5; } .note .last pre, .warning .last pre, .tip .last pre, .caution .last pre, .versionchanged .last pre, .versionadded .last pre { padding-bottom: 4%; margin-bottom: 0; } cleo-2.1.0/docs/api/000077500000000000000000000000001451777547300141615ustar00rootroot00000000000000cleo-2.1.0/docs/api/index.rst000066400000000000000000000117331451777547300160270ustar00rootroot00000000000000API Reference ############# Command ======= .. py:class:: Command(name=None) A ``Command`` represents a single CLI command. .. py:method:: argument(key=None) Get the value of a command argument. :param key: The argument name :type key: str :rtype: mixed .. py:method:: ask(question, default=None) Prompt the user for input. :param question: The question to ask :type question: str :param default: The default value :type default: str or None :rtype: str .. py:method:: call(name, options=None) Call another command. :param name: The command name :type name: str :param options: The options :type options: list or None .. py:method:: call_silent(name, options=None) Call another command silently. :param name: The command name :type name: str :param options: The options :type options: list or None .. py:method:: choice(question, choices, default=None, attempts=None, multiple=False) Give the user a single choice from an list of answers. :param question: The question to ask :type question: str :param choices: The available choices :type choices: list :param default: The default value :type default: str or None :param attempts: The max number of attempts :type attempts: int :param multiple: Multiselect :type multiple: int :rtype: str .. py:method:: comment(text) Write a string as comment output. :param text: The line to write :type text: str .. py:method:: confirm(self, question, default=False, true_answer_regex='(?i)^y') Confirm a question with the user. :param question: The question to ask :type question: str :param default: The default value :type default: bool :param true_answer_regex: A regex to match the "yes" answer :type true_answer_regex: str :rtype: bool .. py:method:: error(text) Write a string as error output. :param text: The line to write :type text: str .. py:method:: info(text) Write a string as information output. :param text: The line to write :type text: str .. py:method:: line(text, style=None, verbosity=None) Write a string as information output. :param text: The line to write :type text: str :param style: The style of the string :type style: str :param verbosity: The verbosity :type verbosity: None or int str .. py:method:: list(elements) Write a list of elements. :param elements: The elements to write a list for :type elements: list .. py:method:: option(key=None) Get the value of a command option. :param key: The option name :type key: str :rtype: mixed .. py:method:: progress_bar(max=0) Create a new progress bar :param max: The maximum number of steps :type max: int :rtype: ProgressBar .. py:method:: question(text) Write a string as question output. :param text: The line to write :type text: str .. py:method:: render_table(headers, rows, style='default') Format input to textual table.. :param headers: The table headers :type headers: list :param rows: The table rows :type rows: list :param style: The table style :type style: str .. py:method:: secret(question) Prompt the user for input but hide the answer from the console. :param question: The question to ask :type question: str :rtype: str .. py:method:: set_style(name, fg=None, bg=None, options=None) Set a new style :param name: The name of the style :type name: str :param fg: The foreground color :type fg: str :param bg: The background color :type bg: str :param options: The options :type options: list .. py:method:: table(headers=None, rows=None, style='default') Return a ``Table`` instance. :param headers: The table headers :type headers: list :param rows: The table rows :type rows: list :param style: The table style :type style: str .. py:method:: table_cell(value, **options) Return a ``TableCell`` instance :param value: The cell value :type value: str :param options: The cell options :type options: dict .. py:method:: table_separator() Return a ``TableSeparator`` instance :rtype: TableSeparator .. py:method:: table_style() Return a ``TableStyle`` instance :rtype: TableStyle .. py:method:: warning(text) Write a string as warning output. :param text: The line to write :type text: str cleo-2.1.0/docs/conf.py000066400000000000000000000206141451777547300147120ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html from __future__ import annotations import datetime import os import sys sys.path.insert(0, os.path.abspath("../src")) import cleo # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "Cleo" author = "Sébastien Eustace" this_year = datetime.datetime.now().year copyright = f"{this_year}, {author}" release = cleo.__version__ version_major, version_minor, _ = release.split(".", 2) version = f"{version_major}.{version_minor}" # -- General configuration ------------------------------------------------ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ["sphinx.ext.autodoc"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "Cleodoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ("index", "Cleo.tex", "Cleo Documentation", "Sébastien Eustace", "manual") ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [("index", "cleo", "Cleo Documentation", ["Sébastien Eustace"], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "Cleo", "Cleo Documentation", "Sébastien Eustace", "Cleo", "One line description of project.", "Miscellaneous", ) ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False on_rtd = os.environ.get("READTHEDOCS", None) == "True" if not on_rtd: html_theme = "sphinx_rtd_theme" def setup(app): app.add_css_file("theme_overrides.css") else: html_context = { "css_files": [ "https://media.readthedocs.org/css/sphinx_rtd_theme.css", "https://media.readthedocs.org/css/readthedocs-doc-embed.css", "_static/theme_overrides.css", ] } cleo-2.1.0/docs/helpers/000077500000000000000000000000001451777547300150525ustar00rootroot00000000000000cleo-2.1.0/docs/helpers/index.rst000066400000000000000000000003131451777547300167100ustar00rootroot00000000000000Helpers ------- Cleo also contains a set of "helpers" - different small tools capable of helping you with different tasks: .. toctree:: :maxdepth: 1 question_helper progress_bar table cleo-2.1.0/docs/helpers/progress_2.gif000066400000000000000000000420211451777547300176250ustar00rootroot00000000000000GIF89a@!ғḽ¿Ȓ鈚ﷺͷѱղȮ˲筲Րүవի竭ׂ鞩𣍟ؔv! NETSCAPE2.0! XMP DataXMP ~}|{zyxwvutsrqponmlkjihgfedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$#"!  !,@!~ƕοɜpӌԥ%̪ߋچ!!3 ~! ;QD)|8(Cȃ4HТ%E F.Nz%9!\(Ic ~`Hã  ` QSR5jS)⇀_rD"̹J/hHhР Frbƞ>0IiX 4~DXcrM(5 rMe•Eg94eAWCvm˔5\4בxoڣ1A%jLRJ*X`À'0` ?(<0< E OD64s(0 pN# " F%e (Z g~'{. PDj nXPY P#h@F 4= 0* $a@)_)h쀥t`YXk|i'8QVh ė[ҡj9ԙXlĤ)$G kf*H0ciBX : t`'ZXi(hB*)lhpkl A "U( (U0Du8`r 0V H@i>vTB$A 9TB (BZQPa6B'Ð8L ]'h#5 +>(@B# H샦WJ@%l#jz 2K< Ї  (MlP4kxp%)͆0a#Ba;, 5F :TP $ُ_sD3,KF_csn!D؅&b5fH_ gVr$ӃQ.TNSk3s oNI!5<5bsBAHÈM1 D2A|x9(6pG?.BqQ&<h# 1`x(#$R)S' 9qJliq@ *64~P(#/1t,aDGGP@v &ЁDDx(  (㌅$SCs r\5pGZGtDJQŌAE 4@h啃(1=x"!AhC=\@ G4cX*Kg5$H 5Ae29F ,v§z\2$ R-֘*X>I뭸6!,? D.01^m-'!,!,!,$~~~~%3I!..̓՘K3L3QĂIHbF ֜ǑH!b)4 0f# ϙi#F8!bD%HxX@;`ļG)PN`1 * E~\H ?Ib)+IBF + D2V AFX"NH ?lݻ'*4Af#AB*mP>80:Aʧ/bؼۑ42 فȀ9i(6h#o.B( B,0y/ބQE#mb򁄋d"J~h~pQ!xPQO| 4 R*ֵa-("%*q؂ qRu=S~`APA1 T` QIdB`ewҁ % r0'($J,41|'tTO@#W A! $w(}8'Pf)M8>X(HWP5 R(0bώ*iʕ ~~5QQ!M 4 nDl)ؕ"3#ٸ[SD>p@+ I !, " /A~ƪČ%*Ǒ+R+фD١MCS!ڃɒW܅I݅PlF"* "#&#B1#$FBp1dD, ͓?%dlI"!+ ɓgvD JGtE!կ,CQ#`F !,!,!,$~~&!$γL~AI;/ަЎM .5$)xID4͍?H"JgĈ?J1":t(ǕI.܉H;uv8%ʾ?W0SCX ҰȓQhcCƊ4Ad?N "1$4 `U<4)T}tb) w5Au82%`T񡃟ʕat A%d'E2$q0*rrR@ζAnRaz :b 51{#^FAJ,{Vn pD@{SDBQAF|U[  tq@ -p@ lA@C\(!Cd0@h-b*p ,1H6!8BZ@A ɩ$ -.Qb*TBH&DY DҔ 'h*N C444M!Fb8pǓ0ǎ.J,4H.p ( cgFz+ M̹對tbdEU.(gQkO0 @@ ao@4O(bo]!-bB!Ȼ-g{BF_ /dpjphDop \ 00 0x 0Lʌf3$8ᇘjuP@ T, !O!4p'ֆԷ Ho]fxi]׆ ĽD -YvwP ,0*SmٲhDqQbD!d*cX0X!"\cʯ/ ޴cHpkn0#q%<9 0 B Z.Kŵ (#O7SEa!??4L33Q=0 (C ]"jc@#DkL rh( 8>H80 aCboHL1&:xH*fV̢bq` (2!,!,!,!,#~~̐ē%I(ݑ!!؉$T/L/3/1rf wJQ>FW0PH`&̉H H1 ̟,gLDF91Bč#n "lX! !E`?PFH)* @Eh)U4Ƌ&CQ "+ +!DJ`"\ c~r#cDw4Cq@CeD4f, aN#8 c ? …'x23B 0 gC"x *xԥC.:b`1ő,x[q D!2!@ET0iI1\eFhnUR@"-a h@@-(!$q@JAY=Xk/hv 4&#qN '.jyđW2xGEЀ/H(gPQ5$"J8Ϭ#9l!d"L>фW%Q)13U z!z;6c,d )&kO !,!,!,$~~.o L!ȆȽی3L(p~b=`~N5~AZb kP@Oi@CF3 GpJ@?XgВdP bOh0Ōy'xHʲ)+ha &LHJ$tfppW'=^`4&A_xu扏 fܩVc@ٿhc &T@9y1$PZ ?|b=Hǟ8hÂő#Ycfc14U68ǃ$E 5Gn"șg@rB,.>liB "C W J$ = A"g2 IX0 el%A XB<<lG# `l1%`Ay\cSp0O5z'pDAl`敢dTc  b|! vQ R͉>!d9) 9 D `y(4J !`h3C= *ڔ#2>e k"뮵( k,1$!,~~p%%!#7;LˇD+YؾІ….oy3!,^K&\$JCAuN; #B !,!,!, ~~~ǑTTIQ(ِ.ڶpITIL~LQ gFSt Kh$L=F)h7A qDXH#Lpr Xѡ(GKvRÉ2O,0 % C 9Ru`Y?lX!ʼn2jW!a D$ Aǀ/KrJ  a3^ 2|a 06Q@JpQϟ Pa( $4LY#OZ0 Q^ĠF# @4i@t\C a" Aq9pxR$7AU)7G5XXށ@ p@ JtAq qŃ\p@ _K! 1 P%)@EqF`( %ܴSMEPd{QS$_$@b88;X@40C!囂pAcو@I3p2 h lOB K|HG {ni  R#O|Q Y!~lP=U5FY! G1WF*H0+@ ,k""%ǚ"dzɲjmVЊ"D : 'tø,A ~PU~\y :0H<2CFV 2@sp)h@Gr I?(Yw"736-r E;L@(]J@JЉ sŃP7 tlqխ&u[qJF hp Gx@hpD:`eQ$ 4%4LhW r9aadBrh.xʄXp;`d)B5 ƕP{cJ6b{u{ctLi%Y%t 4{ctBJ& ۫%=^B$ ?a CqTehj &IxBH $" `B-`vĊ"7d!G$=8C <""Bk\pE(2Ԡ#P  D *B!!(sD ї%$ja,u3M<0{`"q@diB@Nvnz38IN_L:OOs !.B஡f#uᆈ#8b 7FѡcKVڜ&T,(:.4`{H(&hcc "HiB`8%Bd;"+E|UЀA A&il@t1ʷo+ n@7?( @%HX'M|QъEvP6ˀ)9d! -IG?0|hF# Lje 5Y+Z\v(T4dJoPezYC ALxߨ5-(ApЁ * !!@ZKDX1g_tApEE$_DRBHP $u0 '$KR.0J;>\8 h/#Aa(v> CB@BKO wgDшl&?G@eXG M"fs W@NUj1j@l"kSn< ,%<*̱\VKg !, ( o!!!#6+ ?0p),A'9U>~W(ƃ~&ɒbV!~.Ӌꂁ!,2! H*\Ȱ@!,2!~ľǂчʣא!!Ћ~.mKS}jHAQ*D)8P@jv,DA?+iL\ڦSfA,sI('QA<)є0*ˆltH3j6 W AuA-Y6Ac6E\*An:[C:eΛ@1)xx'4 EQFNP(P SD$4C;a Q!B4,` \P/!,8b@xn#0^@Ȃtanj``Mv " , " 0& ̠` B@j0M>+ `/, BA(H!BÏ#? 1tT.hJ ,(0"hGʍv!ACs Zh075`"]G@:P@,Ѕ"ƆGJKpaq|IG^EDPhpBpPp@ hlj%df*AAxX98PAT ̀}$t3$zErA0P>I&r&t`j%j&zQyO$  tuE~ AG@ 9aC0*Go &,-F#;4%@ G 2|l!D ump/ D22*e`l+@ =LFX,\8&GW D B׫ANb̖' @x F(өHA 2`8j 0!z,1 VHy!"I0 =$@da&3Y/f! pJf$RR\A rx f@ @h]U!|P&P?Ot pD_Ǡp2ӝI詔R!'!.tЅ |p8x TBOb(MP)!Ǩ$a F;9Ia NjeDZ|UC\dz#@ȳ`!Z {L{H rKH|@춋ȳ%Ht^b˭K'R!0p쨸Щ;FQA )]bN)h`Z2H4aT㚆Lp JqCӃ\ - VO]J`m!TRm(&-dRd~,tI+:->K3zMP֦} -=U+C(J71nB9$ =A 0B C] "#]h0]tS'&H B`{c*NYsfeIWC^D4 0P+ jAQ_ B$$*XQ=,R0b"5;d(qȀ[.'`"RE)% 8x s$$ J!p1Ob(D X$@?hp T; AI) *ńHB!G9lߜ%$D&Ep"D$=bɐ2Xd(> IS*;RR(*l&BJ"El ?.~ FB:OɒU$niȚXNxDǸN`"W!FRWpJII(e: SͨF7юz if(MUҖ/ԨI_JӚ{MwS!,2! H*\Ȱ@;cleo-2.1.0/docs/helpers/progress_bar.rst000066400000000000000000000151311451777547300202750ustar00rootroot00000000000000Progress Bar ############ When executing longer-running commands, it may be helpful to show progress information, which updates as your command runs: .. image:: progress_2.gif To display progress details, use the ``progress_bar()`` method (which returns a ``ProgressBar`` instance), pass it a total number of units, and advance the progress as the command executes: .. code-block:: python def handle(self): # Create a new progress bar (50 units) progress = self.progress_bar(50) # Start and displays the progress bar for _ in range(50): # ... do some work # Advance the progress bar 1 unit progress.advance() # You can also advance the progress bar by more than 1 unit # progress.advance(3) # Ensure that the progress bar is at 100% progress.finish() Instead of advancing the bar by a number of steps (with the ``advance()`` method), you can also set the current progress by calling the ``set_progress()`` method. .. tip:: If your platform doesn't support ANSI codes, updates to the progress bar are added as new lines. To prevent the output from being flooded, adjust the ``set_redraw_frequency()`` accordingly. By default, when using a ``max``, the redraw frequency is set to *10%* of your ``max``. If you don't know the number of steps in advance, just omit the steps argument when using the ``progress_bar`` method: .. code-block:: python progress = self.progress_bar() The progress will then be displayed as a throbber: .. code-block:: text # no max steps (displays it like a throbber) 0 [>---------------------------] 5 [----->----------------------] 5 [============================] # max steps defined 0/3 [>---------------------------] 0% 1/3 [=========>------------------] 33% 3/3 [============================] 100% Whenever your task is finished, don't forget to call ``finish()`` to ensure that the progress bar display is refreshed with a 100% completion. .. note:: If you want to output something while the progress bar is running, call ``clear()`` first. After you're done, call ``display()`` to show the progress bar again. Customizing the Progress Bar ============================ Built-in Formats ---------------- By default, the information rendered on a progress bar depends on the current level of verbosity of the ``IO`` instance: .. code-block:: text # Verbosity.NORMAL (CLI with no verbosity flag) 0/3 [>---------------------------] 0% 1/3 [=========>------------------] 33% 3/3 [============================] 100% # Verbosity.VERBOSE (-v) 0/3 [>---------------------------] 0% 1 sec 1/3 [=========>------------------] 33% 1 sec 3/3 [============================] 100% 1 sec # Verbosity.VERY_VERBOSE (-vv) 0/3 [>---------------------------] 0% 1 sec 1/3 [=========>------------------] 33% 1 sec 3/3 [============================] 100% 1 sec # Verbosity.DEBUG (-vvv) 0/3 [>---------------------------] 0% 1 sec/1 sec 1.0 MB 1/3 [=========>------------------] 33% 1 sec/1 sec 1.0 MB 3/3 [============================] 100% 1 sec/1 sec 1.0 MB .. note:: If you call a command with the quiet flag (``-q``), the progress bar won't be displayed. Instead of relying on the verbosity mode of the current command, you can also force a format via ``set_format()``: .. code-block:: python progress.set_format('verbose') The built-in formats are the following: * ``normal`` * ``verbose`` * ``very_verbose`` * ``debug`` If you don't set the number of steps for your progress bar, use the ``_nomax`` variants: * ``normal_nomax`` * ``verbose_nomax`` * ``very_verbose_nomax`` * ``debug_nomax`` Custom Formats -------------- Instead of using the built-in formats, you can also set your own: .. code-block:: python progress.set_format('%bar%') This sets the format to only display the progress bar itself: .. code-block:: text >--------------------------- =========>------------------ ============================ A progress bar format is a string that contains specific placeholders (a name enclosed with the ``%`` character); the placeholders are replaced based on the current progress of the bar. Here is a list of the built-in placeholders: * ``current``: The current step * ``max``: The maximum number of steps (or 0 if no max is defined) * ``bar``: The bar itself * ``percent``: The percentage of completion (not available if no max is defined) * ``elapsed``: The time elapsed since the start of the progress bar * ``remaining``: The remaining time to complete the task (not available if no max is defined) * ``estimated``: The estimated time to complete the task (not available if no max is defined) * ``memory``: The current memory usage * ``message``: The current message attached to the progress bar For instance, here is how you could set the format to be the same as the ``debug`` one: .. code-block:: python progress.set_format(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%') Notice the ``:6s`` part added to some placeholders? That's how you can tweak the appearance of the bar (formatting and alignment). The part after the colon (``:``) is used to set the format of the string. The ``message`` placeholder is a bit special as you must set the value yourself: .. code-block:: python progress.set_message('Task starts') progress.start() progress.set_message('Task in progress...') progress.advance() # ... progress.set_message('Task is finished') progress.finish() Bar Settings ------------ Amongst the placeholders, ``bar`` is a bit special as all the characters used to display it can be customized: .. code-block:: python # the finished part of the bar progress.set_bar_character('=') # the unfinished part of the bar progress.set_empty_bar_character(' ') # the progress character progress.set_progress_character('|') # the bar width progress.set_bar_width(50) .. warning:: For performance reasons, be careful if you set the total number of steps to a high number. For example, if you're iterating over a large number of items, consider setting the redraw frequency to a higher value by calling ``ProgressHelper.set_redraw_frequency()``, so it updates on only some iterations: .. code-block:: python progress.start(50000) # update every 100 iterations progress.set_redraw_frequency(100) for _ in range(50000) # ... do some work progress.advance() cleo-2.1.0/docs/helpers/question_helper.rst000066400000000000000000000130141451777547300210110ustar00rootroot00000000000000Question Helper ############### Asking the User for Confirmation ================================ Suppose you want to confirm an action before actually executing it. Add the following to your command: .. code-block:: python def handle(self): if not self.confirm('Continue with this action?', False): return In this case, the user will be asked "Continue with this action?". If the user answers with ``y`` it returns ``True`` or ``False`` if they answer with ``n``. The second argument to ``confirm()`` is the default value to return if the user doesn't enter any valid input. If the second argument is not provided, ``True`` is assumed. .. tip:: You can customize the regex used to check if the answer means "yes" in the third argument of the ``customize()`` method. For instance, to allow anything that starts with either ``y`` or ``j``, you would set it to: .. code-block:: python self.confirm('Continue with this action?', False, '(?i)^(y|j)') The regex defaults to ``(?i)^y``. Asking the User for Information =============================== You can also ask a question with more than a simple yes/no answer. For instance, if you want to know a user name, you can add this to your command: .. code-block:: python def handle(self): name = self.ask('Please enter your name', 'John Doe') The user will be asked "Please enter your name". They can type some name which will be returned by the ``ask()`` method. If they leave it empty, the default value (``John Doe`` here) is returned. Let the User Choose from a List of Answers ------------------------------------------ If you have a predefined set of answers the user can choose from, you could use a ``ChoiceQuestion`` or the ``choice()`` method which makes sure that the user can only enter a valid string from a predefined list: .. code-block:: python def handle(self): color = self.choice( 'Please select your favorite color (defaults to red)', ['red', 'blue', 'yellow'], 0 ) self.line('You have just selected: %s' % color) The option which should be selected by default is provided with the third argument. The default is ``None``, which means that no option is the default one. If the user enters an invalid string, an error message is shown and the user is asked to provide the answer another time, until they enter a valid string or reach the maximum number of attempts. The default value for the maximum number of attempts is ``None``, which means infinite number of attempts. Multiple Choices ---------------- Sometimes, multiple answers can be given. The ``ChoiceQuestion`` or ``choice()`` method provides this feature using comma separated values. This is disabled by default, to enable this use the ``multiple`` keyword if using the ``choice()`` method or the ``multiselect`` attribute if using the ``ChoiceQuestion`` directly: .. code-block:: python def handle(self): colors = self.choice( 'Please select your favorite color (defaults to red and blue), ['red', 'blue', 'yellow'], '0,1' multiple=True ) self.line('You have just selected: %s' % ', '.join(colors)) Now, when the user enters ``1,2``, the result will be: ``You have just selected: blue, yellow``. If the user does not enter anything, the result will be: ``You have just selected: red, blue``. Autocompletion -------------- You can also specify an array of potential answers for a given question. These will be autocompleted as the user types: .. code-block:: python def handle(self): names = ['John', 'Jane', 'Paul'] question = self.create_question('Please enter a name', default='John') question.set_autocomplete_values(names) name = self.ask(question) Hiding the User's Response -------------------------- You can also ask a question and hide the response. This is particularly convenient for passwords: .. code-block:: python def handle(self): password = self.secret('What is the database password?') Validating the Answer ===================== You can even validate the answer. For instance, you might only accept integers: .. code-block:: python def handle(self): question = self.create_question('Choose a number') question.set_validator(int) question.set_max_attempts(2) number = self.ask(question) The ``validator`` a callback which handles the validation. It should throw an exception if there is something wrong. The exception message is displayed in the console, so it is a good practice to put some useful information in it. The validator or the callback function should also return the value of the user's input if the validation was successful. You can set the max number of times to ask with the ``set_max_attempts()`` method. If you reach this max number it will use the default value. Using ``None`` means the amount of attempts is infinite. The user will be asked as long as they provide an invalid answer and will only be able to proceed if their input is valid. Testing a Command that Expects Input ==================================== If you want to write a unit test for a command which expects some kind of input from the command line, you need to set the helper input stream: .. code-block:: python def test_execute_command(self): command_tester = CommandTester(command) # Equals to a user inputting "Test" and hitting ENTER # If you need to enter a confirmation, "yes\n" will work command_tester.execute(inputs="Test\n") cleo-2.1.0/docs/helpers/table.rst000066400000000000000000000171641451777547300167040ustar00rootroot00000000000000Table ##### When building a console application it may be useful to display tabular data: .. code-block:: text +---------------+--------------------------+------------------+ | 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 | +---------------+--------------------------+------------------+ To display a table, use the ``table()`` method, set the headers, set the rows and then render the table: .. code-block:: python def handle(self): table = self.table() table.set_headers(['ISBN', 'Title', 'Author']) table.set_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(self.io) .. tip:: All these steps can be done in one go using the ``render_table`` method: .. code-block:: python self.render_table( ['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'] ] ) You can add a table separator anywhere in the output by using ``table_separator()``, which returns a ``TableSeparator``, as a row: .. code-block:: python table.set_rows([ ['99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'], ['9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens'], self.table_separator(), ['960-425-059-0', 'The Lord of the Rings', 'J. R. R. Tolkien'], ['80-902734-1-6', 'And Then There Were None', 'Agatha Christie'] ]) .. code-block:: text +---------------+--------------------------+------------------+ | 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 | +---------------+--------------------------+------------------+ The table style can be changed to any built-in styles via ``set_style()``: .. code-block:: python # same as calling nothing table.set_style('default') # changes the default style to compact table.set_style('compact') This code results in: .. code-block:: text 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 You can also set the style to ``borderless``: .. code-block:: python table.set_style('borderless') which outputs: .. code-block:: text =============== ========================== ================== 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 =============== ========================== ================== If the built-in styles do not fit your need, define your own: .. code-block:: python # by default, this is based on the default style style = self.table_style() # customize the style style.set_horizontal_border_char('|') style.set_vertical_border_char('-') style.set_crossing_char(' ') # use the style for this table table.set_style(style) Here is a full list of things you can customize: * ``set_adding_char()`` * ``set_horizontal_border_char()`` * ``set_vertical_border_char()`` * ``set_crossing_char()`` * ``set_cell_header_format()`` * ``set_cell_row_format()`` * ``set_border_format()`` * ``set_pad_type()`` .. tip:: The style can also be passed as a keyword argument to ``render_table()`` .. code-block:: python self.render_table( ['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'] ] style='borderless' ) Spanning Multiple Columns and Rows ================================== To make a table cell that spans multiple columns you can use ``table_cell()``, which returns a ``TableCell`` instance: .. code-block:: python table = self.table() table.set_headers(['ISBN', 'Title', 'Author']) table.set_rows([ ['99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'], self.table_separator(), [self.table_cell('This value spans 3 columns.', colspan=3)] ]) table.render() This results in: .. code-block:: text +---------------+---------------+-----------------+ | ISBN | Title | Author | +---------------+---------------+-----------------+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri | +---------------+---------------+-----------------+ | This value spans 3 columns. | +---------------+---------------+-----------------+ .. tip:: You can create a multiple-line page title using a header cell that spans the entire table width: .. code-block:: python table.set_headers([ [self.table_cell('Main table title', colspan=3)], ['ISBN', 'Title', 'Author'] ]) This generate: .. code-block:: text +-------+-------+--------+ | Main table title | +-------+-------+--------+ | ISBN | Title | Author | +-------+-------+--------+ | ... | +-------+-------+--------+ In a similar way you can span multiple rows: .. code-block:: python table = self.table() table.set_headers(['ISBN', 'Title', 'Author']) table.set_rows([ [ '978-0521567817', 'De Monarchia', self.table_cell('Dante Alighieri\nspans multiple rows', rowspan=2) ] ]) table.render() This outputs: .. code-block:: text +----------------+---------------+---------------------+ | ISBN | Title | Author | +----------------+---------------+---------------------+ | 978-0521567817 | De Monarchia | Dante Alighieri | | 978-0804169127 | Divine Comedy | spans multiple rows | +----------------+---------------+---------------------+ You can use the ``colspan`` and ``rowspan`` options at the same time which allows you to create any table layout you may wish. cleo-2.1.0/docs/index.rst000066400000000000000000000005251451777547300152530ustar00rootroot00000000000000Cleo ==== Cleo allows you to create beautiful and testable command-line commands. It is heavily inspired by the `Symfony Console Component `_, with some useful additions. .. toctree:: :maxdepth: 2 installation introduction helpers/index usage single_command_tool api/index cleo-2.1.0/docs/installation.rst000066400000000000000000000004701451777547300166440ustar00rootroot00000000000000Installation ############ You can install Cleo in various ways: * Using `Poetry `_ : .. code-block:: bash $ poetry add cleo * Using ``pip`` .. code-block:: bash $ pip install cleo * Use the `official repository `_ cleo-2.1.0/docs/introduction.rst000066400000000000000000000365051451777547300166740ustar00rootroot00000000000000Usage ##### To make a command that greets you from the command line, create ``greet_command.py`` and add the following to it: .. code-block:: python from cleo import Command class GreetCommand(Command): """ Greets someone greet {name? : Who do you want to greet?} {--y|yell : If set, the task will yell in uppercase letters} """ def handle(self): name = self.argument('name') if name: text = 'Hello {}'.format(name) else: text = 'Hello' if self.option('yell'): text = text.upper() self.line(text) You also need to create the file to run at the command line which creates an ``Application`` and adds commands to it: .. code-block:: python #!/usr/bin/env python from greet_command import GreetCommand from cleo import Application application = Application() application.add(GreetCommand()) if __name__ == '__main__': application.run() Test the new command by running the following .. code-block:: bash $ python application.py greet John This will print the following to the command line: .. code-block:: text Hello John You can also use the ``--yell`` option to make everything uppercase: .. code-block:: bash $ python application.py greet John --yell This prints: .. code-block:: text HELLO JOHN As you may have already seen, Cleo uses the command docstring to determine the command definition. The docstring must be in the following form : .. code-block:: python """ Command description Command signature """ The signature being in the following form: .. code-block:: python """ command:name {argument : Argument description} {--option : Option description} """ The signature can span multiple lines. .. code-block:: python """ command:name {argument : Argument description} {--option : Option description} """ Coloring the Output =================== Whenever you output text, you can surround the text with tags to color its output. For example: .. code-block:: python # blue text self.line("foo") # green text self.line("foo") # cyan text self.line("foo") # bold red text self.line("foo") # cyan text self.line("foo") # bold text self.line("foo") # bold text self.line("foo") The closing tag can be replaced by ````, which revokes all formatting options established by the last opened tag. It is possible to define your own styles using the ``add_style()`` method: .. code-block:: python self.add_style('fire', fg='red', bg='yellow', options=['bold', 'blink']) self.line('foo') Available foreground and background colors are: ``black``, ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, ``cyan`` and ``white``. And available options are: ``bold``, ``underscore``, ``blink``, ``reverse`` and ``conceal``. You can also set these colors and options inside the tag name: .. code-block:: python # green text self.line('foo') # black text on a cyan background self.line('foo') # bold text on a yellow background self.line('foo') Verbosity Levels ================ Cleo has four verbosity levels. These are defined in the ``Output`` class: ======================================= ================================== ====================== Mode Meaning Console option ======================================= ================================== ====================== ``NA`` Do not output any messages ``-q`` or ``--quiet`` ``Verbosity.NORMAL`` The default verbosity level (none) ``Verbosity.VERBOSE`` Increased verbosity of messages ``-v`` ``Verbosity.VERY_VERBOSE`` Informative non essential messages ``-vv`` ``Verbosity.DEBUG`` Debug messages ``-vvv`` ======================================= ================================== ====================== It is possible to print a message in a command for only a specific verbosity level. For example: .. code-block:: python if Verbosity.VERBOSE <= self.io.verbosity: self.line(...) There are also more semantic methods you can use to test for each of the verbosity levels: .. code-block:: python if self.output.is_quiet(): # ... if self.output.is_verbose(): # ... You can also pass the verbosity flag directly to `line()`. .. code-block:: python self.line("", verbosity=Verbosity.VERBOSE) When the quiet level is used, all output is suppressed. Using Arguments =============== The most interesting part of the commands are the arguments and options that you can make available. Arguments are the strings - separated by spaces - that come after the command name itself. They are ordered, and can be optional or required. For example, add an optional ``last_name`` argument to the command and make the ``name`` argument required: .. code-block:: python class GreetCommand(Command): """ Greets someone greet {name : Who do you want to greet?} {last_name? : Your last name?} {--y|yell : If set, the task will yell in uppercase letters} """ You now have access to a ``last_name`` argument in your command: .. code-block:: python last_name = self.argument('last_name') if last_name: text += ' {}'.format(last_name) The command can now be used in either of the following ways: .. code-block:: bash $ python application.py greet John $ python application.py greet John Doe It is also possible to let an argument take a list of values (imagine you want to greet all your friends). For this it must be specified at the end of the argument list: .. code-block:: python class GreetCommand(Command): """ Greets someone greet {names* : Who do you want to greet?} {--y|yell : If set, the task will yell in uppercase letters} """ To use this, just specify as many names as you want: .. code-block:: bash $ python application.py demo:greet John Jane You can access the ``names`` argument as a list: .. code-block:: python names = self.argument('names') if names: text += ' {}'.format(', '.join(names)) There are 3 argument variants you can use: ================================ ==================================== =============================================================================================================== Mode Notation Value ================================ ==================================== =============================================================================================================== ``Required`` none (just write the argument name) The argument is required ``Optional`` ``argument?`` The argument is optional and therefore can be omitted ``List`` ``argument*`` The argument can contain an indefinite number of arguments and must be used at the end of the argument list ================================ ==================================== =============================================================================================================== You can combine them like this: .. code-block:: python class GreetCommand(Command): """ Greets someone greet {names?* : Who do you want to greet?} {--y|yell : If set, the task will yell in uppercase letters} """ If you want to set a default value, you can it like so: .. code-block:: text argument=default The argument will then be considered optional. Using Options ============= Unlike arguments, options are not ordered (meaning you can specify them in any order) and are specified with two dashes (e.g. ``--yell`` - you can also declare a one-letter shortcut that you can call with a single dash like ``-y``). Options are *always* optional, and can be setup to accept a value (e.g. ``--dir=src``) or simply as a boolean flag without a value (e.g. ``--yell``). .. tip:: It is also possible to make an option *optionally* accept a value (so that ``--yell`` or ``--yell=loud`` work). Options can also be configured to accept a list of values. For example, add a new option to the command that can be used to specify how many times in a row the message should be printed: .. code-block:: python class GreetCommand(Command): """ Greets someone greet {name? : Who do you want to greet?} {--y|yell : If set, the task will yell in uppercase letters} {--iterations=1 : How many times should the message be printed?} """ Next, use this in the command to print the message multiple times: .. code-block:: python for _ in range(0, self.option('iterations')): self.line(text) Now, when you run the task, you can optionally specify a ``--iterations`` flag: .. code-block:: bash $ python application.py demo:greet John $ python application.py demo:greet John --iterations=5 The first example will only print once, since ``iterations`` is empty and defaults to ``1``. The second example will print five times. Recall that options don't care about their order. So, either of the following will work: .. code-block:: bash $ python application.py demo:greet John --iterations=5 --yell $ python application.py demo:greet John --yell --iterations=5 There are 4 option variants you can use: ================================ =================================== ====================================================================================== Option Notation Value ================================ =================================== ====================================================================================== ``List`` ``--option=*`` This option accepts multiple values (e.g. ``--dir=/foo --dir=/bar``) ``Flag`` ``--option`` Do not accept input for this option (e.g. ``--yell``) ``Requires value`` ``--option=`` This value is required (e.g. ``--iterations=5``), the option itself is still optional ``Optional value`` ``--option=?`` This option may or may not have a value (e.g. ``--yell`` or ``--yell=loud``) ================================ =================================== ====================================================================================== You can combine them like this: .. code-block:: python class GreetCommand(Command): """ Greets someone greet {name? : Who do you want to greet?} {--y|yell : If set, the task will yell in uppercase letters} {--iterations=?*1 : How many times should the message be printed?} """ Helpers ======= Cleo also contains a set of "helpers" - different small tools capable of helping you with different tasks: * :doc:`helpers/question_helper`: interactively ask the user for information * :doc:`helpers/progress_bar`: shows a progress bar * :doc:`helpers/table`: displays tabular data as a table Testing Commands ================ Cleo provides several tools to help you test your commands. The most useful one is the ``CommandTester`` class. It uses a special IO class to ease testing without a real console: .. code-block:: python import pytest from cleo import Application from cleo.testers.command_tester import CommandTester def test_execute(self): application = Application() application.add(GreetCommand()) command = application.find('demo:greet') command_tester = CommandTester(command) command_tester.execute() assert "..." == command_tester.io.fetch_output() The ``CommandTester.io.fetch_output()`` method returns what would have been displayed during a normal call from the console. ``CommandTester.io.fetch_error()`` is also available to get what you have been written to the stderr. You can test sending arguments and options to the command by passing them as a string to the ``CommandTester.execute()`` method: .. code-block:: python import pytest from cleo import Application from cleo.testers.command_tester import CommandTester def test_execute(self): application = Application() application.add(GreetCommand()) command = application.find('demo:greet') command_tester = CommandTester(command) command_tester.execute("John") assert "John" in command_tester.io.fetch_output() Testing with user inputs ------------------------ To test user inputs, you pass it to ``execute()``. .. code-block:: python command_tester = CommandTester(command) command_tester.execute(inputs="123\nfoo\nbar") .. tip:: You can also test a whole console application by using the ``ApplicationTester`` class. Calling an existing Command =========================== If a command depends on another one being run before it, instead of asking the user to remember the order of execution, you can call it directly yourself. This is also useful if you want to create a "meta" command that just runs a bunch of other commands. Calling a command from another one is straightforward: .. code-block:: python def handle(self): return_code = self.call('demo:greet', "John --yell") # ... .. tip:: If you want to suppress the output of the executed command, you can use the ``call_silent()`` method instead. Overwrite the current line ========================== If you want to overwrite the current line, you can use the ``overwrite()`` method. .. code-block:: python def handle(self): self.write('Processing...') # do some work self.overwrite('Done!') .. warning:: ``overwrite()`` will only work in combination with the ``write()`` method which does not add a new line. .. note:: ``overwrite()`` does not automatically add a new line so you must call ``line('')`` if necessary. Autocompletion ============== Cleo supports automatic (tab) completion in ``bash``, ``zsh`` and ``fish``. By default, your application will have a ``completions`` command. To register these completions for your application, run one of the following in a terminal (replacing ``[program]`` with the command you use to run your application): .. code-block:: bash # Bash [program] completions bash | sudo tee /etc/bash_completion.d/[program].bash-completion # Bash - macOS/Homebrew (requires `brew install bash-completion`) [program] completions bash > $(brew --prefix)/etc/bash_completion.d/[program].bash-completion # Zsh mkdir ~/.zfunc echo "fpath+=~/.zfunc" >> ~/.zshrc [program] completions zsh > ~/.zfunc/_[program] # Zsh - macOS/Homebrew [program] completions zsh > $(brew --prefix)/share/zsh/site-functions/_[program] # Fish [program] completions fish > ~/.config/fish/completions/[program].fish cleo-2.1.0/docs/make.bat000066400000000000000000000014401451777547300150140ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd cleo-2.1.0/docs/single_command_tool.rst000066400000000000000000000010411451777547300201520ustar00rootroot00000000000000Building a Single Command Application ##################################### When building a command line tool, you may not need to provide several commands. In such case, having to pass the command name each time is tedious. Fortunately, it is possible to remove this need by using `default()` when adding a command: .. code-block:: python from cleo import Application command = GreetCommand() app = Application() app.add(command.default()) # this now executes the 'GreetCommand' without passing its name app.run() cleo-2.1.0/docs/usage.rst000066400000000000000000000053721451777547300152550ustar00rootroot00000000000000Using Console Commands, Shortcuts and Built-in Commands ####################################################### In addition to the options you specify for your commands, there are some built-in options as well as a couple of built-in commands for Cleo. .. note:: These examples assume you have added a file ``application.py`` to run at the cli: .. code-block:: python #!/usr/bin/env python # application.py from cleo import Application application = Application() # ... if __name__ == '__main__': application.run() Built-in Commands ================= The help command lists the help information for the specified command. For example, to get the help for the ``list`` command: .. code-block:: bash $ python application.py help list Running ``help`` without specifying a command will list the global options: .. code-block:: bash $ python application.py help Global Options ============== You can get help information for any command with the ``--help`` option. To get help for the ``greet`` command: .. code-block:: bash $ python application.py greet --help $ python application.py greet -h You can suppress output with: .. code-block:: bash $ python application.py greet --quiet $ python application.py greet -q You can get more verbose messages (if this is supported for a command) with: .. code-block:: bash $ python application.py greet --verbose $ python application.py greet -v If you need more verbose output, use `-vv` or `-vvv` .. code-block:: bash $ python application.py greet -vv $ python application.py greet -vvv If you set the optional arguments to give your application a name and version: .. code-block:: python application = Application('console', '1.2') then you can use: .. code-block:: bash $ python application.py --version $ python application.py -V to get this information output: .. code-block:: text Console version 1.2 If you do not provide both arguments then it will just output: .. code-block:: text console tool You can force turning on ANSI output coloring with: .. code-block:: bash $ python application.py greet --ansi or turn it off with: .. code-block:: bash $ python application.py greet --no-ansi You can suppress any interactive questions from the command you are running with: .. code-block:: bash $ python application.py greet --no-interaction $ python application.py greet -n Shortcut Syntax =============== You do not have to type out the full command names. You can just type the shortest unambiguous name to run a command. So if there are non-clashing commands, then you can run ``help`` like this: .. code-block:: bash $ python application.py h cleo-2.1.0/news/000077500000000000000000000000001451777547300134345ustar00rootroot00000000000000cleo-2.1.0/news/.gitignore000066400000000000000000000000141451777547300154170ustar00rootroot00000000000000!.gitignore cleo-2.1.0/news/news_template.jinja2000066400000000000000000000013711451777547300174040ustar00rootroot00000000000000{% if top_line %} {{ top_line }} {{ top_underline * ((top_line)|length)}} {% endif %} {% for section in sections %} {% if sections[section] %} {% for category, val in definitions.items() if category in sections[section] and category != 'trivial' %} ### {{ definitions[category]['name'] }} {% if definitions[category]['showcontent'] %} {% for text, values in sections[section][category]|dictsort(by='value') %} - {{ text }} {% if category != 'process' %}{{ values|sort|join(',\n ') }}{% endif %} {% endfor %} {% else %} - {{ sections[section][category]['']|sort|join(', ') }} {% endif %} {% if sections[section][category]|length == 0 %} No significant changes. {% else %} {% endif %} {% endfor %} {% else %} No significant changes. {% endif %} {% endfor %} cleo-2.1.0/poetry.lock000066400000000000000000003120031451777547300146530ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" optional = false python-versions = ">=3.6" files = [ {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, ] [[package]] name = "babel" version = "2.13.1" description = "Internationalization utilities" optional = false python-versions = ">=3.7" files = [ {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"}, {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, ] [package.dependencies] pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} setuptools = {version = "*", markers = "python_version >= \"3.12\""} [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] [[package]] name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.6.1" files = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] [[package]] name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] name = "charset-normalizer" version = "3.3.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ {file = "charset-normalizer-3.3.1.tar.gz", hash = "sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e"}, {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8"}, {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690"}, {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2"}, {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821"}, {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d"}, {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147"}, {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5"}, {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846"}, {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3"}, {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605"}, {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1"}, {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1"}, {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056"}, {file = "charset_normalizer-3.3.1-cp310-cp310-win32.whl", hash = "sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f"}, {file = "charset_normalizer-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397"}, {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf"}, {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d"}, {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c"}, {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72"}, {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b"}, {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d"}, {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673"}, {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008"}, {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f"}, {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7"}, {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4"}, {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3"}, {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213"}, {file = "charset_normalizer-3.3.1-cp311-cp311-win32.whl", hash = "sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8"}, {file = "charset_normalizer-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff"}, {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1"}, {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55"}, {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f"}, {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296"}, {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728"}, {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf"}, {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a"}, {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd"}, {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b"}, {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14"}, {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514"}, {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c"}, {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4"}, {file = "charset_normalizer-3.3.1-cp312-cp312-win32.whl", hash = "sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61"}, {file = "charset_normalizer-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-win32.whl", hash = "sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9"}, {file = "charset_normalizer-3.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae"}, {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761"}, {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557"}, {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45"}, {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b"}, {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5"}, {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f"}, {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2"}, {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b"}, {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2"}, {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c"}, {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42"}, {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597"}, {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c"}, {file = "charset_normalizer-3.3.1-cp38-cp38-win32.whl", hash = "sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb"}, {file = "charset_normalizer-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97"}, {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f"}, {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93"}, {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041"}, {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e"}, {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62"}, {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273"}, {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67"}, {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656"}, {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5"}, {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57"}, {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e"}, {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97"}, {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2"}, {file = "charset_normalizer-3.3.1-cp39-cp39-win32.whl", hash = "sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4"}, {file = "charset_normalizer-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727"}, {file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"}, ] [[package]] name = "click" version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "click-default-group" version = "1.2.4" description = "click_default_group" optional = false python-versions = ">=2.7" files = [ {file = "click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f"}, {file = "click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e"}, ] [package.dependencies] click = "*" [package.extras] test = ["pytest"] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "coverage" version = "7.2.7" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.7" files = [ {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "crashtest" version = "0.4.1" description = "Manage Python errors with ease" optional = false python-versions = ">=3.7,<4.0" files = [ {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"}, {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, ] [[package]] name = "distlib" version = "0.3.7" description = "Distribution utilities" optional = false python-versions = "*" files = [ {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, ] [[package]] name = "docutils" version = "0.18.1" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, ] [[package]] name = "exceptiongroup" version = "1.1.3" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "filelock" version = "3.12.2" description = "A platform independent file lock." optional = false python-versions = ">=3.7" files = [ {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, ] [package.extras] docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] [[package]] name = "filelock" version = "3.12.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, ] [package.extras] docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] typing = ["typing-extensions (>=4.7.1)"] [[package]] name = "identify" version = "2.5.24" description = "File identification library for Python" optional = false python-versions = ">=3.7" files = [ {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "identify" version = "2.5.30" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"}, {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] [[package]] name = "importlib-metadata" version = "6.7.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.7" files = [ {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, ] [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] [[package]] name = "importlib-resources" version = "5.12.0" description = "Read resources from Python packages" optional = false python-versions = ">=3.7" files = [ {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "incremental" version = "22.10.0" description = "\"A small library that versions your Python projects.\"" optional = false python-versions = "*" files = [ {file = "incremental-22.10.0-py2.py3-none-any.whl", hash = "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51"}, {file = "incremental-22.10.0.tar.gz", hash = "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0"}, ] [package.extras] mypy = ["click (>=6.0)", "mypy (==0.812)", "twisted (>=16.4.0)"] scripts = ["click (>=6.0)", "twisted (>=16.4.0)"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] [[package]] name = "mypy" version = "1.4.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.7" files = [ {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] [[package]] name = "mypy" version = "1.6.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ {file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"}, {file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"}, {file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"}, {file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"}, {file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"}, {file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"}, {file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"}, {file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"}, {file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"}, {file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"}, {file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"}, {file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"}, {file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"}, {file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"}, {file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"}, {file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"}, {file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"}, {file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"}, {file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"}, {file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"}, {file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"}, {file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"}, {file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"}, {file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"}, {file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"}, {file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"}, {file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] reports = ["lxml"] [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, ] [package.dependencies] setuptools = "*" [[package]] name = "packaging" version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] name = "platformdirs" version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, ] [package.dependencies] typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""} [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] name = "pluggy" version = "1.2.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.7" files = [ {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, ] [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.7" files = [ {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, ] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] name = "pre-commit" version = "3.5.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.8" files = [ {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, ] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] name = "pygments" version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, ] [package.extras] plugins = ["importlib-metadata"] [[package]] name = "pytest" version = "7.4.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.7" files = [ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, ] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytest-mock" version = "3.11.1" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.7" files = [ {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, ] [package.dependencies] pytest = ">=5.0" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytz" version = "2023.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] [[package]] name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [[package]] name = "rapidfuzz" version = "3.4.0" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.7" files = [ {file = "rapidfuzz-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1438e68fe8869fe6819a313140e98641b34bfc89234b82486d8fd02044a067e8"}, {file = "rapidfuzz-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59f851c7a54a9652b9598553547e0940244bfce7c9b672bac728efa0b9028d03"}, {file = "rapidfuzz-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6286510910fcd649471a7f5b77fcc971e673729e7c84216dbf321bead580d5a1"}, {file = "rapidfuzz-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87409e12f9a82aa33a5b845c49dd8d5d4264f2f171f0a69ddc638e100fcc50de"}, {file = "rapidfuzz-3.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1d81d380ceabc8297880525c9d8b9e93fead38d3d2254e558c36c18aaf2553f"}, {file = "rapidfuzz-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a716efcfc92659d8695291f07da4fa60f42a131dc4ceab583931452dd5662e92"}, {file = "rapidfuzz-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83387fb81c4c0234b199110655779762dd5982cdf9de4f7c321110713193133e"}, {file = "rapidfuzz-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55efb3231bb954f3597313ebdf104289b8d139d5429ad517051855f84e12b94e"}, {file = "rapidfuzz-3.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51d47d52c890cbdb2d8b2085d747e557f15efd9c990cb6ae624c8f6948c4aa3a"}, {file = "rapidfuzz-3.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3db79070888d0dcd4f6a20fd30b8184dd975d6b0f7818acff5d7e07eba19b71f"}, {file = "rapidfuzz-3.4.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:46efc5e4675e2bd5118427513f86eaf3689e1482ebd309ad4532bcefae78179d"}, {file = "rapidfuzz-3.4.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d15c364c5aa8f032dadf5b82fa02b7a4bd9688a961a27961cd5b985203f58037"}, {file = "rapidfuzz-3.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f1e91460baa42f5408f3c062913456a24b2fc1a181959b58a9c06b5eef700ca6"}, {file = "rapidfuzz-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c7f4f6dac25c120de8845a65a97090658c8a976827ac22b6b86e2a16a60bb820"}, {file = "rapidfuzz-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:124578029d926b2be32d60b748be95ee0de6cb2753eb49d6d1d6146269b428b9"}, {file = "rapidfuzz-3.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:3af0384132e79fe6f6370d49347649382e04f689277525903bef84d30f3992fd"}, {file = "rapidfuzz-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:66ff93b81b382269dc7c2d46c839ce72e2d2331ad46a06321770bc94016fe236"}, {file = "rapidfuzz-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da2764604a31fd1e3f1cacf226b43a871cc9f28844a3196c2a6b1ba52ae12922"}, {file = "rapidfuzz-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8eb33895353bfcc33ccf4b4bae837c0afb4eaf20a0361aa6f0800cef12505e91"}, {file = "rapidfuzz-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed3da08830c08c8bcd49414cc06b704a760d3067804775facc0df725b52085a4"}, {file = "rapidfuzz-3.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b38c7021f6114cfacba5717192fb3e1e50053261d49a774e645021a2f77e20a3"}, {file = "rapidfuzz-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5ea97886d2ec7b2b9a8172812a76e1d243f2ce705c2f24baf46f9ef5d3951"}, {file = "rapidfuzz-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b9a7ab061c1b75b274fc2ebd1d29cfa2e510c36e2f4cd9518a6d56d589003c8"}, {file = "rapidfuzz-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23b07685c21c93cdf6d68b49eccacfe975651b8d99ea8a02687400c60315e5bc"}, {file = "rapidfuzz-3.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c2a564f748497b6a5e08a1dc0ac06655f65377cf072c4f0e2c73818acc655d36"}, {file = "rapidfuzz-3.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ef30b5f2720f0acbcfba0e0661a4cc118621c47cf69b5fe92531dfed1e369e1c"}, {file = "rapidfuzz-3.4.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab981f9091ae8bd32bca9289fa1019b4ec656543489e7e13e64882d57d989282"}, {file = "rapidfuzz-3.4.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:a80f9aa4245a49e0677896d1b51b2b3bc36472aff7cec31c4a96f789135f03fe"}, {file = "rapidfuzz-3.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d8c6cb80b5d2edf88bf6a88ac6827a353c974405c2d7e3025ed9527a5dbe1a6"}, {file = "rapidfuzz-3.4.0-cp311-cp311-win32.whl", hash = "sha256:c0150d521199277b5ad8bd3b060a5f3c1dbdf11df0533b4d79f458ef11d07e8c"}, {file = "rapidfuzz-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:bd50bc90167601963e2a90b820fb862d239ecb096a991bf3ce33ffaa1d6eedee"}, {file = "rapidfuzz-3.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:bd10d68baabb63a3bb36b683f98fc481fcc62230e493e4b31e316bd5b299ef68"}, {file = "rapidfuzz-3.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7f497f850d46c5e08f3340343842a28ede5d3997e5d1cadbd265793cf47417e5"}, {file = "rapidfuzz-3.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7d6a9f04ea1277add8943d4e144e59215009f54f2668124ff26dee18a875343"}, {file = "rapidfuzz-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b6fe2aff0d9b35191701714e05afe08f79eaea376a3a6ca802b72d9e5b48b545"}, {file = "rapidfuzz-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b81b8bc29114ca861fed23da548a837832b85495b0c1b2600e6060e3cf4d50aa"}, {file = "rapidfuzz-3.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:805dc2aa3ac295dcbf2df8c1e420e8a73b1f632d6820a5a1c8506d22c11e0f27"}, {file = "rapidfuzz-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1276c7f50cd90a48b00084feb25256135c9ace6c599295dd5932949ec30c0e70"}, {file = "rapidfuzz-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b9197656a6d71483959bf7d216e7fb7a6b80ca507433bcb3015fb92abc266f8"}, {file = "rapidfuzz-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3456f4df5b8800315fd161045c996479016c112228e4da370d09ed80c24853e5"}, {file = "rapidfuzz-3.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:734046d557550589edb83d5ad1468a1341d1092f1c64f26fd0b1fc50f9efdce1"}, {file = "rapidfuzz-3.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:37d5f0fbad6c092c89840eea2c4c845564d40849785de74c5e6ff48b47b0ecf6"}, {file = "rapidfuzz-3.4.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:bfe14711b9a7b744e242a482c6cabb696517a1a9946fc1e88d353cd3eb384788"}, {file = "rapidfuzz-3.4.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a733c10b1fcc47f837c23ab4a255cc4021a88939ff81baa64d6738231cba33d"}, {file = "rapidfuzz-3.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:929e6b71e5b36caee2ee11c209e75a0fcbd716a1b76ae6162b89ee9b591b63b1"}, {file = "rapidfuzz-3.4.0-cp312-cp312-win32.whl", hash = "sha256:c56073ba1d1b25585359ad9769163cb2f3183e7a03c03b914a0667fcbd95dc5c"}, {file = "rapidfuzz-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf58ba21df06fc8aeef3056fd137eca0a593c2f5c82923a4524d251dc5f3df5d"}, {file = "rapidfuzz-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:f3effbe9c677658b3149da0d2778a740a6b7d8190c1407fd0c0770a4e223cfe0"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ed0d5761b44d9dd87278d5c32903bb55632346e4d84ea67ba2e4a84afc3b7d45"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bafbd3e2e9e0b5f740f66155cc7e1e23eee1e1f2c44eff12daf14f90af0e8ab"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2543fd8d0fb3b1ac065bf94ee54c0ea33343c62481d8e54b6117a88c92c9b721"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93ceb62ade1a0e62696487274002157a58bb751fc82cd25016fc5523ba558ca5"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76f4162ce5fe08609455d318936ed4aa709f40784be61fb4e200a378137b0230"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f723197f2dbce508a7030dcf6d3fc940117aa54fc876021bf6f6feeaf3825ba1"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cfdc74afd93ac71270b5be5c25cb864b733b9ae32b07495705a6ac294ac4c390"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:273c7c7f5b405f2f54d41e805883572d57e1f0a56861f93ca5a6733672088acb"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:712dd91d429afaddbf7e86662155f2ad9bc8135fca5803a01035a3c1d76c5977"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9814905414696080d8448d6e6df788a0148954ab34d7cd8d75bcb85ba30e0b25"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:01013ee67fb15608c8c5961af3bc2b1f242cff94c19f53237c9b3f0edb8e0a2d"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:8f5d2adc48c181486125d42230e80479a1e0568942e883d1ebdeb76cd3f83470"}, {file = "rapidfuzz-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c92d847c997c384670e3b4cf6727cb73a4d7a7ba6457310e2083cf06d56013c4"}, {file = "rapidfuzz-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d0bda173b0ec1fa546f123088c0d42c9096304771b4c0555d4e08a66a246b3f6"}, {file = "rapidfuzz-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bbb05b1203f683b341f44ebe8fe38afed6e56f606094f9840d6406e4a7bf0eab"}, {file = "rapidfuzz-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f0075ff8990437923da42202b60cf04b5c122ee2856f0cf2344fb890cadecf57"}, {file = "rapidfuzz-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f295842c282fe7fe93bfe7a20e78f33f43418f47fb601f2f0a05df8a8282b43"}, {file = "rapidfuzz-3.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebee7313719dfe652debb74bdd4024e8cf381a59adc6d065520ff927f3445f4"}, {file = "rapidfuzz-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f71454249ddd29d8ba5415ed7307e7b7493fc7e9018f1ff496127b8b9a8df94b"}, {file = "rapidfuzz-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52c6b7a178f0e800488fa1aede17b00f6397cab0b79d48531504b0d89e45315f"}, {file = "rapidfuzz-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d38596c804a9f2bd49360c15e1f4afbf016f181fe37fc4f1a4ddd247d3e91e5"}, {file = "rapidfuzz-3.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8756461e7ee79723b8f762fc6db226e65eb453bf9fa64b14fc0274d4aaaf9e21"}, {file = "rapidfuzz-3.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e14799297f194a4480f373e45142ef16d5dc68a42084c0e2018e0bdba56a8fef"}, {file = "rapidfuzz-3.4.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f813fb663d90038c1171d30ea1b6b275e09fced32f1d12b972c6045d9d4233f2"}, {file = "rapidfuzz-3.4.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0df66e07e42e2831fae84dea481f7803bec7cfa53c31d770e86ac47bb18dcd57"}, {file = "rapidfuzz-3.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b05c7d4b4ddb617e977d648689013e50e5688140ee03538d3760a3a11d4fa8a2"}, {file = "rapidfuzz-3.4.0-cp38-cp38-win32.whl", hash = "sha256:74b9a1c1fc139d325fb0b89ccc85527d27096a76f6ed690ee3378143cc38e91d"}, {file = "rapidfuzz-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5fe3ef7daecd79f852936528e37528fd88818bc000991e0fea23b9ac5b79e875"}, {file = "rapidfuzz-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61f16bb0f3026853500e7968261831a2e1a35d56947752bb6cf6953afd70b9de"}, {file = "rapidfuzz-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d188e8fb5a9709931c6a48cc62c4ac9b9d163969333711e426d9dbd134c1489b"}, {file = "rapidfuzz-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c006aa481d1b91c2600920ce16e42d208a4b6f318d393aef4dd2172d568f2641"}, {file = "rapidfuzz-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02afbe7ed12e9191082ed7bda43398baced1d9d805302b7b010d397de3ae973f"}, {file = "rapidfuzz-3.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01d64710060bc3241c08ac1f1a9012c7184f3f4c3d6e2eebb16c6093a03f6a67"}, {file = "rapidfuzz-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3198f70b97127e52a4f96bb2f7de447f89baa338ff398eb126930c8e3137ad1"}, {file = "rapidfuzz-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50ad7bac98a0f00492687eddda73d2c0bdf71c78b52fddaa5901634ae323d3ce"}, {file = "rapidfuzz-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc3efc06db79e818f4a6783a4e001b3c8b2c61bd05c0d5c4d333adaf64ed1b34"}, {file = "rapidfuzz-3.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:75d1365387ec8ef2128fd7e2f7436aa1a04a1953bc6d7068835bb769cd07c146"}, {file = "rapidfuzz-3.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a0750278693525b5ce58d3b313e432dfa5d90f00d06ae54fa8cde87f2a397eb0"}, {file = "rapidfuzz-3.4.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:2e49151572b842d290dcee2cc6f9ce7a7b40b77cc20d0f6d6b54e7afb7bafa5c"}, {file = "rapidfuzz-3.4.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:8b38d7677b2f20b137bb7aaf0dcd3d8ac2a2cde65f09f5621bf3f57d9a1e5d6e"}, {file = "rapidfuzz-3.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d904ac97f2e370f91e8170802669c8ad68641bf84d742968416b53c5960410c6"}, {file = "rapidfuzz-3.4.0-cp39-cp39-win32.whl", hash = "sha256:53bbef345644eac1c2d7cc21ade4fe9554fa289f60eb2c576f7fdc454dbc0641"}, {file = "rapidfuzz-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:233bf022938c38060a93863ec548e624d69a56d7384634d8bea435b915b88e52"}, {file = "rapidfuzz-3.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:63933792146f3d333680d415cecc237e6275b42ad948d0a798f9a81325517666"}, {file = "rapidfuzz-3.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e182ea5c809e7ed36ebfbcef4bb1808e213d27b33c036007a33bcbb7ba498356"}, {file = "rapidfuzz-3.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e1142c8d35fa6f3af8150d02ff8edcbea3723c851d889e8b2172e0d1b99f3f7"}, {file = "rapidfuzz-3.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b8258846e56b03230fa733d29bb4f9fb1f4790ac97d1ebe9faa3ff9d2850999"}, {file = "rapidfuzz-3.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:950d1dfd2927cd45c9bb2927933926718f0a17792841e651d42f4d1cb04a5c1d"}, {file = "rapidfuzz-3.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:dd54dd0355225dc3c1d55e233d510adcccee9bb25d656b4cf1136114b92e7bf3"}, {file = "rapidfuzz-3.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f5921780e7995e9ac3cea41fa57b623159d7295788618d3f2946d61328c25c25"}, {file = "rapidfuzz-3.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc4b1b69a64d337c40fa07a721dae1b1550d90f17973fb348055f6440d597e26"}, {file = "rapidfuzz-3.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5c8b901b6d3be63591c68e2612f76ad85af27193d0a88d4d87bb047aeafcb3"}, {file = "rapidfuzz-3.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c67f5ced39aff6277dd772b239ef8aa8fc810200a3b42f69ddbb085ea0e18232"}, {file = "rapidfuzz-3.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4fd94acab871afbc845400814134a83512a711e824dc2c9a9776d6123464a221"}, {file = "rapidfuzz-3.4.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:437508ec1ea6e71a77126715ac6208cb9c3e74272536ebfa79be9dd008cfb85f"}, {file = "rapidfuzz-3.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7215f7c5de912b364d5cf7c4c66915ccf4acf71aafbb8da62ad346569196e15"}, {file = "rapidfuzz-3.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:698488002eb7be2f737e48679ed0cd310b76291f26d8ec792db8345d13eb6573"}, {file = "rapidfuzz-3.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e77873126eb07e7461f0b675263e6c5d42c8a952e88e4a44eeff96f237b2b024"}, {file = "rapidfuzz-3.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:28d03cd33817f6e0bea9b618b460f85ff9c9c3fedc6c19cfa0992f719a0d1801"}, {file = "rapidfuzz-3.4.0.tar.gz", hash = "sha256:a74112e2126b428c77db5e96f7ce34e91e750552147305b2d361122cbede2955"}, ] [package.extras] full = ["numpy"] [[package]] name = "requests" version = "2.31.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.7" files = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "setuptools" version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] [[package]] name = "sphinx" version = "5.3.0" description = "Python documentation generator" optional = false python-versions = ">=3.6" files = [ {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, ] [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.14,<0.20" imagesize = ">=1.3" importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.12" requests = ">=2.5.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] [[package]] name = "sphinx-rtd-theme" version = "1.3.0" description = "Read the Docs theme for Sphinx" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, ] [package.dependencies] docutils = "<0.19" sphinx = ">=1.6,<8" sphinxcontrib-jquery = ">=4,<5" [package.extras] dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] [[package]] name = "sphinxcontrib-applehelp" version = "1.0.2" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.6" files = [ {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jquery" version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" optional = false python-versions = ">=2.7" files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, ] [package.dependencies] Sphinx = ">=1.8" [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] [package.extras] test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] [[package]] name = "towncrier" version = "23.6.0" description = "Building newsfiles for your project." optional = false python-versions = ">=3.7" files = [ {file = "towncrier-23.6.0-py3-none-any.whl", hash = "sha256:da552f29192b3c2b04d630133f194c98e9f14f0558669d427708e203fea4d0a5"}, {file = "towncrier-23.6.0.tar.gz", hash = "sha256:fc29bd5ab4727c8dacfbe636f7fb5dc53b99805b62da1c96b214836159ff70c1"}, ] [package.dependencies] click = "*" click-default-group = "*" importlib-resources = {version = ">=5", markers = "python_version < \"3.10\""} incremental = "*" jinja2 = "*" tomli = {version = "*", markers = "python_version < \"3.11\""} [package.extras] dev = ["furo", "packaging", "sphinx (>=5)", "twisted"] [[package]] name = "typed-ast" version = "1.5.5" description = "a fork of Python 2 and 3 ast modules with type comment support" optional = false python-versions = ">=3.6" files = [ {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"}, {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"}, {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"}, {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"}, {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"}, {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"}, {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, ] [[package]] name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" optional = false python-versions = ">=3.7" files = [ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] [[package]] name = "urllib3" version = "2.0.7" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" version = "20.24.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} platformdirs = ">=3.9.1,<4" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.7" files = [ {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "2.0" python-versions = "^3.7" content-hash = "99e188820ea9b593c65e9e260af8dfe70c19299938075726d30d1a3cc566c9fe" cleo-2.1.0/pyproject.toml000066400000000000000000000067671451777547300154140ustar00rootroot00000000000000[build-system] requires = ["poetry-core>=1.1.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cleo" version = "2.1.0" description = "Cleo allows you to create beautiful and testable command-line interfaces." authors = [ "Sébastien Eustace " ] maintainers = [ "Branch Vincent ", "Bartosz Sokorski ", ] license = "MIT" readme = "README.md" packages = [{ include = "cleo", from = "src" }] include = [{ path = "tests", format = "sdist" }] repository = "https://github.com/python-poetry/cleo" keywords = ["cli", "commands"] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries", "Topic :: Software Development", ] [tool.poetry.dependencies] python = "^3.7" crashtest = "^0.4.1" rapidfuzz = "^3.0.0" [tool.poetry.group.dev.dependencies] mypy = [ { version = "^1.0", python = "<3.8" }, { version = "^1.5", python = ">=3.8" }, ] pre-commit = [ { version = "^2.0", python = "<3.8" }, { version = "^3.0", python = ">=3.8" }, ] pytest = "^7.1.2" pytest-cov = "^4.0" pytest-mock = "^3.8.2" towncrier = ">=22.12.0" [tool.poetry.group.doc.dependencies] Sphinx = "^5.2.3" sphinx-rtd-theme = "^1.0.0" [tool.ruff] fix = true unfixable = [ "ERA", # do not autoremove commented out code ] target-version = "py37" line-length = 88 extend-select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions "ERA", # flake8-eradicate/eradicate "I", # isort "N", # pep8-naming "PIE", # flake8-pie "PGH", # pygrep "RUF", # ruff checks "SIM", # flake8-simplify "TCH", # flake8-type-checking "TID", # flake8-tidy-imports "UP", # pyupgrade ] extend-exclude = [ "docs/*", "tests/fixtures/exceptions/*" ] [tool.ruff.flake8-tidy-imports] ban-relative-imports = "all" [tool.ruff.isort] force-single-line = true lines-between-types = 1 lines-after-imports = 2 known-first-party = ["cleo"] required-imports = ["from __future__ import annotations"] [tool.mypy] strict = true files = ["src", "tests"] pretty = true [tool.pytest.ini_options] addopts = "-q" testpaths = ["tests"] [tool.coverage.report] omit = [ "src/cleo/_compat.py", ] exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError" ] [tool.towncrier] package = "cleo" filename = "CHANGELOG.md" issue_format = "([#{issue}](https://github.com/python-poetry/cleo/pull/{issue}))" directory = "news/" title_format = "{version} ({project_date})" template = "news/news_template.jinja2" underlines = "-~^" start_string = "\n" [tool.towncrier.fragment.break] name = "Breaking Changes" showcontent = true [tool.towncrier.fragment.feat] name = "Features & Improvements" showcontent = true [tool.towncrier.fragment.bugfix] name = "Bug Fixes" showcontent = true [tool.towncrier.fragment.docs] name = "Documentation" showcontent = true [tool.towncrier.fragment.deps] name = "Dependencies" showcontent = true [tool.towncrier.fragment.removal] name = "Removals and Deprecations" showcontent = true [tool.towncrier.fragment.misc] name = "Miscellaneous" showcontent = true cleo-2.1.0/src/000077500000000000000000000000001451777547300132475ustar00rootroot00000000000000cleo-2.1.0/src/cleo/000077500000000000000000000000001451777547300141715ustar00rootroot00000000000000cleo-2.1.0/src/cleo/__init__.py000066400000000000000000000000731451777547300163020ustar00rootroot00000000000000from __future__ import annotations __version__ = "2.1.0" cleo-2.1.0/src/cleo/_compat.py000066400000000000000000000003671451777547300161730ustar00rootroot00000000000000from __future__ import annotations import shlex import subprocess import sys WINDOWS = sys.platform == "win32" def shell_quote(token: str) -> str: if WINDOWS: return subprocess.list2cmdline([token]) return shlex.quote(token) cleo-2.1.0/src/cleo/_utils.py000066400000000000000000000053101451777547300160410ustar00rootroot00000000000000from __future__ import annotations import math from dataclasses import dataclass from html.parser import HTMLParser from rapidfuzz.distance import Levenshtein class TagStripper(HTMLParser): def __init__(self) -> None: super().__init__(convert_charrefs=False) self.reset() self.fed: list[str] = [] def handle_data(self, d: str) -> None: self.fed.append(d) def handle_entityref(self, name: str) -> None: self.fed.append(f"&{name};") def handle_charref(self, name: str) -> None: self.fed.append(f"&#{name};") def get_data(self) -> str: return "".join(self.fed) def _strip(value: str) -> str: s = TagStripper() s.feed(value) s.close() return s.get_data() def strip_tags(value: str) -> str: while "<" in value and ">" in value: new_value = _strip(value) if value.count("<") == new_value.count("<"): break value = new_value return value def find_similar_names(name: str, names: list[str]) -> list[str]: """ Finds names similar to a given command name. """ threshold = 1e3 distance_by_name = {} for actual_name in names: # Get Levenshtein distance between the input and each command name distance = Levenshtein.distance(name, actual_name) is_similar = distance <= len(name) / 3 substring_index = actual_name.find(name) is_substring = substring_index != -1 if is_similar or is_substring: distance_by_name[actual_name] = ( distance, substring_index if is_substring else float("inf"), ) # Only keep results with a distance below the threshold distance_by_name = { key: value for key, value in distance_by_name.items() if value[0] < 2 * threshold } # Display results with shortest distance first return sorted(distance_by_name, key=lambda key: distance_by_name[key]) @dataclass class TimeFormat: threshold: int alias: str divisor: int | None = None def apply(self, secs: float) -> str: if self.divisor: return f"{math.ceil(secs / self.divisor)} {self.alias}" return self.alias _TIME_FORMATS: list[TimeFormat] = [ TimeFormat(1, "< 1 sec"), TimeFormat(2, "1 sec"), TimeFormat(60, "secs", 1), TimeFormat(61, "1 min"), TimeFormat(3600, "mins", 60), TimeFormat(5401, "1 hr"), TimeFormat(86400, "hrs", 3600), TimeFormat(129601, "1 day"), TimeFormat(604801, "days", 86400), ] def format_time(secs: float) -> str: time_format = next( (fmt for fmt in _TIME_FORMATS if secs < fmt.threshold), _TIME_FORMATS[-1] ) return time_format.apply(secs) cleo-2.1.0/src/cleo/application.py000066400000000000000000000477051451777547300170630ustar00rootroot00000000000000from __future__ import annotations import os import re import sys from contextlib import suppress from typing import TYPE_CHECKING from typing import cast from cleo.commands.completions_command import CompletionsCommand from cleo.commands.help_command import HelpCommand from cleo.commands.list_command import ListCommand from cleo.events.console_command_event import ConsoleCommandEvent from cleo.events.console_error_event import ConsoleErrorEvent from cleo.events.console_events import COMMAND from cleo.events.console_events import ERROR from cleo.events.console_events import TERMINATE from cleo.events.console_terminate_event import ConsoleTerminateEvent from cleo.exceptions import CleoCommandNotFoundError from cleo.exceptions import CleoError from cleo.exceptions import CleoLogicError from cleo.exceptions import CleoNamespaceNotFoundError from cleo.exceptions import CleoUserError from cleo.io.inputs.argument import Argument from cleo.io.inputs.argv_input import ArgvInput from cleo.io.inputs.definition import Definition from cleo.io.inputs.option import Option from cleo.io.io import IO from cleo.io.outputs.output import Verbosity from cleo.io.outputs.stream_output import StreamOutput from cleo.terminal import Terminal from cleo.ui.ui import UI if TYPE_CHECKING: from crashtest.solution_providers.solution_provider_repository import ( SolutionProviderRepository, ) from cleo.commands.command import Command from cleo.events.event_dispatcher import EventDispatcher from cleo.io.inputs.input import Input from cleo.io.outputs.output import Output from cleo.loaders.command_loader import CommandLoader class Application: """ An Application is the container for a collection of commands. This class is optimized for a standard CLI environment. Usage: >>> app = Application('myapp', '1.0 (stable)') >>> app.add(Command()) >>> app.run() """ def __init__(self, name: str = "console", version: str = "") -> None: self._name = name self._version = version self._display_name: str | None = None self._terminal = Terminal().size self._default_command = "list" self._single_command = False self._commands: dict[str, Command] = {} self._running_command: Command | None = None self._want_helps = False self._definition: Definition | None = None self._catch_exceptions = True self._auto_exit = True self._initialized = False self._ui: UI | None = None # TODO: signals support self._event_dispatcher: EventDispatcher | None = None self._command_loader: CommandLoader | None = None self._solution_provider_repository: SolutionProviderRepository | None = None @property def name(self) -> str: return self._name @property def display_name(self) -> str: if self._display_name is None: return re.sub(r"[\s\-_]+", " ", self._name).title() return self._display_name @property def version(self) -> str: return self._version @property def long_version(self) -> str: if self._name: if self._version: return f"{self.display_name} (version {self._version})" return f"{self.display_name}" return "Console application" @property def definition(self) -> Definition: if self._definition is None: self._definition = self._default_definition if self._single_command: definition = self._definition definition.set_arguments([]) return definition return self._definition @property def default_commands(self) -> list[Command]: return [HelpCommand(), ListCommand(), CompletionsCommand()] @property def help(self) -> str: return self.long_version @property def ui(self) -> UI: if self._ui is None: self._ui = self._get_default_ui() return self._ui @property def event_dispatcher(self) -> EventDispatcher | None: return self._event_dispatcher def set_event_dispatcher(self, event_dispatcher: EventDispatcher) -> None: self._event_dispatcher = event_dispatcher def set_name(self, name: str) -> None: self._name = name def set_display_name(self, display_name: str) -> None: self._display_name = display_name def set_version(self, version: str) -> None: self._version = version def set_ui(self, ui: UI) -> None: self._ui = ui def set_command_loader(self, command_loader: CommandLoader) -> None: self._command_loader = command_loader def auto_exits(self, auto_exits: bool = True) -> None: self._auto_exit = auto_exits def is_auto_exit_enabled(self) -> bool: return self._auto_exit def are_exceptions_caught(self) -> bool: return self._catch_exceptions def catch_exceptions(self, catch_exceptions: bool = True) -> None: self._catch_exceptions = catch_exceptions def is_single_command(self) -> bool: return self._single_command def set_solution_provider_repository( self, solution_provider_repository: SolutionProviderRepository ) -> None: self._solution_provider_repository = solution_provider_repository def add(self, command: Command) -> Command | None: self._init() command.set_application(self) if not command.enabled: command.set_application() return None if not command.name: raise CleoLogicError( f'The command "{command.__class__.__name__}" cannot have an empty name' ) self._commands[command.name] = command for alias in command.aliases: self._commands[alias] = command return command def get(self, name: str) -> Command: self._init() if not self.has(name): raise CleoCommandNotFoundError(name) if name not in self._commands: # The command was registered in a different name in the command loader raise CleoCommandNotFoundError(name) command = self._commands[name] if self._want_helps: self._want_helps = False help_command: HelpCommand = cast(HelpCommand, self.get("help")) help_command.set_command(command) return help_command return command def has(self, name: str) -> bool: self._init() if name in self._commands: return True if not self._command_loader: return False return bool( self._command_loader.has(name) and self.add(self._command_loader.get(name)) ) def get_namespaces(self) -> list[str]: namespaces = [] seen = set() for command in self.all().values(): if command.hidden or not command.name: continue for namespace in self._extract_all_namespaces(command.name): if namespace in seen: continue namespaces.append(namespace) seen.add(namespace) for alias in command.aliases: for namespace in self._extract_all_namespaces(alias): if namespace in seen: continue namespaces.append(namespace) seen.add(namespace) return namespaces def find_namespace(self, namespace: str) -> str: all_namespaces = self.get_namespaces() if namespace not in all_namespaces: raise CleoNamespaceNotFoundError(namespace, all_namespaces) return namespace def find(self, name: str) -> Command: self._init() if self.has(name): return self.get(name) all_commands = [] if self._command_loader: all_commands += self._command_loader.names all_commands += [ name for name, command in self._commands.items() if not command.hidden ] raise CleoCommandNotFoundError(name, all_commands) def all(self, namespace: str | None = None) -> dict[str, Command]: self._init() if namespace is None: commands = self._commands.copy() if not self._command_loader: return commands for name in self._command_loader.names: if name not in commands and self.has(name): commands[name] = self.get(name) return commands commands = {} for name, command in self._commands.items(): if namespace == self.extract_namespace(name, name.count(" ") + 1): commands[name] = command if self._command_loader: for name in self._command_loader.names: if ( name not in commands and namespace == self.extract_namespace(name, name.count(" ") + 1) and self.has(name) ): commands[name] = self.get(name) return commands def run( self, input: Input | None = None, output: Output | None = None, error_output: Output | None = None, ) -> int: try: io = self.create_io(input, output, error_output) self._configure_io(io) try: exit_code = self._run(io) except BrokenPipeError: # If we are piped to another process, it may close early and send a # SIGPIPE: https://docs.python.org/3/library/signal.html#note-on-sigpipe devnull = os.open(os.devnull, os.O_WRONLY) os.dup2(devnull, sys.stdout.fileno()) exit_code = 0 except Exception as e: if not self._catch_exceptions: raise self.render_error(e, io) exit_code = 1 # TODO: Custom error exit codes except KeyboardInterrupt: exit_code = 1 if self._auto_exit: sys.exit(exit_code) return exit_code def _run(self, io: IO) -> int: if io.input.has_parameter_option(["--version", "-V"], True): io.write_line(self.long_version) return 0 definition = self.definition input_definition = Definition() for argument in definition.arguments: if argument.name == "command": argument = Argument( "command", required=True, is_list=True, description=definition.argument("command").description, ) input_definition.add_argument(argument) input_definition.set_options(definition.options) # Errors must be ignored, full binding/validation # happens later when the command is known. with suppress(CleoError): # Makes ArgvInput.first_argument() able to # distinguish an option from an argument. io.input.bind(input_definition) name = self._get_command_name(io) if io.input.has_parameter_option(["--help", "-h"], True): if not name: name = "help" io.set_input(ArgvInput(["console", "help", self._default_command])) else: self._want_helps = True if not name: name = self._default_command definition = self.definition arguments = definition.arguments if not definition.has_argument("command"): arguments.append( Argument( "command", required=False, description=definition.argument("command").description, default=name, ) ) definition.set_arguments(arguments) self._running_command = None command = self.find(name) self._running_command = command if " " in name and isinstance(io.input, ArgvInput): # If the command is namespaced we rearrange # the input to parse it as a single argument argv = io.input._tokens[:] if io.input.script_name is not None: argv.insert(0, io.input.script_name) namespace = name.split(" ")[0] index = None for i, arg in enumerate(argv): if arg == namespace and i > 0: argv[i] = name index = i break if index is not None: del argv[index + 1 : index + 1 + name.count(" ")] stream = io.input.stream interactive = io.input.is_interactive() io.set_input(ArgvInput(argv)) io.input.set_stream(stream) io.input.interactive(interactive) exit_code = self._run_command(command, io) self._running_command = None return exit_code def _run_command(self, command: Command, io: IO) -> int: if self._event_dispatcher is None: return command.run(io) # Bind before the console.command event, # so the listeners have access to the arguments and options try: command.merge_application_definition() io.input.bind(command.definition) except CleoError: # Ignore invalid option/arguments for now, # to allow the listeners to customize the definition pass command_event = ConsoleCommandEvent(command, io) error = None try: self._event_dispatcher.dispatch(command_event, COMMAND) if command_event.command_should_run(): exit_code = command.run(io) else: exit_code = ConsoleCommandEvent.RETURN_CODE_DISABLED except Exception as e: error_event = ConsoleErrorEvent(command, io, e) self._event_dispatcher.dispatch(error_event, ERROR) error = error_event.error exit_code = error_event.exit_code if exit_code == 0: error = None terminate_event = ConsoleTerminateEvent(command, io, exit_code) self._event_dispatcher.dispatch(terminate_event, TERMINATE) if error is not None: raise error return terminate_event.exit_code def create_io( self, input: Input | None = None, output: Output | None = None, error_output: Output | None = None, ) -> IO: if input is None: input = ArgvInput() input.set_stream(sys.stdin) if output is None: output = StreamOutput(sys.stdout) if error_output is None: error_output = StreamOutput(sys.stderr) return IO(input, output, error_output) def render_error(self, error: Exception, io: IO) -> None: from cleo.ui.exception_trace import ExceptionTrace trace = ExceptionTrace( error, solution_provider_repository=self._solution_provider_repository ) simple = not io.is_verbose() or isinstance(error, CleoUserError) trace.render(io.error_output, simple) def _configure_io(self, io: IO) -> None: if io.input.has_parameter_option("--ansi", True): io.decorated(True) elif io.input.has_parameter_option("--no-ansi", True): io.decorated(False) if io.input.has_parameter_option(["--no-interaction", "-n"], True) or ( io.input._interactive is None and io.input.stream and not io.input.stream.isatty() ): io.interactive(False) shell_verbosity = int(os.getenv("SHELL_VERBOSITY", 0)) if shell_verbosity == -1: io.set_verbosity(Verbosity.QUIET) elif shell_verbosity == 1: io.set_verbosity(Verbosity.VERBOSE) elif shell_verbosity == 2: io.set_verbosity(Verbosity.VERY_VERBOSE) elif shell_verbosity == 3: io.set_verbosity(Verbosity.DEBUG) else: shell_verbosity = 0 if io.input.has_parameter_option(["--quiet", "-q"], True): io.set_verbosity(Verbosity.QUIET) shell_verbosity = -1 else: if io.input.has_parameter_option("-vvv", True): io.set_verbosity(Verbosity.DEBUG) shell_verbosity = 3 elif io.input.has_parameter_option("-vv", True): io.set_verbosity(Verbosity.VERY_VERBOSE) shell_verbosity = 2 elif io.input.has_parameter_option( "-v", True ) or io.input.has_parameter_option("--verbose", only_params=True): io.set_verbosity(Verbosity.VERBOSE) shell_verbosity = 1 if shell_verbosity == -1: io.interactive(False) @property def _default_definition(self) -> Definition: return Definition( [ Argument( "command", required=True, description="The command to execute.", ), Option( "--help", "-h", flag=True, description=( "Display help for the given command. " "When no command is given display help for " f"the {self._default_command} command." ), ), Option( "--quiet", "-q", flag=True, description="Do not output any message." ), Option( "--verbose", "-v|vv|vvv", flag=True, description=( "Increase the verbosity of messages: " "1 for normal output, 2 for more verbose " "output and 3 for debug." ), ), Option( "--version", "-V", flag=True, description="Display this application version.", ), Option("--ansi", flag=True, description="Force ANSI output."), Option("--no-ansi", flag=True, description="Disable ANSI output."), Option( "--no-interaction", "-n", flag=True, description="Do not ask any interactive question.", ), ] ) def _get_command_name(self, io: IO) -> str | None: if self._single_command: return self._default_command if "command" in io.input.arguments and io.input.argument("command"): candidates: list[str] = [] for command_part in io.input.argument("command"): if candidates: candidates.append(candidates[-1] + " " + command_part) else: candidates.append(command_part) for candidate in reversed(candidates): if self.has(candidate): return candidate return io.input.first_argument def extract_namespace(self, name: str, limit: int | None = None) -> str: parts = name.split(" ")[:-1] return " ".join(parts[:limit]) def _get_default_ui(self) -> UI: from cleo.ui.progress_bar import ProgressBar io = self.create_io() return UI([ProgressBar(io)]) def _extract_all_namespaces(self, name: str) -> list[str]: parts = name.split(" ")[:-1] namespaces: list[str] = [] for part in parts: namespaces.append(namespaces[-1] + " " + part if namespaces else part) return namespaces def _init(self) -> None: if self._initialized: return self._initialized = True for command in self.default_commands: self.add(command) cleo-2.1.0/src/cleo/color.py000066400000000000000000000100731451777547300156620ustar00rootroot00000000000000from __future__ import annotations import os from typing import ClassVar from cleo.exceptions import CleoValueError class Color: COLORS: ClassVar[dict[str, tuple[int, int]]] = { "black": (30, 40), "red": (31, 41), "green": (32, 42), "yellow": (33, 43), "blue": (34, 44), "magenta": (35, 45), "cyan": (36, 46), "light_gray": (37, 47), "default": (39, 49), "dark_gray": (90, 100), "light_red": (91, 101), "light_green": (92, 102), "light_yellow": (93, 103), "light_blue": (94, 104), "light_magenta": (95, 105), "light_cyan": (96, 106), "white": (97, 107), } AVAILABLE_OPTIONS: ClassVar[dict[str, dict[str, int]]] = { "bold": {"set": 1, "unset": 22}, "dark": {"set": 2, "unset": 22}, "italic": {"set": 3, "unset": 23}, "underline": {"set": 4, "unset": 24}, "blink": {"set": 5, "unset": 25}, "reverse": {"set": 7, "unset": 27}, "conceal": {"set": 8, "unset": 28}, } def __init__( self, foreground: str = "", background: str = "", options: list[str] | None = None, ) -> None: self._foreground = self._parse_color(foreground, False) self._background = self._parse_color(background, True) self._options = {} for option in options or []: if option not in self.AVAILABLE_OPTIONS: raise ValueError( f'"{option}" is not a valid color option. ' f"It must be one of {', '.join(self.AVAILABLE_OPTIONS)}" ) self._options[option] = self.AVAILABLE_OPTIONS[option] def apply(self, text: str) -> str: return self.set() + text + self.unset() def set(self) -> str: codes = [] if self._foreground: codes.append(self._foreground) if self._background: codes.append(self._background) for option in self._options.values(): codes.append(str(option["set"])) if not codes: return "" return f"\033[{';'.join(codes)}m" def unset(self) -> str: codes = [] if self._foreground: codes.append("39") if self._background: codes.append("49") for option in self._options.values(): codes.append(str(option["unset"])) if not codes: return "" return f"\033[{';'.join(codes)}m" def _parse_color(self, color: str, background: bool) -> str: if not color: return "" if color.startswith("#"): color = color[1:] if len(color) == 3: color = color[0] * 2 + color[1] * 2 + color[2] * 2 if len(color) != 6: raise CleoValueError(f'"{color}" is an invalid color') return ("4" if background else "3") + self._convert_hex_color_to_ansi( int(color, 16) ) if color not in self.COLORS: raise CleoValueError( f'"{color}" is an invalid color.' f" It must be one of {', '.join(self.COLORS)}" ) return str(self.COLORS[color][int(background)]) def _convert_hex_color_to_ansi(self, color: int) -> str: r = (color >> 16) & 255 g = (color >> 8) & 255 b = color & 255 if os.getenv("COLORTERM") != "truecolor": return str(self._degrade_hex_color_to_ansi(r, g, b)) return f"8;2;{r};{g};{b}" def _degrade_hex_color_to_ansi(self, r: int, g: int, b: int) -> int: if round(self._get_saturation(r, g, b) / 50) == 0: return 0 return (round(b / 255) << 2) | (round(g / 255) << 1) | round(r / 255) def _get_saturation(self, r: int, g: int, b: int) -> int: r_float = r / 255 g_float = g / 255 b_float = b / 255 v = max(r_float, g_float, b_float) diff = v - min(r_float, g_float, b_float) if diff == 0: return 0 return int(diff * 100 / v) cleo-2.1.0/src/cleo/commands/000077500000000000000000000000001451777547300157725ustar00rootroot00000000000000cleo-2.1.0/src/cleo/commands/__init__.py000066400000000000000000000000001451777547300200710ustar00rootroot00000000000000cleo-2.1.0/src/cleo/commands/base_command.py000066400000000000000000000073401451777547300207600ustar00rootroot00000000000000from __future__ import annotations import inspect from typing import TYPE_CHECKING from typing import ClassVar from cleo.exceptions import CleoError from cleo.io.inputs.definition import Definition if TYPE_CHECKING: from cleo.application import Application from cleo.io.io import IO class BaseCommand: name: str | None = None description = "" help = "" enabled = True hidden = False usages: ClassVar[list[str]] = [] def __init__(self) -> None: self._definition = Definition() self._full_definition: Definition | None = None self._application: Application | None = None self._ignore_validation_errors = False self._synopsis: dict[str, str] = {} self.configure() for i, usage in enumerate(self.usages): if self.name and not usage.startswith(self.name): self.usages[i] = f"{self.name} {usage}" @property def application(self) -> Application | None: return self._application @property def definition(self) -> Definition: if self._full_definition is not None: return self._full_definition return self._definition @property def processed_help(self) -> str: help_text = self.help if not self.help: help_text = self.description is_single_command = self._application and self._application.is_single_command() if self._application: current_script = self._application.name else: current_script = inspect.stack()[-1][1] return help_text.format( command_name=self.name, command_full_name=current_script if is_single_command else f"{current_script} {self.name}", script_name=current_script, ) def ignore_validation_errors(self) -> None: self._ignore_validation_errors = True def set_application(self, application: Application | None = None) -> None: self._application = application self._full_definition = None def configure(self) -> None: """ Configures the current command. """ def execute(self, io: IO) -> int: raise NotImplementedError def interact(self, io: IO) -> None: """ Interacts with the user. """ def initialize(self, io: IO) -> None: pass def run(self, io: IO) -> int: self.merge_application_definition() try: io.input.bind(self.definition) except CleoError: if not self._ignore_validation_errors: raise self.initialize(io) if io.is_interactive(): self.interact(io) if io.input.has_argument("command") and io.input.argument("command") is None: io.input.set_argument("command", self.name) io.input.validate() return self.execute(io) or 0 def merge_application_definition(self, merge_args: bool = True) -> None: if self._application is None: return self._full_definition = Definition() self._full_definition.add_options(self._definition.options) self._full_definition.add_options(self._application.definition.options) if merge_args: self._full_definition.set_arguments(self._application.definition.arguments) self._full_definition.add_arguments(self._definition.arguments) else: self._full_definition.set_arguments(self._definition.arguments) def synopsis(self, short: bool = False) -> str: key = "short" if short else "long" if key not in self._synopsis: self._synopsis[key] = f"{self.name} {self.definition.synopsis(short)}" return self._synopsis[key] cleo-2.1.0/src/cleo/commands/command.py000066400000000000000000000216601451777547300177670ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from typing import ContextManager from typing import cast from cleo.commands.base_command import BaseCommand from cleo.formatters.style import Style from cleo.io.inputs.string_input import StringInput from cleo.io.null_io import NullIO from cleo.io.outputs.output import Verbosity from cleo.ui.table_separator import TableSeparator if TYPE_CHECKING: import sys if sys.version_info >= (3, 8): from typing import Literal else: from typing_extensions import Literal from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option from cleo.io.io import IO from cleo.ui.progress_bar import ProgressBar from cleo.ui.progress_indicator import ProgressIndicator from cleo.ui.question import Question from cleo.ui.table import Rows from cleo.ui.table import Table class Command(BaseCommand): arguments: ClassVar[list[Argument]] = [] options: ClassVar[list[Option]] = [] aliases: ClassVar[list[str]] = [] usages: ClassVar[list[str]] = [] commands: ClassVar[list[BaseCommand]] = [] def __init__(self) -> None: self._io: IO = None # type: ignore[assignment] super().__init__() @property def io(self) -> IO: return self._io def configure(self) -> None: for argument in self.arguments: self._definition.add_argument(argument) for option in self.options: self._definition.add_option(option) def execute(self, io: IO) -> int: self._io = io try: return self.handle() except KeyboardInterrupt: return 1 def handle(self) -> int: """ Execute the command. """ raise NotImplementedError def call(self, name: str, args: str | None = None) -> int: """ Call another command. """ assert self.application is not None command = self.application.get(name) return self.application._run_command( command, self._io.with_input(StringInput(args or "")) ) def call_silent(self, name: str, args: str | None = None) -> int: """ Call another command silently. """ assert self.application is not None command = self.application.get(name) return self.application._run_command(command, NullIO(StringInput(args or ""))) def argument(self, name: str) -> Any: """ Get the value of a command argument. """ return self._io.input.argument(name) def option(self, name: str) -> Any: """ Get the value of a command option. """ return self._io.input.option(name) def confirm( self, question: str, default: bool = False, true_answer_regex: str = r"(?i)^y" ) -> bool: """ Confirm a question with the user. """ from cleo.ui.confirmation_question import ConfirmationQuestion confirmation = ConfirmationQuestion( question, default=default, true_answer_regex=true_answer_regex ) return cast(bool, confirmation.ask(self._io)) def ask(self, question: str | Question, default: Any | None = None) -> Any: """ Prompt the user for input. """ from cleo.ui.question import Question if not isinstance(question, Question): question = Question(question, default=default) return question.ask(self._io) def secret(self, question: str | Question, default: Any | None = None) -> Any: """ Prompt the user for input but hide the answer from the console. """ from cleo.ui.question import Question if not isinstance(question, Question): question = Question(question, default=default) question.hide() return question.ask(self._io) def choice( self, question: str, choices: list[str], default: Any | None = None, attempts: int | None = None, multiple: bool = False, ) -> Any: """ Give the user a single choice from an list of answers. """ from cleo.ui.choice_question import ChoiceQuestion choice = ChoiceQuestion(question, choices, default) choice.set_max_attempts(attempts) choice.set_multi_select(multiple) return choice.ask(self._io) def create_question( self, question: str, type: Literal["choice", "confirmation"] | None = None, **kwargs: Any, ) -> Question: """ Returns a Question of specified type. """ from cleo.ui.choice_question import ChoiceQuestion from cleo.ui.confirmation_question import ConfirmationQuestion from cleo.ui.question import Question if type == "confirmation": return ConfirmationQuestion(question, **kwargs) if type == "choice": return ChoiceQuestion(question, **kwargs) return Question(question, **kwargs) def table( self, header: str | None = None, rows: Rows | None = None, style: str | None = None, ) -> Table: """ Return a Table instance. """ from cleo.ui.table import Table table = Table(self._io, style=style) if header: table.set_headers([header]) if rows: table.set_rows(rows) return table def table_separator(self) -> TableSeparator: """ Return a TableSeparator instance. """ return TableSeparator() def render_table(self, headers: str, rows: Rows, style: str | None = None) -> None: """ Format input to textual table. """ table = self.table(headers, rows, style) table.render() def write(self, text: str, style: str | None = None) -> None: """ Writes a string without a new line. Useful if you want to use overwrite(). """ styled = f"<{style}>{text}" if style else text self._io.write(styled) def line( self, text: str, style: str | None = None, verbosity: Verbosity = Verbosity.NORMAL, ) -> None: """ Write a string as information output. """ styled = f"<{style}>{text}" if style else text self._io.write_line(styled, verbosity=verbosity) def line_error( self, text: str, style: str | None = None, verbosity: Verbosity = Verbosity.NORMAL, ) -> None: """ Write a string as information output to stderr. """ styled = f"<{style}>{text}" if style else text self._io.write_error_line(styled, verbosity) def info(self, text: str) -> None: """ Write a string as information output. :param text: The line to write :type text: str """ self.line(text, "info") def comment(self, text: str) -> None: """ Write a string as comment output. :param text: The line to write :type text: str """ self.line(text, "comment") def question(self, text: str) -> None: """ Write a string as question output. :param text: The line to write :type text: str """ self.line(text, "question") def progress_bar(self, max: int = 0) -> ProgressBar: """ Creates a new progress bar """ from cleo.ui.progress_bar import ProgressBar return ProgressBar(self._io, max=max) def progress_indicator( self, fmt: str | None = None, interval: int = 100, values: list[str] | None = None, ) -> ProgressIndicator: """ Creates a new progress indicator. """ from cleo.ui.progress_indicator import ProgressIndicator return ProgressIndicator(self.io, fmt, interval, values) def spin( self, start_message: str, end_message: str, fmt: str | None = None, interval: int = 100, values: list[str] | None = None, ) -> ContextManager[ProgressIndicator]: """ Automatically spin a progress indicator. """ spinner = self.progress_indicator(fmt, interval, values) return spinner.auto(start_message, end_message) def add_style( self, name: str, fg: str | None = None, bg: str | None = None, options: list[str] | None = None, ) -> None: """ Adds a new style """ style = Style(fg, bg, options) self._io.output.formatter.set_style(name, style) self._io.error_output.formatter.set_style(name, style) def overwrite(self, text: str) -> None: """ Overwrites the current line. It will not add a new line so use line('') if necessary. """ self._io.overwrite(text) cleo-2.1.0/src/cleo/commands/completions/000077500000000000000000000000001451777547300203265ustar00rootroot00000000000000cleo-2.1.0/src/cleo/commands/completions/__init__.py000066400000000000000000000000001451777547300224250ustar00rootroot00000000000000cleo-2.1.0/src/cleo/commands/completions/templates.py000066400000000000000000000042751451777547300227060ustar00rootroot00000000000000from __future__ import annotations BASH_TEMPLATE = """\ %(function)s() { local cur script coms opts com COMPREPLY=() _get_comp_words_by_ref -n : cur words # for an alias, get the real script behind it if [[ $(type -t ${words[0]}) == "alias" ]]; then script=$(alias ${words[0]} | sed -E "s/alias ${words[0]}='(.*)'/\\1/") else script=${words[0]} fi # lookup for command for word in ${words[@]:1}; do if [[ $word != -* ]]; then com=$word break fi done # completing for an option if [[ ${cur} == --* ]] ; then opts="%(opts)s" case "$com" in %(cmds_opts)s esac COMPREPLY=($(compgen -W "${opts}" -- ${cur})) __ltrim_colon_completions "$cur" return 0; fi # completing for a command if [[ $cur == $com ]]; then coms="%(cmds)s" COMPREPLY=($(compgen -W "${coms}" -- ${cur})) __ltrim_colon_completions "$cur" return 0 fi } %(compdefs)s""" ZSH_TEMPLATE = """\ #compdef %(script_name)s %(function)s() { local state com cur local -a opts local -a coms cur=${words[${#words[@]}]} # lookup for command for word in ${words[@]:1}; do if [[ $word != -* ]]; then com=$word break fi done if [[ ${cur} == --* ]]; then state="option" opts+=(%(opts)s) elif [[ $cur == $com ]]; then state="command" coms+=(%(cmds)s) fi case $state in (command) _describe 'command' coms ;; (option) case "$com" in %(cmds_opts)s esac _describe 'option' opts ;; *) # fallback to file completion _arguments '*:file:_files' esac } %(function)s "$@" %(compdefs)s""" FISH_TEMPLATE = """\ function __fish%(function)s_no_subcommand for i in (commandline -opc) if contains -- $i %(cmds_names)s return 1 end end return 0 end # global options %(opts)s # commands %(cmds)s # command options %(cmds_opts)s""" TEMPLATES = {"bash": BASH_TEMPLATE, "zsh": ZSH_TEMPLATE, "fish": FISH_TEMPLATE} cleo-2.1.0/src/cleo/commands/completions_command.py000066400000000000000000000315311451777547300224010ustar00rootroot00000000000000from __future__ import annotations import hashlib import inspect import os import posixpath import re import subprocess from pathlib import Path from typing import TYPE_CHECKING from typing import ClassVar from typing import cast from cleo import helpers from cleo._compat import shell_quote from cleo.commands.command import Command from cleo.commands.completions.templates import TEMPLATES from cleo.exceptions import CleoRuntimeError if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class CompletionsCommand(Command): name = "completions" description = "Generate completion scripts for your shell." arguments: ClassVar[list[Argument]] = [ helpers.argument( "shell", "The shell to generate the scripts for.", optional=True ) ] options: ClassVar[list[Option]] = [ helpers.option( "alias", None, "Alias for the current command.", flag=False, multiple=True ) ] SUPPORTED_SHELLS = ("bash", "zsh", "fish") hidden = True help = """ One can generate a completion script for `{script_name}` \ that is compatible with a given shell. The script is output on \ `stdout` allowing one to re-direct \ the output to the file of their choosing. Where you place the file will \ depend on which shell, and which operating system you are using. Your \ particular configuration may also determine where these scripts need \ to be placed. Here are some common set ups for the three supported shells under \ Unix and similar operating systems (such as GNU/Linux). BASH: Completion files are commonly stored in `/etc/bash_completion.d/` Run the command: `{script_name} {command_name} bash >\ /etc/bash_completion.d/{script_name}.bash-completion` This installs the completion script. You may have to log out and log \ back in to your shell session for the changes to take effect. FISH: Fish completion files are commonly stored in\ `$HOME/.config/fish/completions` Run the command: `{script_name} {command_name} fish > \ ~/.config/fish/completions/{script_name}.fish` This installs the completion script. You may have to log out and log \ back in to your shell session for the changes to take effect. ZSH: ZSH completions are commonly stored in any directory listed in your \ `$fpath` variable. To use these completions, you must either add the \ generated script to one of those directories, or add your own \ to this list. Adding a custom directory is often the safest best if you're unsure \ of which directory to use. First create the directory, for this \ example we'll create a hidden directory inside our `$HOME` directory `mkdir ~/.zfunc` Then add the following lines to your `.zshrc` \ just before `compinit` `fpath+=~/.zfunc` Now you can install the completions script using the following command `{script_name} {command_name} zsh > ~/.zfunc/_{script_name}` You must then either log out and log back in, or simply run `exec zsh` For the new completions to take affect. CUSTOM LOCATIONS: Alternatively, you could save these files to the place of your choosing, \ such as a custom directory inside your $HOME. Doing so will require you \ to add the proper directives, such as `source`ing inside your login \ script. Consult your shells documentation for how to add such directives. """ def handle(self) -> int: shell = self.argument("shell") if not shell: shell = self.get_shell_type() if shell not in self.SUPPORTED_SHELLS: raise ValueError( f"[shell] argument must be one of {', '.join(self.SUPPORTED_SHELLS)}" ) self.line(self.render(shell)) return 0 def render(self, shell: str) -> str: if shell == "bash": return self.render_bash() if shell == "zsh": return self.render_zsh() if shell == "fish": return self.render_fish() raise RuntimeError(f"Unrecognized shell: {shell}") @staticmethod def _get_prog_name_from_stack() -> str: package_name = "" frame = inspect.currentframe() f_back = frame.f_back if frame is not None else None f_globals = f_back.f_globals if f_back is not None else None # break reference cycle # https://docs.python.org/3/library/inspect.html#the-interpreter-stack del frame if f_globals is not None: package_name = cast(str, f_globals.get("__name__")) if package_name == "__main__": package_name = cast(str, f_globals.get("__package__")) if package_name: package_name = package_name.partition(".")[0] if not package_name: raise CleoRuntimeError("Can not determine package name") return package_name def _get_script_name_and_path(self) -> tuple[str, str]: script_name = self._io.input.script_name or self._get_prog_name_from_stack() script_path = posixpath.realpath(script_name) script_name = Path(script_path).name return script_name, script_path def render_bash(self) -> str: script_name, script_path = self._get_script_name_and_path() aliases = [script_name, script_path, *self.option("alias")] function = self._generate_function_name(script_name, script_path) # Global options assert self.application opts = [ f"--{opt.name}" for opt in sorted(self.application.definition.options, key=lambda o: o.name) ] # Commands + options cmds = [] cmds_opts = [] for cmd in sorted(self.application.all().values(), key=lambda c: c.name or ""): if cmd.hidden or not (cmd.enabled and cmd.name): continue command_name = shell_quote(cmd.name) if " " in cmd.name else cmd.name cmds.append(command_name) options = " ".join( f"--{opt.name}".replace(":", "\\:") for opt in sorted(cmd.definition.options, key=lambda o: o.name) ) cmds_opts += [ f" ({command_name})", f' opts="${{opts}} {options}"', " ;;", "", # newline ] return TEMPLATES["bash"] % { "script_name": script_name, "function": function, "opts": " ".join(opts), "cmds": " ".join(cmds), "cmds_opts": "\n".join(cmds_opts[:-1]), # trim trailing newline "compdefs": "\n".join( f"complete -o default -F {function} {alias}" for alias in aliases ), } def render_zsh(self) -> str: script_name, script_path = self._get_script_name_and_path() aliases = [script_path, *self.option("alias")] function = self._generate_function_name(script_name, script_path) def sanitize(s: str) -> str: return self._io.output.formatter.remove_format(s) # Global options assert self.application opts = [ self._zsh_describe(f"--{opt.name}", sanitize(opt.description)) for opt in sorted(self.application.definition.options, key=lambda o: o.name) ] # Commands + options cmds = [] cmds_opts = [] for cmd in sorted(self.application.all().values(), key=lambda c: c.name or ""): if cmd.hidden or not (cmd.enabled and cmd.name): continue command_name = shell_quote(cmd.name) if " " in cmd.name else cmd.name cmds.append(self._zsh_describe(command_name, sanitize(cmd.description))) options = " ".join( self._zsh_describe(f"--{opt.name}", sanitize(opt.description)) for opt in sorted(cmd.definition.options, key=lambda o: o.name) ) cmds_opts += [ f" ({command_name})", f" opts+=({options})", " ;;", "", # newline ] return TEMPLATES["zsh"] % { "script_name": script_name, "function": function, "opts": " ".join(opts), "cmds": " ".join(cmds), "cmds_opts": "\n".join(cmds_opts[:-1]), # trim trailing newline "compdefs": "\n".join(f"compdef {function} {alias}" for alias in aliases), } def render_fish(self) -> str: script_name, script_path = self._get_script_name_and_path() function = self._generate_function_name(script_name, script_path) def sanitize(s: str) -> str: return self._io.output.formatter.remove_format(s).replace("'", "\\'") # Global options assert self.application opts = [ f"complete -c {script_name} -n '__fish{function}_no_subcommand' " f"-l {opt.name} -d '{sanitize(opt.description)}'" for opt in sorted(self.application.definition.options, key=lambda o: o.name) ] # Commands + options cmds = [] cmds_opts = [] namespaces = set() for cmd in sorted(self.application.all().values(), key=lambda c: c.name or ""): if cmd.hidden or not cmd.enabled or not cmd.name: continue cmd_path = cmd.name.split(" ") namespace = cmd_path[0] cmd_name = cmd_path[-1] if " " in cmd.name else cmd.name # We either have a command like `poetry add` or a nested (namespaced) # command like `poetry cache clear`. if len(cmd_path) == 1: cmds.append( f"complete -c {script_name} -f -n '__fish{function}_no_subcommand' " f"-a {cmd_name} -d '{sanitize(cmd.description)}'" ) condition = f"__fish_seen_subcommand_from {cmd_name}" else: # Complete the namespace first if namespace not in namespaces: cmds.append( f"complete -c {script_name} -f -n " f"'__fish{function}_no_subcommand' -a {namespace}" ) # Now complete the command subcmds = [ name.split(" ")[-1] for name in self.application.all(namespace) ] cmds.append( f"complete -c {script_name} -f -n '__fish_seen_subcommand_from " f"{namespace}; and not __fish_seen_subcommand_from {' '.join(subcmds)}' " f"-a {cmd_name} -d '{sanitize(cmd.description)}'" ) condition = ( f"__fish_seen_subcommand_from {namespace}; " f"and __fish_seen_subcommand_from {cmd_name}" ) cmds_opts += [ f"# {cmd.name}", *[ f"complete -c {script_name} " f"-n '{condition}' " f"-l {opt.name} -d '{sanitize(opt.description)}'" for opt in sorted(cmd.definition.options, key=lambda o: o.name) ], "", # newline ] namespaces.add(namespace) return TEMPLATES["fish"] % { "script_name": script_name, "function": function, "opts": "\n".join(opts), "cmds": "\n".join(cmds), "cmds_opts": "\n".join(cmds_opts[:-1]), # trim trailing newline "cmds_names": " ".join(sorted(namespaces)), } def get_shell_type(self) -> str: shell = os.getenv("SHELL") if not shell: raise RuntimeError( "Could not read SHELL environment variable. " "Please specify your shell type by passing it as the first argument." ) return Path(shell).name def _generate_function_name(self, script_name: str, script_path: str) -> str: sanitized_name = self._sanitize_for_function_name(script_name) md5_hash = hashlib.md5(script_path.encode()).hexdigest()[:16] return f"_{sanitized_name}_{md5_hash}_complete" def _sanitize_for_function_name(self, name: str) -> str: name = name.replace("-", "_") return re.sub(r"[^A-Za-z0-9_]+", "", name) def _zsh_describe(self, value: str, description: str | None = None) -> str: value = '"' + value.replace(":", "\\:") if description: description = re.sub( r"([\"'#&;`|*?~<>^()\[\]{}$\\\x0A\xFF])", r"\\\1", description ) value += ":" + subprocess.list2cmdline([description]).strip('"') value += '"' return value cleo-2.1.0/src/cleo/commands/help_command.py000066400000000000000000000024041451777547300207720ustar00rootroot00000000000000from __future__ import annotations from typing import ClassVar from cleo.commands.command import Command from cleo.io.inputs.argument import Argument class HelpCommand(Command): name = "help" description = "Displays help for a command." arguments: ClassVar[list[Argument]] = [ Argument( "command_name", required=False, description="The command name", default="help", ) ] help = """\ The {command_name} command displays help for a given command: {command_full_name} list To display the list of available commands, please use the list command. """ _command = None def set_command(self, command: Command) -> None: self._command = command def configure(self) -> None: self.ignore_validation_errors() super().configure() def handle(self) -> int: from cleo.descriptors.text_descriptor import TextDescriptor if self._command is None: assert self._application is not None self._command = self._application.find(self.argument("command_name")) self.line("") TextDescriptor().describe(self._io, self._command) self._command = None return 0 cleo-2.1.0/src/cleo/commands/list_command.py000066400000000000000000000014721451777547300210210ustar00rootroot00000000000000from __future__ import annotations from typing import ClassVar from cleo.commands.command import Command from cleo.io.inputs.argument import Argument class ListCommand(Command): name = "list" description = "Lists commands." help = """\ The {command_name} command lists all commands: {command_full_name} You can also display the commands for a specific namespace: {command_full_name} test """ arguments: ClassVar[list[Argument]] = [ Argument("namespace", required=False, description="The namespace name") ] def handle(self) -> int: from cleo.descriptors.text_descriptor import TextDescriptor TextDescriptor().describe( self._io, self.application, namespace=self.argument("namespace") ) return 0 cleo-2.1.0/src/cleo/cursor.py000066400000000000000000000044601451777547300160640ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING from typing import TextIO from cleo.io.io import IO if TYPE_CHECKING: from cleo.io.outputs.output import Output class Cursor: def __init__(self, io: IO | Output, input: TextIO | None = None) -> None: if isinstance(io, IO): io = io.output self._output = io if input is None: input = sys.stdin self._input = input def move_up(self, lines: int = 1) -> Cursor: self._output.write(f"\x1b[{lines}A") return self def move_down(self, lines: int = 1) -> Cursor: self._output.write(f"\x1b[{lines}B") return self def move_right(self, columns: int = 1) -> Cursor: self._output.write(f"\x1b[{columns}C") return self def move_left(self, columns: int = 1) -> Cursor: self._output.write(f"\x1b[{columns}D") return self def move_to_column(self, column: int) -> Cursor: self._output.write(f"\x1b[{column}G") return self def move_to_position(self, column: int, row: int) -> Cursor: self._output.write(f"\x1b[{row + 1};{column}H") return self def save_position(self) -> Cursor: self._output.write("\x1b7") return self def restore_position(self) -> Cursor: self._output.write("\x1b8") return self def hide(self) -> Cursor: self._output.write("\x1b[?25l") return self def show(self) -> Cursor: self._output.write("\x1b[?25h\x1b[?0c") return self def clear_line(self) -> Cursor: """ Clears all the output from the current line. """ self._output.write("\x1b[2K") return self def clear_line_after(self) -> Cursor: """ Clears all the output from the current line after the current position. """ self._output.write("\x1b[K") return self def clear_output(self) -> Cursor: """ Clears all the output from the cursors' current position to the end of the screen. """ self._output.write("\x1b[0J") return self def clear_screen(self) -> Cursor: """ Clears the entire screen. """ self._output.write("\x1b[2J") return self cleo-2.1.0/src/cleo/descriptors/000077500000000000000000000000001451777547300165325ustar00rootroot00000000000000cleo-2.1.0/src/cleo/descriptors/__init__.py000066400000000000000000000000001451777547300206310ustar00rootroot00000000000000cleo-2.1.0/src/cleo/descriptors/application_description.py000066400000000000000000000052131451777547300240130ustar00rootroot00000000000000from __future__ import annotations from collections import defaultdict from typing import TYPE_CHECKING from cleo.exceptions import CleoCommandNotFoundError if TYPE_CHECKING: from cleo.application import Application from cleo.commands.command import Command class ApplicationDescription: GLOBAL_NAMESPACE = "_global" def __init__( self, application: Application, namespace: str | None = None, show_hidden: bool = False, ) -> None: self._application: Application = application self._namespace = namespace self._show_hidden = show_hidden self._namespaces: dict[str, dict[str, str | list[str]]] = {} self._commands: dict[str, Command] = {} self._aliases: dict[str, Command] = {} self._inspect_application() @property def namespaces(self) -> dict[str, dict[str, str | list[str]]]: return self._namespaces @property def commands(self) -> dict[str, Command]: return self._commands def command(self, name: str) -> Command: if name in self._commands: return self._commands[name] if name in self._aliases: return self._aliases[name] raise CleoCommandNotFoundError(name) def _inspect_application(self) -> None: namespace = None if self._namespace: namespace = self._application.find_namespace(self._namespace) all_commands = self._application.all(namespace) for namespace, commands in self._sort_commands(all_commands): names = [] for name, command in commands: if not command.name or command.hidden: continue if command.name == name: self._commands[name] = command else: self._aliases[name] = command names.append(name) self._namespaces[namespace] = {"id": namespace, "commands": names} def _sort_commands( self, commands: dict[str, Command] ) -> list[tuple[str, list[tuple[str, Command]]]]: """ Sorts command in alphabetical order """ namespaced_commands: dict[str, dict[str, Command]] = defaultdict(dict) for name, command in commands.items(): key = self._application.extract_namespace(name, 1) or "_global" namespaced_commands[key][name] = command namespaced_commands_list: dict[str, list[tuple[str, Command]]] = { namespace: sorted(commands.items()) for namespace, commands in namespaced_commands.items() } return sorted(namespaced_commands_list.items()) cleo-2.1.0/src/cleo/descriptors/descriptor.py000066400000000000000000000032771451777547300212730ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import Any from cleo.application import Application from cleo.commands.command import Command from cleo.io.inputs.argument import Argument from cleo.io.inputs.definition import Definition from cleo.io.inputs.option import Option from cleo.io.outputs.output import Type if TYPE_CHECKING: from cleo.io.io import IO class Descriptor: def describe(self, io: IO, obj: Any, **options: Any) -> None: self._io = io if isinstance(obj, Argument): self._describe_argument(obj, **options) elif isinstance(obj, Option): self._describe_option(obj, **options) elif isinstance(obj, Definition): self._describe_definition(obj, **options) elif isinstance(obj, Command): self._describe_command(obj, **options) elif isinstance(obj, Application): self._describe_application(obj, **options) def _write(self, content: str, decorated: bool = True) -> None: self._io.write( content, new_line=False, type=Type.NORMAL if decorated else Type.RAW ) def _describe_argument(self, argument: Argument, **options: Any) -> None: raise NotImplementedError def _describe_option(self, option: Option, **options: Any) -> None: raise NotImplementedError def _describe_definition(self, definition: Definition, **options: Any) -> None: raise NotImplementedError def _describe_command(self, command: Command, **options: Any) -> None: raise NotImplementedError def _describe_application(self, application: Application, **options: Any) -> None: raise NotImplementedError cleo-2.1.0/src/cleo/descriptors/text_descriptor.py000066400000000000000000000223011451777547300223240ustar00rootroot00000000000000from __future__ import annotations import json import re from typing import TYPE_CHECKING from typing import Any from typing import Sequence from cleo.commands.command import Command from cleo.descriptors.descriptor import Descriptor from cleo.formatters.formatter import Formatter from cleo.io.inputs.definition import Definition if TYPE_CHECKING: from cleo.application import Application from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class TextDescriptor(Descriptor): def _describe_argument(self, argument: Argument, **options: Any) -> None: if argument.default is not None and ( not isinstance(argument.default, list) or argument.default ): default = ( f" [default: {self._format_default_value(argument.default)}]" "" ) else: default = "" total_width = options.get("total_width", len(argument.name)) spacing_width = total_width - len(argument.name) sub_argument_description = re.sub( r"\s*[\r\n]\s*", "\n" + " " * (total_width + 4), argument.description, ) self._write( f" {argument.name} {' ' * spacing_width}" f"{sub_argument_description}{default}" ) def _describe_option(self, option: Option, **options: Any) -> None: if ( option.accepts_value() and option.default is not None and (not isinstance(option.default, list) or option.default) ): default = ( " [default: " f"{self._format_default_value(option.default)}]" ) else: default = "" value = "" if option.accepts_value(): value = "=" + option.name.upper() if not option.requires_value(): value = "[" + value + "]" total_width = options.get( "total_width", self._calculate_total_width_for_options([option]) ) option_shortcut = f"-{option.shortcut}, " if option.shortcut else " " synopsis = f"{option_shortcut}--{option.name}{value}" spacing_width = total_width - len(synopsis) sub_option_description = re.sub( r"\s*[\r\n]\s*", "\n" + " " * (total_width + 4), option.description, ) are_multiple_values_allowed = ( " (multiple values allowed)" if option.is_list() else "" ) self._write( f" {synopsis} " f"{' ' * spacing_width}{sub_option_description}" f"{default}" f"{are_multiple_values_allowed}" ) def _describe_definition(self, definition: Definition, **options: Any) -> None: arguments = definition.arguments definition_options = definition.options total_width = self._calculate_total_width_for_options(definition_options) for argument in arguments: total_width = max(total_width, len(argument.name)) if arguments: self._write("Arguments:") self._write("\n") for argument in arguments: self._describe_argument(argument, total_width=total_width) self._write("\n") if arguments and definition_options: self._write("\n") if definition_options: later_options = [] self._write("Options:") for option in definition_options: if option.shortcut and len(option.shortcut) > 1: later_options.append(option) continue self._write("\n") self._describe_option(option, total_width=total_width) for option in later_options: self._write("\n") self._describe_option(option, total_width=total_width) def _describe_command(self, command: Command, **options: Any) -> None: command.merge_application_definition(False) description = command.description if description: self._write("Description:") self._write("\n") self._write(" " + description) self._write("\n\n") self._write("Usage:") for usage in [command.synopsis(True), *command.aliases, *command.usages]: self._write("\n") self._write(" " + Formatter.escape(usage)) self._write("\n") definition = command.definition if definition.options or definition.arguments: self._write("\n") self._describe_definition(definition, **options) self._write("\n") help_text = command.processed_help if help_text and help_text != description: self._write("\n") self._write("Help:") self._write("\n") self._write(" " + help_text.replace("\n", "\n ")) self._write("\n") def _describe_application(self, application: Application, **options: Any) -> None: from cleo.descriptors.application_description import ApplicationDescription described_namespace = options.get("namespace") description = ApplicationDescription(application, namespace=described_namespace) help_text = application.help if help_text: self._write(f"{help_text}\n\n") self._write("Usage:\n") self._write(" command [options] [arguments]\n\n") self._describe_definition(Definition(application.definition.options), **options) self._write("\n\n") commands = description.commands namespaces = description.namespaces if described_namespace and namespaces: described_namespace_info = next(iter(namespaces.values())) for name in described_namespace_info["commands"]: commands[name] = description.command(name) # calculate max width based on available commands per namespace all_commands = list(commands) for namespace in namespaces.values(): all_commands += namespace["commands"] width = self._get_column_width(all_commands) if described_namespace: self._write( f'Available commands for the "{described_namespace}" namespace:' ) else: self._write("Available commands:") for namespace in namespaces.values(): namespace["commands"] = [c for c in namespace["commands"] if c in commands] if not namespace["commands"]: continue if not ( described_namespace or namespace["id"] == ApplicationDescription.GLOBAL_NAMESPACE ): self._write("\n") self._write(f" {namespace['id']}") for name in namespace["commands"]: self._write("\n") spacing_width = width - len(name) command = commands[name] command_aliases = ( self._get_command_aliases_text(command) if command.name == name else "" ) self._write( f" {name}{' ' * spacing_width}" f"{command_aliases + command.description}" ) self._write("\n") def _format_default_value(self, default: Any) -> str: if isinstance(default, str): default = Formatter.escape(default) elif isinstance(default, list): default = [ Formatter.escape(value) for value in default if isinstance(value, str) ] elif isinstance(default, dict): default = { key: Formatter.escape(value) for key, value in default.items() if isinstance(value, str) } return json.dumps(default).replace("\\\\", "\\") def _calculate_total_width_for_options(self, options: list[Option]) -> int: total_width = 0 for option in options: name_length = 1 + max(len(option.shortcut or ""), 1) + 4 + len(option.name) if option.accepts_value(): value_length = 1 + len(option.name) if not option.requires_value(): value_length += 2 name_length += value_length total_width = max(total_width, name_length) return total_width def _get_column_width(self, commands: Sequence[Command | str]) -> int: widths: list[int] = [] for command in commands: if isinstance(command, Command): assert command.name is not None widths.append(len(command.name)) for alias in command.aliases: widths.append(len(alias)) else: widths.append(len(command)) if not widths: return 0 return max(widths) + 2 def _get_command_aliases_text(self, command: Command) -> str: aliases = command.aliases if aliases: return f"[{ '|'.join(aliases) }] " return "" cleo-2.1.0/src/cleo/events/000077500000000000000000000000001451777547300154755ustar00rootroot00000000000000cleo-2.1.0/src/cleo/events/__init__.py000066400000000000000000000000001451777547300175740ustar00rootroot00000000000000cleo-2.1.0/src/cleo/events/console_command_event.py000066400000000000000000000014721451777547300224140ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from cleo.events.console_event import ConsoleEvent if TYPE_CHECKING: from cleo.commands.command import Command from cleo.io.io import IO class ConsoleCommandEvent(ConsoleEvent): """ An event triggered before the command is executed. It allows to do things like skipping the command or changing the input. """ RETURN_CODE_DISABLED: int = 113 def __init__(self, command: Command, io: IO) -> None: super().__init__(command, io) self._command_should_run = True def disable_command(self) -> None: self._command_should_run = False def enable_command(self) -> None: self._command_should_run = True def command_should_run(self) -> bool: return self._command_should_run cleo-2.1.0/src/cleo/events/console_error_event.py000066400000000000000000000021121451777547300221170ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from cleo.events.console_event import ConsoleEvent from cleo.exceptions import CleoError if TYPE_CHECKING: from cleo.commands.command import Command from cleo.io.io import IO class ConsoleErrorEvent(ConsoleEvent): """ An event triggered when an exception is raised during the execution of a command. """ def __init__(self, command: Command, io: IO, error: Exception) -> None: super().__init__(command, io) self._error = error self._exit_code: int | None = None @property def error(self) -> Exception: return self._error @property def exit_code(self) -> int: if self._exit_code is not None: return self._exit_code if isinstance(self._error, CleoError) and self._error.exit_code is not None: return self._error.exit_code return 1 def set_error(self, error: Exception) -> None: self._error = error def set_exit_code(self, exit_code: int) -> None: self._exit_code = exit_code cleo-2.1.0/src/cleo/events/console_event.py000066400000000000000000000011101451777547300207030ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from cleo.events.event import Event if TYPE_CHECKING: from cleo.commands.command import Command from cleo.io.io import IO class ConsoleEvent(Event): """ An event that gives access to the IO of a command. """ def __init__(self, command: Command, io: IO) -> None: super().__init__() self._command = command self._io = io @property def command(self) -> Command: return self._command @property def io(self) -> IO: return self._io cleo-2.1.0/src/cleo/events/console_events.py000066400000000000000000000012551451777547300211000ustar00rootroot00000000000000# The COMMAND event allows to attach listeners before any command # is executed. It also allows the modification of the command and IO # before it's handed to the command. from __future__ import annotations COMMAND = "console.command" # The SIGNAL event allows some actions to be performed after # the command execution is interrupted. SIGNAL = "console.signal" # The TERMINATE event allows listeners to be attached after the command # is executed by the console. TERMINATE = "console.terminate" # The ERROR event occurs when an uncaught exception is raised. # # This event gives the ability to deal with the exception or to modify # the raised exception. ERROR = "console.error" cleo-2.1.0/src/cleo/events/console_signal_event.py000066400000000000000000000011661451777547300222530ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from cleo.events.console_event import ConsoleEvent if TYPE_CHECKING: import signal from cleo.commands.command import Command from cleo.io.io import IO class ConsoleSignalEvent(ConsoleEvent): """ An event triggered by a system signal. """ def __init__( self, command: Command, io: IO, handling_signal: signal.Signals ) -> None: super().__init__(command, io) self._handling_signal = handling_signal @property def handling_signal(self) -> signal.Signals: return self._handling_signal cleo-2.1.0/src/cleo/events/console_terminate_event.py000066400000000000000000000012201451777547300227550ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from cleo.events.console_event import ConsoleEvent if TYPE_CHECKING: from cleo.commands.command import Command from cleo.io.io import IO class ConsoleTerminateEvent(ConsoleEvent): """ An event triggered by after the execution of a command. """ def __init__(self, command: Command, io: IO, exit_code: int) -> None: super().__init__(command, io) self._exit_code = exit_code @property def exit_code(self) -> int: return self._exit_code def set_exit_code(self, exit_code: int) -> None: self._exit_code = exit_code cleo-2.1.0/src/cleo/events/event.py000066400000000000000000000005011451777547300171640ustar00rootroot00000000000000from __future__ import annotations class Event: """ Event """ def __init__(self) -> None: self._propagation_stopped = False def is_propagation_stopped(self) -> bool: return self._propagation_stopped def stop_propagation(self) -> None: self._propagation_stopped = True cleo-2.1.0/src/cleo/events/event_dispatcher.py000066400000000000000000000056611451777547300214060ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import Callable from typing import cast if TYPE_CHECKING: from cleo.events.event import Event Listener = Callable[[Event, str, "EventDispatcher"], None] class EventDispatcher: def __init__(self) -> None: self._listeners: dict[str, dict[int, list[Listener]]] = {} self._sorted: dict[str, list[Listener]] = {} def dispatch(self, event: Event, event_name: str | None = None) -> Event: if event_name is None: event_name = type(event).__name__ listeners = cast("list[Listener]", self.get_listeners(event_name)) if listeners: self._do_dispatch(listeners, event_name, event) return event def get_listeners( self, event_name: str | None = None ) -> list[Listener] | dict[str, list[Listener]]: 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 in self._listeners: if event_name not in self._sorted: self._sort_listeners(event_name) return self._sorted def get_listener_priority(self, event_name: str, listener: Listener) -> int | None: if event_name not in self._listeners: return None for priority, listeners in self._listeners[event_name].items(): for v in listeners: if v == listener: return priority return None def has_listeners(self, event_name: str | None = None) -> bool: if event_name is not None: return bool(self._listeners.get(event_name)) return any(self._listeners.values()) def add_listener( self, event_name: str, listener: Listener, priority: int = 0 ) -> 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: list[Listener], event_name: str, event: Event ) -> None: for listener in listeners: if event.is_propagation_stopped(): break listener(event, event_name, self) def _sort_listeners(self, event_name: str) -> None: """ Sorts the internal list of listeners for the given event by priority. """ prioritized_listeners = self._listeners[event_name] sorted_listeners = self._sorted[event_name] = [] for priority in sorted(prioritized_listeners, reverse=True): sorted_listeners.extend(prioritized_listeners[priority]) cleo-2.1.0/src/cleo/exceptions/000077500000000000000000000000001451777547300163525ustar00rootroot00000000000000cleo-2.1.0/src/cleo/exceptions/__init__.py000066400000000000000000000043141451777547300204650ustar00rootroot00000000000000from __future__ import annotations from cleo._utils import find_similar_names class CleoError(Exception): """ Base Cleo exception. """ exit_code: int | None = None class CleoLogicError(CleoError): """ Raised when there is error in command arguments and/or options configuration logic. """ class CleoRuntimeError(CleoError): """ Raised when command is called with invalid options or arguments. """ class CleoValueError(CleoError): """ Raised when wrong value was given to Cleo components. """ class CleoNoSuchOptionError(CleoError): """ Raised when command does not have given option. """ class CleoUserError(CleoError): """ Base exception for user errors. """ class CleoMissingArgumentsError(CleoUserError): """ Raised when called command was not given required arguments. """ def _suggest_similar_names(name: str, names: list[str]) -> str | None: if not names: return None suggested_names = find_similar_names(name, names) if not suggested_names: return None newline_separator = "\n " return "Did you mean " + newline_separator.join( ( ("this?" if len(suggested_names) == 1 else "one of these?"), newline_separator.join(suggested_names), ) ) class CleoCommandNotFoundError(CleoUserError): """ Raised when called command does not exist. """ def __init__(self, name: str, commands: list[str] | None = None) -> None: message = f'The command "{name}" does not exist.' if commands: suggestions = _suggest_similar_names(name, commands) if suggestions: message += "\n\n" + suggestions super().__init__(message) class CleoNamespaceNotFoundError(CleoUserError): """ Raised when called namespace has no commands. """ def __init__(self, name: str, namespaces: list[str] | None = None) -> None: message = f'There are no commands in the "{name}" namespace.' if namespaces: suggestions = _suggest_similar_names(name, namespaces) if suggestions: message += "\n\n" + suggestions super().__init__(message) cleo-2.1.0/src/cleo/formatters/000077500000000000000000000000001451777547300163575ustar00rootroot00000000000000cleo-2.1.0/src/cleo/formatters/__init__.py000066400000000000000000000000001451777547300204560ustar00rootroot00000000000000cleo-2.1.0/src/cleo/formatters/formatter.py000066400000000000000000000140651451777547300207420ustar00rootroot00000000000000from __future__ import annotations import re from typing import ClassVar from cleo.exceptions import CleoValueError from cleo.formatters.style import Style from cleo.formatters.style_stack import StyleStack class Formatter: TAG_REGEX = re.compile(r"(?ix)<(([a-z](?:[^<>]*)) | /([a-z](?:[^<>]*))?)>") _inline_styles_cache: ClassVar[dict[str, Style]] = {} def __init__( self, decorated: bool = False, styles: dict[str, Style] | None = None ) -> None: self._decorated = decorated self._styles: dict[str, Style] = {} self.set_style("error", Style("red", options=["bold"])) self.set_style("info", Style("blue")) self.set_style("comment", Style("green")) self.set_style("question", Style("cyan")) self.set_style("c1", Style("cyan")) self.set_style("c2", Style("default", options=["bold"])) self.set_style("b", Style("default", options=["bold"])) for name, style in (styles or {}).items(): self.set_style(name, style) self._style_stack = StyleStack() @classmethod def escape(cls, text: str) -> str: """ Escapes "<" special char in given text. """ text = re.sub(r"([^\\]?)<", "\\1\\<", text) return cls.escape_trailing_backslash(text) @staticmethod def escape_trailing_backslash(text: str) -> str: """ Escapes trailing "\\" in given text. """ if text.endswith("\\"): length = len(text) text = text.rstrip("\\").replace("\0", "").ljust(length, "\0") return text def decorated(self, decorated: bool = True) -> None: self._decorated = decorated def is_decorated(self) -> bool: return self._decorated def set_style(self, name: str, style: Style) -> None: self._styles[name] = style def has_style(self, name: str) -> bool: return name in self._styles def style(self, name: str) -> Style: if not self.has_style(name): raise CleoValueError(f'Undefined style: "{name}"') return self._styles[name] def format(self, message: str) -> str: return self.format_and_wrap(message, 0) def format_and_wrap(self, message: str, width: int) -> str: offset = 0 output = "" current_line_length = 0 for match in self.TAG_REGEX.finditer(message): pos = match.start() text = match.group(0) if pos != 0 and message[pos - 1] == "\\": continue # add the text up to the next tag formatted, current_line_length = self._apply_current_style( message[offset:pos], output, width, current_line_length ) output += formatted offset = pos + len(text) # Opening tag seen_open = text[1] != "/" tag = match.group(1) if seen_open else match.group(2) style = None if tag: style = self._create_style_from_string(tag) if not (seen_open or tag): # self._style_stack.pop() elif style is None: formatted, current_line_length = self._apply_current_style( text, output, width, current_line_length ) output += formatted elif seen_open: self._style_stack.push(style) else: self._style_stack.pop(style) formatted, current_line_length = self._apply_current_style( message[offset:], output, width, current_line_length ) output += formatted return output.replace("\0", "\\").replace("\\<", "<") def remove_format(self, text: str) -> str: decorated = self._decorated self._decorated = False text = re.sub(r"\033\[[^m]*m", "", self.format(text)) self._decorated = decorated return text def _create_style_from_string(self, string: str) -> Style | None: if string in self._styles: return self._styles[string] if string in self._inline_styles_cache: return self._inline_styles_cache[string] matches = re.findall(r"([^=]+)=([^;]+)(;|$)", string.lower()) if not matches: return None style = Style() for where, style_options, _ in matches: if where == "fg": style.foreground(style_options) elif where == "bg": style.background(style_options) else: try: for option in map(str.strip, style_options.split(",")): style.set_option(option) except ValueError: return None self._inline_styles_cache[string] = style return style def _apply_current_style( self, text: str, current: str, width: int, current_line_length: int ) -> tuple[str, int]: if not text: return "", current_line_length if not width: if self.is_decorated(): return self._style_stack.current.apply(text), current_line_length return text, current_line_length if not current_line_length and current: text = text.lstrip() if current_line_length: i = width - current_line_length prefix = text[:i] + "\n" text = text[i:] else: prefix = "" m = re.match(r"(\n)$", text) text = prefix + re.sub(rf"([^\n]{{{width}}})\ *", "\\1\n", text) text = text.rstrip("\n") + (m.group(1) if m else "") if not current_line_length and current and not current.endswith("\n"): text = "\n" + text lines = text.split("\n") for line in lines: current_line_length += len(line) if current_line_length >= width: current_line_length = 0 if self.is_decorated(): apply = self._style_stack.current.apply text = "\n".join(map(apply, lines)) return text, current_line_length cleo-2.1.0/src/cleo/formatters/style.py000066400000000000000000000043301451777547300200710ustar00rootroot00000000000000from __future__ import annotations from cleo.color import Color class Style: def __init__( self, foreground: str | None = None, background: str | None = None, options: list[str] | None = None, ) -> None: self._foreground = foreground or "" self._background = background or "" self._options = options or [] self._color = Color(self._foreground, self._background, self._options) def foreground(self, foreground: str) -> Style: self._color = Color(foreground, self._background, self._options) self._foreground = foreground return self def background(self, background: str) -> Style: self._color = Color(self._foreground, background, self._options) self._background = background return self def bold(self, bold: bool = True) -> Style: return self._toggle_option(bold, "bold") def dark(self, dark: bool = True) -> Style: return self._toggle_option(dark, "dark") def underlines(self, underlined: bool = True) -> Style: return self._toggle_option(underlined, "underline") def italic(self, italic: bool = True) -> Style: return self._toggle_option(italic, "italic") def blinking(self, blinking: bool = True) -> Style: return self._toggle_option(blinking, "blink") def inverse(self, inverse: bool = True) -> Style: return self._toggle_option(inverse, "reverse") def hidden(self, hidden: bool = True) -> Style: return self._toggle_option(hidden, "conceal") def set_option(self, option: str) -> Style: self._options.append(option) self._color = Color(self._foreground, self._background, self._options) return self def unset_option(self, option: str) -> Style: if option in self._options: index = self._options.index(option) del self._options[index] self._color = Color(self._foreground, self._background, self._options) return self def _toggle_option(self, toggle_flag: bool, option: str) -> Style: return (self.set_option if toggle_flag else self.unset_option)(option) def apply(self, text: str) -> str: return self._color.apply(text) cleo-2.1.0/src/cleo/formatters/style_stack.py000066400000000000000000000021401451777547300212530ustar00rootroot00000000000000from __future__ import annotations from cleo.exceptions import CleoValueError from cleo.formatters.style import Style class StyleStack: def __init__(self, empty_style: Style | None = None) -> None: if empty_style is None: empty_style = Style() self._empty_style = empty_style self._styles: list[Style] = [] @property def current(self) -> Style: if not self._styles: return self._empty_style return self._styles[-1] def reset(self) -> None: self._styles = [] def push(self, style: Style) -> None: self._styles.append(style) def pop(self, style: Style | None = None) -> Style: if not self._styles: return self._empty_style if style is None: return self._styles.pop() sample = style.apply("") for i, stacked_style in reversed(list(enumerate(self._styles))): if sample == stacked_style.apply(""): self._styles = self._styles[:i] return stacked_style raise CleoValueError("Invalid nested tag found") cleo-2.1.0/src/cleo/helpers.py000066400000000000000000000016201451777547300162040ustar00rootroot00000000000000from __future__ import annotations from typing import Any from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option def argument( name: str, description: str | None = None, optional: bool = False, multiple: bool = False, default: Any | None = None, ) -> Argument: return Argument( name, required=not optional, is_list=multiple, description=description, default=default, ) def option( long_name: str, short_name: str | None = None, description: str | None = None, flag: bool = True, value_required: bool = True, multiple: bool = False, default: Any | None = None, ) -> Option: return Option( long_name, short_name, flag=flag, requires_value=value_required, is_list=multiple, description=description, default=default, ) cleo-2.1.0/src/cleo/io/000077500000000000000000000000001451777547300146005ustar00rootroot00000000000000cleo-2.1.0/src/cleo/io/__init__.py000066400000000000000000000000001451777547300166770ustar00rootroot00000000000000cleo-2.1.0/src/cleo/io/buffered_io.py000066400000000000000000000031001451777547300174150ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import cast from cleo.io.inputs.string_input import StringInput from cleo.io.io import IO from cleo.io.outputs.buffered_output import BufferedOutput if TYPE_CHECKING: from cleo.io.inputs.input import Input class BufferedIO(IO): def __init__( self, input: Input | None = None, decorated: bool = False, supports_utf8: bool = True, ) -> None: super().__init__( input or StringInput(""), BufferedOutput(decorated=decorated, supports_utf8=supports_utf8), BufferedOutput(decorated=decorated, supports_utf8=supports_utf8), ) def fetch_output(self) -> str: return cast(BufferedOutput, self._output).fetch() def fetch_error(self) -> str: return cast(BufferedOutput, self._error_output).fetch() def clear(self) -> None: cast(BufferedOutput, self._output).clear() cast(BufferedOutput, self._error_output).clear() def clear_output(self) -> None: cast(BufferedOutput, self._output).clear() def clear_error(self) -> None: cast(BufferedOutput, self._error_output).clear() def supports_utf8(self) -> bool: return cast(BufferedOutput, self._output).supports_utf8() def clear_user_input(self) -> None: self._input.stream.truncate(0) self._input.stream.seek(0) def set_user_input(self, user_input: str) -> None: self.clear_user_input() self._input.stream.write(user_input) self._input.stream.seek(0) cleo-2.1.0/src/cleo/io/inputs/000077500000000000000000000000001451777547300161225ustar00rootroot00000000000000cleo-2.1.0/src/cleo/io/inputs/__init__.py000066400000000000000000000000001451777547300202210ustar00rootroot00000000000000cleo-2.1.0/src/cleo/io/inputs/argument.py000066400000000000000000000033171451777547300203220ustar00rootroot00000000000000from __future__ import annotations from typing import Any from cleo.exceptions import CleoLogicError class Argument: """ A command line argument. """ def __init__( self, name: str, required: bool = True, is_list: bool = False, description: str | None = None, default: Any | None = None, ) -> None: self._name = name self._required = required self._is_list = is_list self._description = description or "" self._default: str | list[str] | None = None self.set_default(default) @property def name(self) -> str: return self._name @property def default(self) -> str | list[str] | None: return self._default @property def description(self) -> str: return self._description def is_required(self) -> bool: return self._required def is_list(self) -> bool: return self._is_list def set_default(self, default: Any | None = None) -> None: if self._required and default is not None: raise CleoLogicError("Cannot set a default value for required arguments") if self._is_list: if default is None: default = [] elif not isinstance(default, list): raise CleoLogicError( "A default value for a list argument must be a list" ) self._default = default def __repr__(self) -> str: return ( f"Argument({self._name!r}, " f"required={self._required}, " f"is_list={self._is_list}, " f"description={self._description!r}, " f"default={self._default!r})" ) cleo-2.1.0/src/cleo/io/inputs/argv_input.py000066400000000000000000000230771451777547300206630ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING from typing import Any from cleo.exceptions import CleoNoSuchOptionError from cleo.exceptions import CleoRuntimeError from cleo.io.inputs.input import Input if TYPE_CHECKING: from cleo.io.inputs.definition import Definition class ArgvInput(Input): """ Represents an input coming from the command line. """ def __init__( self, argv: list[str] | None = None, definition: Definition | None = None ) -> None: if argv is None: argv = sys.argv argv = argv[:] # Strip the application name try: self._script_name: str | None = argv.pop(0) except IndexError: self._script_name = None self._tokens = argv self._parsed: list[str] = [] super().__init__(definition=definition) @property def first_argument(self) -> str | None: is_option = False for i, token in enumerate(self._tokens): if token.startswith("-"): if "=" in token or len(self._tokens) == (i + 1): continue # If it's a long option, consider that # everything after "--" is the option name. # Otherwise, use the last character # (if it's a short option set, only the last one # can take a value with space separator). name = token[2:] if token.startswith("--") else token[-1] if not (name in self._options or self._definition.has_shortcut(name)): # noop continue if name not in self._options: name = self._definition.shortcut_to_name(name) if name in self._options and self._tokens[i + 1] == self._options[name]: is_option = True continue if is_option: is_option = False continue return token return None @property def script_name(self) -> str | None: return self._script_name def has_parameter_option( self, values: str | list[str], only_params: bool = False ) -> bool: """ Returns true if the raw parameters (not parsed) contain a value. """ if not isinstance(values, list): values = [values] for token in self._tokens: if only_params and token == "--": return False for value in values: # Options with values: # For long options, test for '--option=' at beginning # For short options, test for '-o' at beginning leading = value + "=" if value.startswith("--") else value if token == value or leading != "" and token.startswith(leading): return True return False def parameter_option( self, values: str | list[str], default: Any = False, only_params: bool = False, ) -> Any: if not isinstance(values, list): values = [values] tokens = self._tokens[:] while tokens: token = tokens.pop(0) if only_params and token == "--": return default for value in values: if token == value: try: return tokens.pop(0) except IndexError: return None # Options with values: # For long options, test for '--option=' at beginning # For short options, test for '-o' at beginning leading = value + "=" if value.startswith("--") else value if token == value or leading != "" and token.startswith(leading): return token[len(leading)] return False def _set_tokens(self, tokens: list[str]) -> None: self._tokens = tokens def _parse(self) -> None: parse_options = True self._parsed = self._tokens[:] try: token = self._parsed.pop(0) except IndexError: return while token is not None: if parse_options and token == "": self._parse_argument(token) elif parse_options and token == "--": parse_options = False elif parse_options and token.startswith("--"): self._parse_long_option(token) elif parse_options and token.startswith("-") and token != "-": self._parse_short_option(token) else: self._parse_argument(token) try: token = self._parsed.pop(0) except IndexError: return def _parse_short_option(self, token: str) -> None: name = token[1:] if len(name) > 1: shortcut = name[0] if ( self._definition.has_shortcut(shortcut) and self._definition.option_for_shortcut(shortcut).accepts_value() ): # An option with a value and no space self._add_short_option(shortcut, name[1:]) else: self._parse_short_option_set(name) else: self._add_short_option(name, None) def _parse_short_option_set(self, name: str) -> None: length = len(name) for i in range(length): shortcut = name[i] if not self._definition.has_shortcut(shortcut): raise CleoRuntimeError(f'The option "{name[i]}" does not exist') option = self._definition.option_for_shortcut(shortcut) if option.accepts_value(): self._add_long_option( option.name, name[i + 1 :] if i < length - 1 else None ) break self._add_long_option(option.name, None) def _parse_long_option(self, token: str) -> None: name = token[2:] pos = name.find("=") if pos != -1: value = name[pos + 1 :] if not value: self._parsed.insert(0, value) self._add_long_option(name[:pos], value) else: self._add_long_option(name, None) def _parse_argument(self, token: str) -> None: next_argument = len(self._arguments) last_argument = next_argument - 1 # If the input is expecting another argument, add it if self._definition.has_argument(next_argument): argument = self._definition.argument(next_argument) self._arguments[argument.name] = [token] if argument.is_list() else token # If the last argument is a list, append the token to it elif ( self._definition.has_argument(last_argument) and self._definition.argument(last_argument).is_list() ): argument = self._definition.argument(last_argument) self._arguments[argument.name].append(token) # Unexpected argument else: all_arguments = self._definition.arguments.copy() command_name = None argument = all_arguments[0] if argument and argument.name == "command": command_name = self._arguments.get("command") del all_arguments[0] if all_arguments: all_names = " ".join(a.name.join('""') for a in all_arguments) if command_name: message = ( f'Too many arguments to "{command_name}" command, ' f"expected arguments {all_names}" ) else: message = f"Too many arguments, expected arguments {all_names}" elif command_name: message = ( f'No arguments expected for "{command_name}" command, ' f'got "{token}"' ) else: message = f'No arguments expected, got "{token}"' raise CleoRuntimeError(message) def _add_short_option(self, shortcut: str, value: Any) -> None: if not self._definition.has_shortcut(shortcut): raise CleoNoSuchOptionError(f'The option "-{shortcut}" does not exist') self._add_long_option( self._definition.option_for_shortcut(shortcut).name, value ) def _add_long_option(self, name: str, value: Any) -> None: if not self._definition.has_option(name): raise CleoNoSuchOptionError(f'The option "--{name}" does not exist') option = self._definition.option(name) if not (value is None or option.accepts_value()): raise CleoRuntimeError(f'The "--{name}" option does not accept a value') if value in ("", None) and option.accepts_value() and self._parsed: # If the option accepts a value, either required or optional, # we check if there is one next_token = self._parsed.pop(0) if not next_token.startswith("-") or next_token in ("", None): value = next_token else: self._parsed.insert(0, next_token) if value is None: if option.requires_value(): raise CleoRuntimeError(f'The "--{name}" option requires a value') if not option.is_list() and option.is_flag(): value = True if option.is_list(): if name not in self._options: self._options[name] = [] self._options[name].append(value) else: self._options[name] = value cleo-2.1.0/src/cleo/io/inputs/definition.py000066400000000000000000000150031451777547300206230ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING from typing import Any from typing import Sequence from cleo.exceptions import CleoLogicError from cleo.io.inputs.option import Option if TYPE_CHECKING: from cleo.io.inputs.argument import Argument class Definition: """ A Definition represents a set of command line arguments and options. """ def __init__(self, definition: Sequence[Argument | Option] | None = None) -> None: self._arguments: dict[str, Argument] = {} self._required_count = 0 self._has_list_argument = False self._has_optional = False self._options: dict[str, Option] = {} self._shortcuts: dict[str, str] = {} self.set_definition(definition or []) @property def arguments(self) -> list[Argument]: return list(self._arguments.values()) @property def argument_count(self) -> int: if self._has_list_argument: return sys.maxsize return len(self._arguments) @property def required_argument_count(self) -> int: return self._required_count @property def argument_defaults(self) -> dict[str, Any]: values = {} for argument in self._arguments.values(): values[argument.name] = argument.default return values @property def options(self) -> list[Option]: return list(self._options.values()) @property def option_defaults(self) -> dict[str, Any]: return {o.name: o.default for o in self._options.values()} def set_definition(self, definition: Sequence[Argument | Option]) -> None: arguments = [] options = [] for item in definition: if isinstance(item, Option): options.append(item) else: arguments.append(item) self.set_arguments(arguments) self.set_options(options) def set_arguments(self, arguments: list[Argument]) -> None: self._arguments = {} self._required_count = 0 self._has_list_argument = False self._has_optional = False self.add_arguments(arguments) def add_arguments(self, arguments: list[Argument]) -> None: for argument in arguments: self.add_argument(argument) def add_argument(self, argument: Argument) -> None: if argument.name in self._arguments: raise CleoLogicError( f'An argument with name "{argument.name}" already exists' ) if self._has_list_argument: raise CleoLogicError("Cannot add an argument after a list argument") if argument.is_required() and self._has_optional: raise CleoLogicError("Cannot add a required argument after an optional one") if argument.is_list(): self._has_list_argument = True if argument.is_required(): self._required_count += 1 else: self._has_optional = True self._arguments[argument.name] = argument def argument(self, name: str | int) -> Argument: if not self.has_argument(name): raise ValueError(f'The "{name}" argument does not exist') if isinstance(name, int): arguments = list(self._arguments.values()) return arguments[name] return self._arguments[name] def has_argument(self, name: str | int) -> bool: if isinstance(name, int): # Check if this is a valid argument index # abs(x + (x < 0)) to normalize negative indices return abs(name + (name < 0)) < len(self._arguments) return name in self._arguments def set_options(self, options: list[Option]) -> None: self._options = {} self._shortcuts = {} self.add_options(options) def add_options(self, options: list[Option]) -> None: for option in options: self.add_option(option) def add_option(self, option: Option) -> None: if option.name in self._options and option != self._options[option.name]: raise CleoLogicError(f'An option named "{option.name}" already exists') if option.shortcut: for shortcut in option.shortcut.split("|"): if ( shortcut in self._shortcuts and option.name != self._shortcuts[shortcut] ): raise CleoLogicError( f'An option with shortcut "{shortcut}" already exists' ) self._options[option.name] = option if option.shortcut: for shortcut in option.shortcut.split("|"): self._shortcuts[shortcut] = option.name def option(self, name: str) -> Option: if not self.has_option(name): raise ValueError(f'The option "--{name}" option does not exist') return self._options[name] def has_option(self, name: str) -> bool: return name in self._options def has_shortcut(self, shortcut: str) -> bool: return shortcut in self._shortcuts def option_for_shortcut(self, shortcut: str) -> Option: return self._options[self.shortcut_to_name(shortcut)] def shortcut_to_name(self, shortcut: str) -> str: if shortcut not in self._shortcuts: raise ValueError(f'The "-{shortcut}" option does not exist') return self._shortcuts[shortcut] def synopsis(self, short: bool = False) -> str: elements = [] if short and self._options: elements.append("[options]") elif not short: for option in self._options.values(): value = "" if option.accepts_value(): formatted = ( option.name.upper() if option.requires_value() else f"[{option.name.upper()}]" ) value = f" {formatted}" shortcut = "" if option.shortcut: shortcut = f"-{option.shortcut}|" elements.append(f"[{shortcut}--{option.name}{value}]") if elements and self._arguments: elements.append("[--]") tail = "" for argument in self._arguments.values(): element = f"<{argument.name}>" if argument.is_list(): element += "..." if not argument.is_required(): element = "[" + element tail += "]" elements.append(element) return " ".join(elements) + tail cleo-2.1.0/src/cleo/io/inputs/input.py000066400000000000000000000120701451777547300176330ustar00rootroot00000000000000from __future__ import annotations import re from typing import Any from typing import TextIO from cleo._compat import shell_quote from cleo.exceptions import CleoMissingArgumentsError from cleo.exceptions import CleoValueError from cleo.io.inputs.definition import Definition class Input: """ This class is the base class for concrete Input implementations. """ def __init__(self, definition: Definition | None = None) -> None: self._definition: Definition self._stream: TextIO = None # type: ignore[assignment] self._options: dict[str, Any] = {} self._arguments: dict[str, Any] = {} self._interactive: bool | None = None if definition is None: self._definition = Definition() else: self.bind(definition) self.validate() @property def arguments(self) -> dict[str, Any]: return {**self._definition.argument_defaults, **self._arguments} @property def options(self) -> dict[str, Any]: return {**self._definition.option_defaults, **self._options} @property def stream(self) -> TextIO: return self._stream @property def first_argument(self) -> str | None: """ Returns the first argument from the raw parameters (not parsed). """ raise NotImplementedError @property def script_name(self) -> str | None: raise NotImplementedError def read(self, length: int, default: str = "") -> str: """ Reads the given amount of characters from the input stream. """ if not self.is_interactive(): return default return self._stream.read(length) def read_line(self, length: int = -1, default: str = "") -> str: """ Reads a line from the input stream. """ if not self.is_interactive(): return default return self._stream.readline(length) def close(self) -> None: """ Closes the input. """ self._stream.close() def is_closed(self) -> bool: """ Returns whether the input is closed. """ return self._stream.closed def is_interactive(self) -> bool: return True if self._interactive is None else self._interactive def interactive(self, interactive: bool = True) -> None: self._interactive = interactive def bind(self, definition: Definition) -> None: """ Binds the current Input instance with the given definition's arguments and options. """ self._arguments = {} self._options = {} self._definition = definition self._parse() def validate(self) -> None: missing_arguments = [] for argument in self._definition.arguments: if argument.name not in self._arguments and argument.is_required(): missing_arguments.append(argument.name) if missing_arguments: raise CleoMissingArgumentsError( f'Not enough arguments (missing: "{", ".join(missing_arguments)}")' ) def argument(self, name: str) -> Any: if not self._definition.has_argument(name): raise CleoValueError(f'The argument "{name}" does not exist') if name in self._arguments: return self._arguments[name] return self._definition.argument(name).default def set_argument(self, name: str, value: Any) -> None: if not self._definition.has_argument(name): raise CleoValueError(f'The argument "{name}" does not exist') self._arguments[name] = value def has_argument(self, name: str) -> bool: return self._definition.has_argument(name) def option(self, name: str) -> Any: if not self._definition.has_option(name): raise CleoValueError(f'The option "--{name}" does not exist') if name in self._options: return self._options[name] return self._definition.option(name).default def set_option(self, name: str, value: Any) -> None: if not self._definition.has_option(name): raise CleoValueError(f'The option "--{name}" does not exist') self._options[name] = value def has_option(self, name: str) -> bool: return self._definition.has_option(name) def escape_token(self, token: str) -> str: if re.match(r"^[\w-]+$", token): return token return shell_quote(token) def set_stream(self, stream: TextIO) -> None: self._stream = stream def has_parameter_option( self, values: str | list[str], only_params: bool = False ) -> bool: """ Returns true if the raw parameters (not parsed) contain a value. """ raise NotImplementedError def parameter_option( self, values: str | list[str], default: Any = False, only_params: bool = False, ) -> Any: """ Returns the value of a raw option (not parsed). """ raise NotImplementedError def _parse(self) -> None: raise NotImplementedError cleo-2.1.0/src/cleo/io/inputs/option.py000066400000000000000000000046331451777547300200120ustar00rootroot00000000000000from __future__ import annotations import re from typing import Any from cleo.exceptions import CleoLogicError from cleo.exceptions import CleoValueError class Option: """ A command line option. """ def __init__( self, name: str, shortcut: str | None = None, flag: bool = True, requires_value: bool = True, is_list: bool = False, description: str | None = None, default: Any | None = None, ) -> None: if name.startswith("--"): name = name[2:] if not name: raise CleoValueError("An option name cannot be empty") if shortcut is not None: shortcuts = re.split(r"\|-?", shortcut.lstrip("-")) shortcut = "|".join(filter(None, shortcuts)) if not shortcut: raise CleoValueError("An option shortcut cannot be empty") self._name = name self._shortcut = shortcut self._flag = flag self._requires_value = requires_value self._is_list = is_list self._description = description or "" self._default = None if self._is_list and self._flag: raise CleoLogicError("A flag option cannot be a list as well") self.set_default(default) @property def name(self) -> str: return self._name @property def shortcut(self) -> str | None: return self._shortcut @property def description(self) -> str: return self._description @property def default(self) -> Any | None: return self._default def is_flag(self) -> bool: return self._flag def accepts_value(self) -> bool: return not self._flag def requires_value(self) -> bool: return not self._flag and self._requires_value def is_list(self) -> bool: return self._is_list def set_default(self, default: Any | None = None) -> None: if self._flag and default is not None: raise CleoLogicError("A flag option cannot have a default value") if self._is_list: if default is None: default = [] elif not isinstance(default, list): raise CleoLogicError("A default value for a list option must be a list") if self._flag: default = False self._default = default def __repr__(self) -> str: return f"Option({self._name})" cleo-2.1.0/src/cleo/io/inputs/string_input.py000066400000000000000000000006751451777547300212310ustar00rootroot00000000000000from __future__ import annotations from cleo.io.inputs.argv_input import ArgvInput from cleo.io.inputs.token_parser import TokenParser class StringInput(ArgvInput): """ Represents an input provided as a string """ def __init__(self, input: str) -> None: super().__init__([]) self._set_tokens(self._tokenize(input)) def _tokenize(self, input: str) -> list[str]: return TokenParser().parse(input) cleo-2.1.0/src/cleo/io/inputs/token_parser.py000066400000000000000000000053171451777547300211760ustar00rootroot00000000000000from __future__ import annotations QUOTES = {"'", '"'} class TokenParser: """ Parses tokens from a string passed to StringArgs. """ def __init__(self) -> None: self._string: str = "" self._cursor: int = 0 self._current: str | None = None self._next_: str | None = None def parse(self, string: str) -> list[str]: self._string = string self._cursor = 0 self._current = None if string: self._current = string[0] self._next_ = string[1] if len(string) > 1 else None return self._parse() def _parse(self) -> list[str]: tokens = [] while self._current is not None: if self._current.isspace(): # Skip spaces self._next() continue tokens.append(self._parse_token()) return tokens def _next(self) -> None: """ Advances the cursor to the next position. """ if self._current is None: 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) -> str: token = "" while self._current is not None: if self._current.isspace(): self._next() break if self._current == "\\": token += self._parse_escape_sequence() elif self._current in QUOTES: token += self._parse_quoted_string() else: token += self._current self._next() return token def _parse_quoted_string(self) -> str: string = "" delimiter = self._current # Skip first delimiter self._next() while self._current is not None: if self._current == delimiter: # Skip last delimiter self._next() break if self._current == "\\": string += self._parse_escape_sequence() elif self._current == '"': string += f'"{self._parse_quoted_string()}"' elif self._current == "'": string += f"'{self._parse_quoted_string()}'" else: string += self._current self._next() return string def _parse_escape_sequence(self) -> str: if self._next_ in QUOTES: sequence = self._next_ else: assert self._next_ is not None sequence = "\\" + self._next_ self._next() self._next() return sequence cleo-2.1.0/src/cleo/io/io.py000066400000000000000000000101161451777547300155600ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import Iterable from cleo.io.outputs.output import Type as OutputType from cleo.io.outputs.output import Verbosity if TYPE_CHECKING: from cleo.io.inputs.input import Input from cleo.io.outputs.output import Output from cleo.io.outputs.section_output import SectionOutput class IO: def __init__(self, input: Input, output: Output, error_output: Output) -> None: self._input = input self._output = output self._error_output = error_output @property def input(self) -> Input: return self._input @property def output(self) -> Output: return self._output @property def error_output(self) -> Output: return self._error_output def read(self, length: int, default: str = "") -> str: """ Reads the given amount of characters from the input stream. """ return self._input.read(length, default=default) def read_line(self, length: int = -1, default: str = "") -> str: """ Reads a line from the input stream. """ return self._input.read_line(length=length, default=default) def write_line( self, messages: str | Iterable[str], verbosity: Verbosity = Verbosity.NORMAL, type: OutputType = OutputType.NORMAL, ) -> None: self._output.write_line(messages, verbosity=verbosity, type=type) def write( self, messages: str | Iterable[str], new_line: bool = False, verbosity: Verbosity = Verbosity.NORMAL, type: OutputType = OutputType.NORMAL, ) -> None: self._output.write(messages, new_line=new_line, verbosity=verbosity, type=type) def write_error_line( self, messages: str | Iterable[str], verbosity: Verbosity = Verbosity.NORMAL, type: OutputType = OutputType.NORMAL, ) -> None: self._error_output.write_line(messages, verbosity=verbosity, type=type) def write_error( self, messages: str | Iterable[str], new_line: bool = False, verbosity: Verbosity = Verbosity.NORMAL, type: OutputType = OutputType.NORMAL, ) -> None: self._error_output.write( messages, new_line=new_line, verbosity=verbosity, type=type ) def overwrite(self, messages: str | Iterable[str]) -> None: from cleo.cursor import Cursor cursor = Cursor(self._output) cursor.move_to_column(1) cursor.clear_line() self.write(messages) def overwrite_error(self, messages: str | Iterable[str]) -> None: from cleo.cursor import Cursor cursor = Cursor(self._error_output) cursor.move_to_column(1) cursor.clear_line() self.write_error(messages) def flush(self) -> None: self._output.flush() def is_interactive(self) -> bool: return self._input.is_interactive() def interactive(self, interactive: bool = True) -> None: self._input.interactive(interactive) def decorated(self, decorated: bool = True) -> None: self._output.decorated(decorated) self._error_output.decorated(decorated) def is_decorated(self) -> bool: return self._output.is_decorated() def supports_utf8(self) -> bool: return self._output.supports_utf8() def set_verbosity(self, verbosity: Verbosity) -> None: self._output.set_verbosity(verbosity) self._error_output.set_verbosity(verbosity) def is_verbose(self) -> bool: return self.output.is_verbose() def is_very_verbose(self) -> bool: return self.output.is_very_verbose() def is_debug(self) -> bool: return self.output.is_debug() def set_input(self, input: Input) -> None: self._input = input def with_input(self, input: Input) -> IO: return self.__class__(input, self._output, self._error_output) def remove_format(self, text: str) -> str: return self._output.remove_format(text) def section(self) -> SectionOutput: return self._output.section() cleo-2.1.0/src/cleo/io/null_io.py000066400000000000000000000006451451777547300166200ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from cleo.io.inputs.string_input import StringInput from cleo.io.io import IO from cleo.io.outputs.null_output import NullOutput if TYPE_CHECKING: from cleo.io.inputs.input import Input class NullIO(IO): def __init__(self, input: Input | None = None) -> None: super().__init__(input or StringInput(""), NullOutput(), NullOutput()) cleo-2.1.0/src/cleo/io/outputs/000077500000000000000000000000001451777547300163235ustar00rootroot00000000000000cleo-2.1.0/src/cleo/io/outputs/__init__.py000066400000000000000000000000001451777547300204220ustar00rootroot00000000000000cleo-2.1.0/src/cleo/io/outputs/buffered_output.py000066400000000000000000000031541451777547300221020ustar00rootroot00000000000000from __future__ import annotations from io import StringIO from typing import TYPE_CHECKING from cleo.io.outputs.output import Output from cleo.io.outputs.output import Verbosity from cleo.io.outputs.section_output import SectionOutput if TYPE_CHECKING: from cleo.formatters.formatter import Formatter class BufferedOutput(Output): def __init__( self, verbosity: Verbosity = Verbosity.NORMAL, decorated: bool = False, formatter: Formatter | None = None, supports_utf8: bool = True, ) -> None: super().__init__(decorated=decorated, verbosity=verbosity, formatter=formatter) self._buffer = StringIO() self._supports_utf8 = supports_utf8 def fetch(self) -> str: """ Empties the buffer and returns its content. """ content = self._buffer.getvalue() self._buffer = StringIO() return content def clear(self) -> None: """ Empties the buffer. """ self._buffer = StringIO() def supports_utf8(self) -> bool: return self._supports_utf8 def set_supports_utf8(self, supports_utf8: bool) -> None: self._supports_utf8 = supports_utf8 def section(self) -> SectionOutput: return SectionOutput( self._buffer, self._section_outputs, verbosity=self.verbosity, decorated=self.is_decorated(), formatter=self.formatter, ) def _write(self, message: str, new_line: bool = False) -> None: self._buffer.write(message) if new_line: self._buffer.write("\n") cleo-2.1.0/src/cleo/io/outputs/null_output.py000066400000000000000000000024351451777547300212730ustar00rootroot00000000000000from __future__ import annotations from typing import Iterable from cleo.io.outputs.output import Output from cleo.io.outputs.output import Type from cleo.io.outputs.output import Verbosity class NullOutput(Output): @property def verbosity(self) -> Verbosity: return Verbosity.QUIET def is_decorated(self) -> bool: return False def decorated(self, decorated: bool = True) -> None: pass def supports_utf8(self) -> bool: return True def set_verbosity(self, verbosity: Verbosity) -> None: pass def is_quiet(self) -> bool: return True def is_verbose(self) -> bool: return False def is_very_verbose(self) -> bool: return False def is_debug(self) -> bool: return False def write_line( self, messages: str | Iterable[str], verbosity: Verbosity = Verbosity.NORMAL, type: Type = Type.NORMAL, ) -> None: pass def write( self, messages: str | Iterable[str], new_line: bool = False, verbosity: Verbosity = Verbosity.NORMAL, type: Type = Type.NORMAL, ) -> None: pass def flush(self) -> None: pass def _write(self, message: str, new_line: bool = False) -> None: pass cleo-2.1.0/src/cleo/io/outputs/output.py000066400000000000000000000061151451777547300202400ustar00rootroot00000000000000from __future__ import annotations from enum import Enum from typing import TYPE_CHECKING from typing import Iterable from cleo._utils import strip_tags from cleo.formatters.formatter import Formatter if TYPE_CHECKING: from cleo.io.outputs.section_output import SectionOutput class Verbosity(Enum): QUIET: int = 16 NORMAL: int = 32 VERBOSE: int = 64 VERY_VERBOSE: int = 128 DEBUG: int = 256 class Type(Enum): NORMAL: int = 1 RAW: int = 2 PLAIN: int = 4 class Output: def __init__( self, verbosity: Verbosity = Verbosity.NORMAL, decorated: bool = False, formatter: Formatter | None = None, ) -> None: self._verbosity: Verbosity = verbosity self._formatter = formatter or Formatter() self._formatter.decorated(decorated) self._section_outputs: list[SectionOutput] = [] @property def formatter(self) -> Formatter: return self._formatter @property def verbosity(self) -> Verbosity: return self._verbosity def set_formatter(self, formatter: Formatter) -> None: self._formatter = formatter def is_decorated(self) -> bool: return self._formatter.is_decorated() def decorated(self, decorated: bool = True) -> None: self._formatter.decorated(decorated) def supports_utf8(self) -> bool: """ Returns whether the stream supports the UTF-8 encoding. """ return True def set_verbosity(self, verbosity: Verbosity) -> None: self._verbosity = verbosity def is_quiet(self) -> bool: return self._verbosity is Verbosity.QUIET def is_verbose(self) -> bool: return self._verbosity.value >= Verbosity.VERBOSE.value def is_very_verbose(self) -> bool: return self._verbosity.value >= Verbosity.VERY_VERBOSE.value def is_debug(self) -> bool: return self._verbosity is Verbosity.DEBUG def write_line( self, messages: str | Iterable[str], verbosity: Verbosity = Verbosity.NORMAL, type: Type = Type.NORMAL, ) -> None: self.write(messages, new_line=True, verbosity=verbosity, type=type) def write( self, messages: str | Iterable[str], new_line: bool = False, verbosity: Verbosity = Verbosity.NORMAL, type: Type = Type.NORMAL, ) -> None: if isinstance(messages, str): messages = [messages] if verbosity.value > self.verbosity.value: return for message in messages: if type is Type.NORMAL: message = self._formatter.format(message) elif type is Type.PLAIN: message = strip_tags(self._formatter.format(message)) self._write(message, new_line=new_line) def flush(self) -> None: pass def remove_format(self, text: str) -> str: return self.formatter.remove_format(text) def section(self) -> SectionOutput: raise NotImplementedError def _write(self, message: str, new_line: bool = False) -> None: raise NotImplementedError cleo-2.1.0/src/cleo/io/outputs/section_output.py000066400000000000000000000057701451777547300217720ustar00rootroot00000000000000from __future__ import annotations import math from typing import TYPE_CHECKING from typing import TextIO from cleo.io.outputs.output import Verbosity from cleo.io.outputs.stream_output import StreamOutput from cleo.terminal import Terminal if TYPE_CHECKING: from cleo.formatters.formatter import Formatter class SectionOutput(StreamOutput): def __init__( self, stream: TextIO, sections: list[SectionOutput], verbosity: Verbosity = Verbosity.NORMAL, decorated: bool | None = None, formatter: Formatter | None = None, ) -> None: super().__init__( stream, verbosity=verbosity, decorated=decorated, formatter=formatter ) self._content: list[str] = [] self._lines = 0 sections.insert(0, self) self._sections = sections self._terminal = Terminal().size @property def content(self) -> str: return "".join(self._content) @property def lines(self) -> int: return self._lines def clear(self, lines: int | None = None) -> None: if not (self._content and self.is_decorated()): 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()._write( self._pop_stream_content_until_current_section(lines), new_line=False ) def overwrite(self, message: str) -> None: self.clear() self.write_line(message) def add_content(self, content: str) -> None: for line_content in content.split("\n"): self._lines += ( math.ceil( len(self.remove_format(line_content).replace("\t", " " * 8)) / self._terminal.width ) or 1 ) self._content.append(line_content) self._content.append("\n") def _write(self, message: str, new_line: bool = False) -> None: if not self.is_decorated(): return super()._write(message, new_line=new_line) erased_content = self._pop_stream_content_until_current_section() self.add_content(message) super()._write(message, new_line=True) super()._write(erased_content, new_line=False) def _pop_stream_content_until_current_section( self, lines_to_clear_count: int = 0 ) -> 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()._write(f"\x1b[{lines_to_clear_count}A", new_line=False) # Erase to end of screen super()._write("\x1b[0J", new_line=False) return "".join(reversed(erased_content)) cleo-2.1.0/src/cleo/io/outputs/stream_output.py000066400000000000000000000102731451777547300216130ustar00rootroot00000000000000from __future__ import annotations import codecs import io import locale import os import sys from typing import TYPE_CHECKING from typing import TextIO from typing import cast from cleo.io.outputs.output import Output from cleo.io.outputs.output import Verbosity if TYPE_CHECKING: from cleo.formatters.formatter import Formatter from cleo.io.outputs.section_output import SectionOutput class StreamOutput(Output): FILE_TYPE_CHAR = 0x0002 FILE_TYPE_REMOTE = 0x8000 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 def __init__( self, stream: TextIO, verbosity: Verbosity = Verbosity.NORMAL, decorated: bool | None = None, formatter: Formatter | None = None, ) -> None: self._stream = stream self._supports_utf8 = self._get_utf8_support_info() super().__init__( verbosity=verbosity, decorated=decorated or self._has_color_support(), formatter=formatter, ) @property def stream(self) -> TextIO: return self._stream def supports_utf8(self) -> bool: return self._supports_utf8 def _get_utf8_support_info(self) -> bool: """ Returns whether the stream supports the UTF-8 encoding. """ encoding = self._stream.encoding or locale.getpreferredencoding(False) try: return codecs.lookup(encoding).name == "utf-8" except Exception: return True def flush(self) -> None: self._stream.flush() def section(self) -> SectionOutput: from cleo.io.outputs.section_output import SectionOutput return SectionOutput( self._stream, self._section_outputs, verbosity=self.verbosity, decorated=self.is_decorated(), formatter=self.formatter, ) def _write(self, message: str, new_line: bool = False) -> None: if new_line: message += "\n" self._stream.write(message) self._stream.flush() def _has_color_support(self) -> bool: # Follow https://no-color.org/ if "NO_COLOR" in os.environ: return False if os.getenv("TERM_PROGRAM") == "Hyper": return True if sys.platform == "win32": shell_supported = ( os.getenv("ANSICON") is not None or os.getenv("ConEmuANSI") == "ON" # noqa: SIM112 or os.getenv("TERM") == "xterm" ) 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 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) & ~self.FILE_TYPE_REMOTE ) != self.FILE_TYPE_CHAR: return False mode = ctypes.wintypes.DWORD() if not kernel32.GetConsoleMode(h, ctypes.byref(mode)): return False if (mode.value & self.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0: return True return cast( bool, kernel32.SetConsoleMode( h, mode.value | self.ENABLE_VIRTUAL_TERMINAL_PROCESSING ) != 0, ) if not hasattr(self._stream, "fileno"): return False try: return os.isatty(self._stream.fileno()) except io.UnsupportedOperation: return False cleo-2.1.0/src/cleo/loaders/000077500000000000000000000000001451777547300156225ustar00rootroot00000000000000cleo-2.1.0/src/cleo/loaders/__init__.py000066400000000000000000000000001451777547300177210ustar00rootroot00000000000000cleo-2.1.0/src/cleo/loaders/command_loader.py000066400000000000000000000010741451777547300211420ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from cleo.commands.command import Command class CommandLoader: @property def names(self) -> list[str]: """ All registered command names. """ raise NotImplementedError def get(self, name: str) -> Command: """ Loads a command. """ raise NotImplementedError def has(self, name: str) -> bool: """ Checks whether a command exists or not. """ raise NotImplementedError cleo-2.1.0/src/cleo/loaders/factory_command_loader.py000066400000000000000000000014641451777547300226740ustar00rootroot00000000000000from __future__ import annotations from typing import Callable from cleo.commands.command import Command from cleo.exceptions import CleoCommandNotFoundError from cleo.loaders.command_loader import CommandLoader Factory = Callable[[], Command] class FactoryCommandLoader(CommandLoader): """ A simple command loader using factories to instantiate commands lazily. """ def __init__(self, factories: dict[str, Factory]) -> None: self._factories = factories @property def names(self) -> list[str]: return list(self._factories) def has(self, name: str) -> bool: return name in self._factories def get(self, name: str) -> Command: if name not in self._factories: raise CleoCommandNotFoundError(name) return self._factories[name]() cleo-2.1.0/src/cleo/py.typed000066400000000000000000000000001451777547300156560ustar00rootroot00000000000000cleo-2.1.0/src/cleo/terminal.py000066400000000000000000000035731451777547300163660ustar00rootroot00000000000000from __future__ import annotations import os import sys from typing import NamedTuple class TerminalSize(NamedTuple): width: int height: int class Terminal: def __init__( self, width: int | None = None, height: int | None = None, fallback: tuple[int, int] | None = None, ) -> None: self._width = width self._height = height self._fallback = TerminalSize(*(fallback or (80, 25))) @property def width(self) -> int: return self.size.width @property def height(self) -> int: return self.size.height @property def size(self) -> TerminalSize: return self._get_terminal_size() def _get_terminal_size(self) -> TerminalSize: if not (self._width is None or self._height is None): return TerminalSize(self._width, self._height) width = 0 height = 0 columns = os.environ.get("COLUMNS") if columns is not None and columns.isdigit(): width = int(columns) lines = os.environ.get("LINES") if lines is not None and lines.isdigit(): height = int(lines) if width <= 0 or height <= 0: try: os_size = os.get_terminal_size(sys.__stdout__.fileno()) size = TerminalSize(*os_size) except (AttributeError, ValueError, OSError): # stdout is None, closed, detached, or not a terminal, or # os.get_terminal_size() is unsupported # noqa: ERA001 size = self._fallback if width <= 0: width = size.width or self._fallback.width if height <= 0: height = size.height or self._fallback.height return TerminalSize( width if self._width is None else self._width, height if self._height is None else self._height, ) cleo-2.1.0/src/cleo/testers/000077500000000000000000000000001451777547300156625ustar00rootroot00000000000000cleo-2.1.0/src/cleo/testers/__init__.py000066400000000000000000000000001451777547300177610ustar00rootroot00000000000000cleo-2.1.0/src/cleo/testers/application_tester.py000066400000000000000000000037171451777547300221350ustar00rootroot00000000000000from __future__ import annotations from io import StringIO from typing import TYPE_CHECKING from cleo.io.buffered_io import BufferedIO from cleo.io.inputs.string_input import StringInput from cleo.io.outputs.buffered_output import BufferedOutput if TYPE_CHECKING: from cleo.application import Application from cleo.io.outputs.output import Verbosity class ApplicationTester: """ Eases the testing of console applications. """ def __init__(self, application: Application) -> None: self._application = application self._application.auto_exits(False) self._io = BufferedIO() self._status_code = 0 @property def application(self) -> Application: return self._application @property def io(self) -> BufferedIO: return self._io @property def status_code(self) -> int: return self._status_code def execute( self, args: str = "", inputs: str | None = None, interactive: bool = True, verbosity: Verbosity | None = None, decorated: bool = False, supports_utf8: bool = True, ) -> int: """ Executes the command """ self._io.clear() self._io.set_input(StringInput(args)) self._io.decorated(decorated) assert isinstance(self._io.output, BufferedOutput) assert isinstance(self._io.error_output, BufferedOutput) self._io.output.set_supports_utf8(supports_utf8) self._io.error_output.set_supports_utf8(supports_utf8) if inputs is not None: self._io.input.set_stream(StringIO(inputs)) if interactive is not None: self._io.interactive(interactive) if verbosity is not None: self._io.set_verbosity(verbosity) self._status_code = self._application.run( self._io.input, self._io.output, self._io.error_output, ) return self._status_code cleo-2.1.0/src/cleo/testers/command_tester.py000066400000000000000000000050571451777547300212470ustar00rootroot00000000000000from __future__ import annotations from io import StringIO from typing import TYPE_CHECKING from cleo.io.buffered_io import BufferedIO from cleo.io.inputs.argv_input import ArgvInput from cleo.io.inputs.string_input import StringInput from cleo.io.outputs.buffered_output import BufferedOutput if TYPE_CHECKING: from cleo.commands.command import Command from cleo.io.outputs.output import Verbosity class CommandTester: """ Eases the testing of console commands. """ def __init__(self, command: Command) -> None: self._command = command self._io = BufferedIO() self._inputs: list[str] = [] self._status_code: int | None = None @property def command(self) -> Command: return self._command @property def io(self) -> BufferedIO: return self._io @property def status_code(self) -> int | None: return self._status_code def execute( self, args: str = "", inputs: str | None = None, interactive: bool | None = None, verbosity: Verbosity | None = None, decorated: bool | None = None, supports_utf8: bool = True, ) -> int: """ Executes the command """ application = self._command.application input_: StringInput | ArgvInput = StringInput(args) if ( application is not None and application.definition.has_argument("command") and self._command.name is not None ): name = self._command.name if " " in name: # If the command is namespaced we rearrange # the input to parse it as a single argument argv = [application.name, self._command.name, *input_._tokens] input_ = ArgvInput(argv) else: input_ = StringInput(name + " " + args) self._io.set_input(input_) assert isinstance(self._io.output, BufferedOutput) assert isinstance(self._io.error_output, BufferedOutput) self._io.output.set_supports_utf8(supports_utf8) self._io.error_output.set_supports_utf8(supports_utf8) if inputs is not None: self._io.input.set_stream(StringIO(inputs)) if interactive is not None: self._io.interactive(interactive) if verbosity is not None: self._io.set_verbosity(verbosity) if decorated is not None: self._io.decorated(decorated) self._status_code = self._command.run(self._io) return self._status_code cleo-2.1.0/src/cleo/ui/000077500000000000000000000000001451777547300146065ustar00rootroot00000000000000cleo-2.1.0/src/cleo/ui/__init__.py000066400000000000000000000000001451777547300167050ustar00rootroot00000000000000cleo-2.1.0/src/cleo/ui/choice_question.py000066400000000000000000000102501451777547300203370ustar00rootroot00000000000000from __future__ import annotations import re from typing import TYPE_CHECKING from typing import Any from typing import cast from cleo.exceptions import CleoValueError from cleo.ui.question import Question if TYPE_CHECKING: from cleo.io.io import IO class SelectChoiceValidator: def __init__(self, question: ChoiceQuestion) -> None: """ Constructor. """ self._question = question self._values = question.choices def validate(self, selected: Any) -> str | list[str] | None: """ Validate a choice. """ # Collapse all spaces. if isinstance(selected, int): selected = str(selected) if selected is None: return None if self._question.supports_multiple_choices(): # Check for a separated comma values _selected = selected.replace(" ", "") if not re.match(r"^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$", _selected): raise CleoValueError(self._question.error_message.format(selected)) selected_choices = _selected.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 CleoValueError( "The provided answer is ambiguous. " f"Value should be one of {' or '.join(str(r) for r in results)}." ) if value in self._values: result = value elif value.isdigit() and 0 <= int(value) < len(self._values): result = self._values[int(value)] else: raise CleoValueError(self._question.error_message.format(value)) multiselect_choices.append(result) if self._question.supports_multiple_choices(): return multiselect_choices return cast("str | list[str] | None", multiselect_choices[0]) class ChoiceQuestion(Question): """ Multiple choice question. """ def __init__( self, question: str, choices: list[str], default: Any | None = None ) -> None: super().__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) -> str: return self._error_message @property def choices(self) -> list[str]: return self._choices def supports_multiple_choices(self) -> bool: return self._multi_select def set_multi_select(self, multi_select: bool) -> None: self._multi_select = multi_select def set_error_message(self, message: str) -> None: self._error_message = message def _write_prompt(self, io: IO) -> None: """ Outputs the question prompt. """ message = self._question default = self._default if default is None: message = f"{message}: " elif self._multi_select: choices = self._choices default = default.split(",") for i, value in enumerate(default): default[i] = choices[int(value.strip())] message = ( f"{message} " f"[{', '.join(default)}]:" ) else: choices = self._choices message = ( f"{message} " f"[{choices[int(default)]}]:" ) width = len(str(len(self._choices) - 1)) if len(self._choices) > 1 else 1 messages = [message] for key, value in enumerate(self._choices): messages.append(f" [{key: {width}}] {value}") io.write_error_line("\n".join(messages)) message = self._prompt io.write_error(message) cleo-2.1.0/src/cleo/ui/component.py000066400000000000000000000001341451777547300171600ustar00rootroot00000000000000from __future__ import annotations class Component: name: str = "" cleo-2.1.0/src/cleo/ui/confirmation_question.py000066400000000000000000000021721451777547300216010ustar00rootroot00000000000000from __future__ import annotations import re from typing import TYPE_CHECKING from cleo.ui.question import Question if TYPE_CHECKING: from cleo.io.io import IO class ConfirmationQuestion(Question): """ Represents a yes/no question. """ def __init__( self, question: str, default: bool = True, true_answer_regex: str = r"(?i)^y" ) -> None: super().__init__(question, default) self._true_answer_regex = true_answer_regex self._normalizer = self._default_normalizer def _write_prompt(self, io: IO) -> None: message = ( f"{self._question} (yes/no) " f'[{"yes" if self._default else "no"}] ' ) io.write_error(message) def _default_normalizer(self, answer: str) -> bool: """ 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 bool(answer and answer_is_true) return not answer or answer_is_true cleo-2.1.0/src/cleo/ui/exception_trace.py000066400000000000000000000353561451777547300203500ustar00rootroot00000000000000from __future__ import annotations import ast import builtins import inspect import io import keyword import os import re import sys import tokenize from typing import TYPE_CHECKING from typing import ClassVar from crashtest.frame_collection import FrameCollection from cleo.formatters.formatter import Formatter if TYPE_CHECKING: from crashtest.frame import Frame from crashtest.solution_providers.solution_provider_repository import ( SolutionProviderRepository, ) from cleo.io.io import IO from cleo.io.outputs.output import Output class Highlighter: 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: ClassVar[dict[str, str]] = { 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: ClassVar[set[str]] = set(keyword.kwlist) BUILTINS: ClassVar[set[str]] = set(dir(builtins)) UI: ClassVar[dict[bool, dict[str, str]]] = { False: {"arrow": ">", "delimiter": "|"}, True: {"arrow": "→", "delimiter": "│"}, } def __init__(self, supports_utf8: bool = True) -> None: self._theme = self.DEFAULT_THEME.copy() self._ui = self.UI[supports_utf8] def code_snippet( self, source: str, line: int, lines_before: int = 2, lines_after: int = 2 ) -> list[str]: 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 return token_lines[offset : offset + length] def highlighted_lines(self, source: str) -> list[str]: source = source.replace("\r\n", "\n").replace("\r", "\n") return self.split_to_lines(source) def split_to_lines(self, source: str) -> list[str]: lines = [] current_line = 1 current_col = 0 buffer = "" current_type = None source_io = io.BytesIO(source.encode()) formatter = Formatter() def readline() -> bytes: return formatter.format( formatter.escape(source_io.readline().decode()) ).encode() 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 if current_type is None: current_type = self.TOKEN_DEFAULT line += f"<{self._theme[current_type]}>{buffer}" lines.append(line) break if lineno > current_line: if current_type is None: current_type = self.TOKEN_DEFAULT diff = lineno - current_line if diff > 1: lines += [""] * (diff - 1) stripped_buffer = buffer.rstrip("\n") line += f"<{self._theme[current_type]}>{stripped_buffer}" # 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 += f"<{self._theme[current_type]}>{buffer}" buffer = "" current_type = new_type if lineno < end[0]: # The token spans multiple lines token_lines = token_string.split("\n") line += f"<{self._theme[current_type]}>{token_lines[0]}" lines.append(line) for token_line in token_lines[1:-1]: lines.append(f"<{self._theme[current_type]}>{token_line}") 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: list[str], mark_line: int | None = None) -> list[str]: max_line_length = max(3, len(str(len(lines)))) snippet_lines = [] marker = f"<{self._theme[self.LINE_MARKER]}>{self._ui['arrow']} " no_marker = " " for i, line in enumerate(lines): snippet = "" if mark_line is not None: snippet = marker if mark_line == i + 1 else no_marker line_number = f"{i + 1:>{max_line_length}}" styling = ( "fg=default;options=bold" if mark_line == i + 1 else self._theme[self.LINE_NUMBER] ) snippet += ( f"<{styling}>" f"{line_number}<{self._theme[self.LINE_NUMBER]}>" f"{self._ui['delimiter']} {line}" ) snippet_lines.append(snippet) return snippet_lines class ExceptionTrace: """ Renders the trace of an exception. """ THEME: ClassVar[dict[str, str]] = { "comment": "", "keyword": "", "builtin": "", "literal": "", } AST_ELEMENTS: ClassVar[dict[str, list[str]]] = { "builtins": 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: ClassVar[dict[tuple[Frame, int, int], list[str]]] = {} def __init__( self, exception: Exception, solution_provider_repository: SolutionProviderRepository | None = None, ) -> None: self._exception = exception self._solution_provider_repository = solution_provider_repository self._exc_info = sys.exc_info() self._ignore: str | None = None def ignore_files_in(self, ignore: str) -> ExceptionTrace: self._ignore = ignore return self def render(self, io: IO | Output, simple: bool = False) -> None: # If simple rendering wouldn't show anything useful, abandon it. simple_string = str(self._exception) if simple else "" if simple_string: io.write_line("") io.write_line(f"{simple_string}") else: self._render_exception(io, self._exception) self._render_solution(io, self._exception) def _render_exception(self, io: IO | Output, exception: BaseException) -> None: from crashtest.inspector import Inspector inspector = Inspector(exception) if not inspector.frames: return if inspector.has_previous_exception(): assert inspector.previous_exception is not None # make mypy happy self._render_exception(io, inspector.previous_exception) io.write_line("") io.write_line( "The following error occurred when trying to handle this error:" ) io.write_line("") self._render_trace(io, inspector.frames) self._render_line(io, f"{inspector.exception_name}", True) io.write_line("") exception_message = ( Formatter().format(inspector.exception_message).replace("\n", "\n ") ) self._render_line(io, f"{exception_message}") current_frame = inspector.frames[-1] self._render_snippet(io, current_frame) def _render_snippet(self, io: IO | Output, frame: Frame) -> None: self._render_line( io, f"at {self._get_relative_file_path(frame.filename)}" f":{frame.lineno} in {frame.function}", True, ) code_lines = Highlighter(supports_utf8=io.supports_utf8()).code_snippet( frame.file_content, frame.lineno, 4, 4 ) for code_line in code_lines: self._render_line(io, code_line, indent=4) def _render_solution(self, io: IO | Output, exception: Exception) -> None: if self._solution_provider_repository is None: return solutions = self._solution_provider_repository.get_solutions_for_exception( exception ) symbol = "•" if io.supports_utf8() else "*" for solution in solutions: title = solution.solution_title description = solution.solution_description links = solution.documentation_links description = description.replace("\n", "\n ").strip(" ") joined_links = ",".join(f"\n {link}" for link in links) self._render_line( io, f"{symbol} " f"{title.rstrip('.')}:" f" {description}{joined_links}", True, ) def _render_trace(self, io: IO | Output, frames: FrameCollection) -> None: 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_very_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 = f"{len(collection)} frames" else: frames_message = "frame" self._render_line( io, f"{'...':>{max_frame_length}} " f"Previous {frames_message} repeated " f"{collection.repetitions + 1} times", True, ) i -= len(collection) * (collection.repetitions + 1) for frame in collection: relative_file_path = self._get_relative_file_path(frame.filename) relative_file_path_parts = relative_file_path.split(os.path.sep) relative_file_path = ( f"{Formatter.escape(os.sep)}".join( relative_file_path_parts[:-1] + [ "" f"{relative_file_path_parts[-1]}" ] ) ) self._render_line( io, f"{i:>{max_frame_length}} " f"{relative_file_path}:" f"{frame.lineno} in {frame.function}", True, ) if io.is_debug(): if (frame, 2, 2) not in self._FRAME_SNIPPET_CACHE: code_lines = Highlighter( supports_utf8=io.supports_utf8() ).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, f"{' ' * max_frame_length}{code_line}", indent=3, ) else: highlighter = Highlighter(supports_utf8=io.supports_utf8()) try: code_line = highlighter.highlighted_lines( frame.line.strip() )[0] except tokenize.TokenError: code_line = frame.line.strip() self._render_line( io, f"{' ' * (max_frame_length + 4)}{code_line}" ) i -= 1 def _render_line( self, io: IO | Output, line: str, new_line: bool = False, indent: int = 2 ) -> None: if new_line: io.write_line("") io.write_line(f"{indent * ' '}{line}") def _get_relative_file_path(self, filepath: str) -> str: 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 cleo-2.1.0/src/cleo/ui/progress_bar.py000066400000000000000000000305421451777547300176540ustar00rootroot00000000000000from __future__ import annotations import math import re import time from typing import TYPE_CHECKING from typing import ClassVar from typing import Match from cleo._utils import format_time from cleo.cursor import Cursor from cleo.io.io import IO from cleo.io.outputs.section_output import SectionOutput from cleo.terminal import Terminal from cleo.ui.component import Component if TYPE_CHECKING: from cleo.io.outputs.output import Output class ProgressBar(Component): """ The ProgressBar provides helpers to display progress output. """ name = "progress_bar" # Options bar_width = 28 bar_char = None empty_bar_char = "-" progress_char = ">" redraw_freq: int | None = 1 formats: ClassVar[dict[str, str]] = { "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: IO | Output, max: int = 0, min_seconds_between_redraws: float = 0.1, ) -> None: # 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().size self._max = 0 self._step_width: int = 1 self._set_max_steps(max) self._step = 0 self._percent = 0.0 self._format: str | None = None self._internal_format: str | None = None self._format_line_count = 0 self._previous_message: str | None = None self._should_overwrite = True self._min_seconds_between_redraws = 0.0 self._max_seconds_between_redraws = 1.0 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.formatter.is_decorated(): # 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: dict[str, str] = {} self._start_time = time.time() self._last_write_time = 0.0 self._cursor = Cursor(self._io) def set_message(self, message: str, name: str = "message") -> None: self._messages[name] = message def get_message(self, name: str = "message") -> str: return self._messages[name] def get_start_time(self) -> float: return self._start_time def get_max_steps(self) -> int: return self._max def get_progress(self) -> int: return self._step def get_progress_percent(self) -> float: return self._percent def set_bar_character(self, character: str) -> ProgressBar: self.bar_char = character return self def get_bar_character(self) -> str: if self.bar_char is None: if self._max: return "=" return self.empty_bar_char return self.bar_char def set_bar_width(self, width: int) -> ProgressBar: self.bar_width = width return self def get_empty_bar_character(self) -> str: return self.empty_bar_char def set_empty_bar_character(self, character: str) -> ProgressBar: self.empty_bar_char = character return self def get_progress_character(self) -> str: return self.progress_char def set_progress_character(self, character: str) -> ProgressBar: self.progress_char = character return self def set_format(self, fmt: str) -> None: self._format = None self._internal_format = fmt def set_redraw_frequency(self, freq: int) -> None: if self.redraw_freq is not None: self.redraw_freq = max(freq, 1) def min_seconds_between_redraws(self, freq: float) -> None: if freq > 0: self.redraw_freq = None self._min_seconds_between_redraws = freq def max_seconds_between_redraws(self, freq: float) -> None: self._max_seconds_between_redraws = freq def start(self, max: int | None = None) -> 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: int = 1) -> None: """ Advances the progress output X steps. """ self.set_progress(self._step + step) def set_progress(self, step: int) -> None: """ Sets the current progress. """ 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 self._percent = step / (self._max or math.inf) 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) -> None: """ 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) -> None: """ 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(self._build_line()) def _overwrite_callback(self, matches: Match[str]) -> str: if hasattr(self, f"_formatter_{matches.group(1)}"): text = str(getattr(self, f"_formatter_{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): n = int(matches.group(2).lstrip("-").rstrip("s")) if matches.group(2).startswith("-"): return text.ljust(n) return text.rjust(n) return text def clear(self) -> None: """ 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: str) -> None: """ 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"] else: self._format = self.formats.get(fmt, fmt) assert self._format is not None self._format_line_count = self._format.count("\n") def _set_max_steps(self, mx: int) -> None: """ Sets the progress bar maximal steps. """ self._max = max(0, mx) self._step_width = len(str(self._max)) if self._max else 4 def _overwrite(self, message: str) -> None: """ Overwrites a previous message to the output. """ if self._previous_message == message: return original_message = message if self._should_overwrite: if self._previous_message is not None: if isinstance(self._io, SectionOutput): lines_to_clear = ( len(self._io.remove_format(message)) // self._terminal.width + self._format_line_count + 1 ) self._io.clear(lines_to_clear) else: if self._format_line_count: self._cursor.move_up(self._format_line_count) self._cursor.move_to_column(1) self._cursor.clear_line() elif self._step > 0: message = "\n" + message self._previous_message = original_message self._last_write_time = time.time() self._io.write(message) self._write_count += 1 def _determine_best_format(self) -> str: fmt = "normal" if self._io.is_debug(): fmt = "debug" elif self._io.is_very_verbose(): fmt = "very_verbose" elif self._io.is_verbose(): fmt = "verbose" return fmt if self._max else f"{fmt}_nomax" @property def bar_offset(self) -> int: if self._max: return math.floor(self._percent * self.bar_width) if self.redraw_freq is None: return math.floor( (min(5, self.bar_width // 15) * self._write_count) % self.bar_width ) return math.floor(self._step % self.bar_width) def _formatter_bar(self) -> str: 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) -> str: return format_time(time.time() - self._start_time) def _formatter_remaining(self) -> str: 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) -> int: 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: return 0 return round((time.time() - self._start_time) / self._step * self._max) def _formatter_current(self) -> str: return str(self._step).rjust(self._step_width) def _formatter_max(self) -> int: return self._max def _formatter_percent(self) -> int: return int(math.floor(self._percent * 100)) def _build_line(self) -> str: regex = re.compile(r"(?i)%([a-z\-_]+)(?::([^%]+))?%") assert self._format is not None line = regex.sub(self._overwrite_callback, self._format) # gets string length for each sub line with multiline format lines_length = [ len(self._io.remove_format(sub_line.rstrip("\r"))) for sub_line in line.split("\n") ] lines_width = max(lines_length) terminal_width = self._terminal.width if lines_width <= terminal_width: return line self.set_bar_width(self.bar_width - lines_width + terminal_width) return regex.sub(self._overwrite_callback, self._format) cleo-2.1.0/src/cleo/ui/progress_indicator.py000066400000000000000000000132601451777547300210620ustar00rootroot00000000000000from __future__ import annotations import re import threading import time from contextlib import contextmanager from typing import TYPE_CHECKING from cleo._utils import format_time from cleo.io.io import IO if TYPE_CHECKING: from typing import Iterator from typing import Match from cleo.io.outputs.output import Output class ProgressIndicator: """ 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: IO | Output, fmt: str | None = None, interval: int = 100, values: list[str] | None = None, ) -> 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: str | None = None self._update_time: int | None = None self._started = False self._current = 0 self._auto_running: threading.Event | None = None self._auto_thread: threading.Thread | None = None self._start_time: float | None = None self._last_message_length = 0 @property def message(self) -> str | None: return self._message def set_message(self, message: str | None) -> None: self._message = message self._display() @property def current_value(self) -> str: return self._values[self._current % len(self._values)] def start(self, message: 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) -> None: if not self._started: raise RuntimeError("Progress indicator has not yet been started.") if not self._io.is_decorated(): return current_time = self._get_current_time_in_milliseconds() if self._update_time is not None and current_time < self._update_time: return self._update_time = current_time + self._interval self._current += 1 self._display() def finish(self, message: str, reset_indicator: bool = False) -> None: if not self._started: raise RuntimeError("Progress indicator has not yet been started.") if not (self._auto_thread is None or self._auto_running is 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: str, end_message: str) -> Iterator[ProgressIndicator]: """ 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) -> None: while not (self._auto_running is None or self._auto_running.is_set()): self.advance() time.sleep(0.1) def _display(self) -> None: if self._io.is_quiet(): return self._overwrite( re.sub( r"(?i){([a-z\-_]+)(?::([^}]+))?}", self._overwrite_callback, self._fmt ) ) def _overwrite_callback(self, matches: Match[str]) -> str: if hasattr(self, f"_formatter_{matches.group(1)}"): return str(getattr(self, f"_formatter_{matches.group(1)}")()) return matches.group(0) def _overwrite(self, message: str) -> None: """ Overwrites a previous message to the output. """ if self._io.is_decorated(): self._io.write("\x0D\x1B[2K") self._io.write(message) else: self._io.write_line(message) def _determine_best_format(self) -> str: decorated = self._io.is_decorated() 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) -> int: return round(time.time() * 1000) def _formatter_indicator(self) -> str: return self.current_value def _formatter_message(self) -> str | None: return self.message def _formatter_elapsed(self) -> str: assert self._start_time is not None return format_time(time.time() - self._start_time) cleo-2.1.0/src/cleo/ui/question.py000066400000000000000000000177231451777547300170410ustar00rootroot00000000000000from __future__ import annotations import getpass import os import subprocess from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Callable from cleo.formatters.style import Style from cleo.io.outputs.stream_output import StreamOutput if TYPE_CHECKING: from cleo.io.io import IO Validator = Callable[[str], Any] Normalizer = Callable[[str], Any] class Question: """ A question that will be asked in a Console. """ def __init__(self, question: str, default: Any = None) -> None: self._question = question self._default = default self._attempts: int | None = None self._hidden = False self._hidden_fallback = True self._autocomplete_values: list[str] = [] self._validator: Validator = lambda s: s self._normalizer: Normalizer = lambda s: s self._error_message = 'Value "{}" is invalid' @property def question(self) -> str: return self._question @property def default(self) -> Any: return self._default @property def autocomplete_values(self) -> list[str]: return self._autocomplete_values @property def max_attempts(self) -> int | None: return self._attempts def is_hidden(self) -> bool: return self._hidden def hide(self, hidden: bool = True) -> 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: list[str]) -> None: if self.is_hidden(): raise RuntimeError("A hidden question cannot use the autocompleter.") self._autocomplete_values = autocomplete_values def set_max_attempts(self, attempts: int | None) -> None: self._attempts = attempts def set_validator(self, validator: Validator) -> None: self._validator = validator def ask(self, io: IO) -> Any: """ Asks the question to the user. """ if not io.is_interactive(): return self.default return self._validate_attempts(lambda: self._do_ask(io), io) def _do_ask(self, io: IO) -> Any: """ Asks the question to the user. """ self._write_prompt(io) if not (self._autocomplete_values and self._has_stty_available()): ret: str | None = None 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 return self._normalizer(ret) # type: ignore[arg-type] def _write_prompt(self, io: IO) -> None: """ Outputs the question prompt. """ io.write_error(f"{self._question} ") def _write_error(self, io: IO, error: Exception) -> None: """ Outputs an error message. """ io.write_error_line(f"{error!s}") def _autocomplete(self, io: IO) -> str: """ Autocomplete a question. """ autocomplete = self._autocomplete_values ret = "" i = 0 ofs = -1 matches = list(autocomplete) num_matches = len(matches) # Add highlighted text style style = Style(options=["reverse"]) io.error_output.formatter.set_style("hl", style) stty_mode = subprocess.check_output(["stty", "-g"]).decode().rstrip("\n") # Disable icanon (so we can read each keypress) and # echo (we'll do echoing here instead) subprocess.check_output(["stty", "-icanon", "-echo"]) try: # 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.write_error("\033[1D") if i == 0: ofs = -1 matches = list(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 in ["\t", "\n"]: if num_matches > 0 and ofs != -1: ret = matches[ofs] # Echo out remaining chars for current match io.write_error(ret[i:]) i = len(ret) if c == "\n": io.write_error(c) break num_matches = 0 continue else: io.write_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.write_error("\033[K") if num_matches > 0 and ofs != -1: # Save cursor position io.write_error("\0337") # Write highlighted text io.write_error("" + matches[ofs][i:] + "") # Restore cursor position io.write_error("\0338") finally: subprocess.call(["stty", f"{stty_mode}"]) return ret def _get_hidden_response(self, io: IO) -> str: """ Gets a hidden response from user. """ stream = None if isinstance(io.error_output, StreamOutput): stream = io.error_output.stream return getpass.getpass("", stream=stream) def _validate_attempts(self, interviewer: Callable[[], Any], io: IO) -> Any: """ 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 assert error raise error def _read_from_input(self, io: IO) -> str: """ Read user input. """ ret = io.read_line(4096) if not ret: raise RuntimeError("Aborted") return ret.strip() def _has_stty_available(self) -> bool: with Path(os.devnull).open("w") as devnull: try: exit_code = subprocess.call(["stty"], stdout=devnull, stderr=devnull) except Exception: exit_code = 2 return exit_code == 0 cleo-2.1.0/src/cleo/ui/table.py000066400000000000000000000567101451777547300162600ustar00rootroot00000000000000from __future__ import annotations import math import re from contextlib import suppress from copy import deepcopy from itertools import repeat from typing import TYPE_CHECKING from typing import Iterator from typing import List from typing import Union from typing import cast from cleo.formatters.formatter import Formatter from cleo.io.outputs.output import Output from cleo.ui.table_cell import TableCell from cleo.ui.table_cell_style import TableCellStyle from cleo.ui.table_separator import TableSeparator from cleo.ui.table_style import TableStyle if TYPE_CHECKING: from cleo.io.io import IO Row = List[Union[str, TableCell]] Rows = List[Union[Row, TableSeparator]] Header = Row class Table: SEPARATOR_TOP: int = 0 SEPARATOR_TOP_BOTTOM: int = 1 SEPARATOR_MID: int = 2 SEPARATOR_BOTTOM: int = 3 BORDER_OUTSIDE: int = 0 BORDER_INSIDE: int = 1 _styles: dict[str, TableStyle] | None = None def __init__(self, io: IO | Output, style: str | None = None) -> None: self._io = io if style is None: style = "default" self._header_title: str | None = None self._footer_title: str | None = None self._headers: list[Header] = [] self._rows: Rows = [] self._horizontal = False self._effective_column_widths: dict[int, int] = {} self._number_of_columns: int | None = None self._column_styles: dict[int, TableStyle] = {} self._column_widths: dict[int, int] = {} self._column_max_widths: dict[int, int] = {} self._rendered = False self._style: TableStyle | None = None self._init_styles() self.set_style(style) @property def style(self) -> TableStyle: assert self._style is not None return self._style def set_style(self, name: str) -> Table: self._init_styles() self._style = self._resolve_style(name) return self def column_style(self, column_index: int) -> TableStyle: if column_index in self._column_styles: return self._column_styles[column_index] return self.style def set_column_style(self, column_index: int, style: str | TableStyle) -> Table: self._column_styles[column_index] = self._resolve_style(style) return self def set_column_width(self, column_index: int, width: int) -> Table: self._column_widths[column_index] = width return self def set_column_widths(self, widths: list[int]) -> Table: self._column_widths = {} for i, width in enumerate(widths): self._column_widths[i] = width return self def set_column_max_width(self, column_index: int, width: int) -> Table: self._column_widths[column_index] = width return self def set_headers(self, headers: Header | list[Header]) -> Table: if headers and not isinstance(headers[0], list): headers = cast("Header", headers) headers = [headers] headers = cast("List[Header]", headers) self._headers = headers return self def set_rows(self, rows: Rows) -> Table: self._rows = [] return self.add_rows(rows) def add_rows(self, rows: Rows) -> Table: for row in rows: self.add_row(row) return self def add_row(self, row: Row | TableSeparator) -> Table: if isinstance(row, TableSeparator): self._rows.append(row) return self self._rows.append(row) return self def set_header_title(self, header_title: str) -> Table: self._header_title = header_title return self def set_footer_title(self, footer_title: str) -> Table: self._footer_title = footer_title return self def horizontal(self, horizontal: bool = True) -> Table: self._horizontal = horizontal return self def render(self) -> None: divider = TableSeparator() if self._horizontal: rows: Rows = [] headers = self._headers[0] if self._headers else [] for i, header in enumerate(headers): rows.append([header]) for row in self._rows: if isinstance(row, TableSeparator): continue rows_i = rows[i] assert not isinstance(rows_i, TableSeparator) if len(row) > i: rows_i.append(row[i]) elif isinstance(rows_i[0], TableCell) and rows_i[0].colspan >= 2: # There is a title pass else: rows_i.append("") else: rows = [*cast("Rows", self._headers), divider, *self._rows] self._calculate_number_of_columns(rows) rows = list(self._build_table_rows(rows)) self._calculate_column_widths(rows) is_header = not self._horizontal is_first_row = self._horizontal for row in rows: if row is divider: is_header = False is_first_row = True continue if isinstance(row, TableSeparator): self._render_row_separator() continue if not row: continue if is_header or is_first_row: if is_first_row: self._render_row_separator(self.SEPARATOR_TOP_BOTTOM) is_first_row = False else: self._render_row_separator( self.SEPARATOR_TOP, self._header_title, self.style.header_title_format, ) if self._horizontal: self._render_row( row, self.style.cell_row_format, self.style.cell_header_format ) else: self._render_row( row, self.style.cell_header_format if is_header else self.style.cell_row_format, ) self._render_row_separator( self.SEPARATOR_BOTTOM, self._footer_title, self.style.footer_title_format, ) self._cleanup() self._rendered = True def _render_row_separator( self, type: int = SEPARATOR_MID, title: str | None = None, title_format: str | None = None, ) -> None: """ Renders horizontal header separator. Example: +-----+-----------+-------+ """ count = self._number_of_columns if not count: return borders = self.style.border_chars if not borders[0] and not borders[2] and not self.style.crossing_char: return crossings = self.style.crossing_chars if type == self.SEPARATOR_MID: horizontal, left_char, mid_char, right_char = ( borders[2], crossings[8], crossings[0], crossings[4], ) elif type == self.SEPARATOR_TOP: horizontal, left_char, mid_char, right_char = ( borders[0], crossings[1], crossings[2], crossings[3], ) elif type == self.SEPARATOR_TOP_BOTTOM: horizontal, left_char, mid_char, right_char = ( borders[0], crossings[9], crossings[10], crossings[11], ) else: horizontal, left_char, mid_char, right_char = ( borders[0], crossings[7], crossings[6], crossings[5], ) markup = left_char for column in range(count): markup += horizontal * self._effective_column_widths[column] markup += right_char if column == count - 1 else mid_char if title is not None: assert title_format is not None formatted_title = title_format.format(title) title_length = len(self._io.remove_format(formatted_title)) markup_length = len(markup) limit = markup_length - 4 if title_length > limit: title_length = limit format_length = len(self._io.remove_format(title_format.format(""))) formatted_title = title_format.format( title[: limit - format_length - 3] + "..." ) title_start = (markup_length - title_length) // 2 markup = ( markup[:title_start] + formatted_title + markup[title_start + title_length :] ) self._io.write_line(self.style.border_format.format(markup)) def _render_column_separator(self, type: int = BORDER_OUTSIDE) -> str: """ Renders vertical column separator. """ borders = self.style.border_chars return self.style.border_format.format( borders[1] if type == self.BORDER_OUTSIDE else borders[3] ) def _render_row( self, row: list[str], cell_format: str, first_cell_format: str | None = None ) -> None: """ Renders table row. Example: | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | """ row_content = self._render_column_separator(self.BORDER_OUTSIDE) columns = self._get_row_columns(row) last = len(columns) - 1 for i, column in enumerate(columns): row_content += self._render_cell( row, column, first_cell_format if first_cell_format and i == 0 else cell_format, ) row_content += self._render_column_separator( self.BORDER_OUTSIDE if i == last else self.BORDER_INSIDE ) self._io.write_line(row_content) def _render_cell(self, row: Row, column: int, cell_format: str) -> str: """ Renders a table cell with padding. """ try: cell = row[column] except IndexError: cell = "" width = self._effective_column_widths[column] if isinstance(cell, TableCell) and cell.colspan > 1: # add the width of the following columns(numbers of colspan). for next_column in range(column + 1, column + cell.colspan): width += ( self._get_column_separator_width() + self._effective_column_widths[next_column] ) style = self.column_style(column) if isinstance(cell, TableSeparator): return style.border_format.format(style.border_chars[2] * width) width += len(cell) - len(self._io.remove_format(cell)) content = style.cell_row_content_format.format(cell) pad = style.pad if isinstance(cell, TableCell) and isinstance(cell.style, TableCellStyle): is_not_styled_by_tag = not re.match( ( r"^<(\w+|((?:fg|bg|options)=[\w,]+;?)+)>" r".+<\/(\w+|((?:fg|bg|options)=[\w,]+;?)+)?>$" ), str(cell), ) if is_not_styled_by_tag: cell_format = ( cell.style.cell_format if cell.style.cell_format is not None else f"<{cell.style.tag}>{{}}" ) if "" in content: content = content.replace("", "") width -= 3 if "" in content: content = content.replace("", "") width -= len("") pad = cell.style.pad return cell_format.format(pad(content, width, style.padding_char)) def _calculate_number_of_columns(self, rows: Rows) -> None: columns = [0] for row in rows: if isinstance(row, TableSeparator): continue columns.append(self._get_number_of_columns(row)) self._number_of_columns = max(columns) def _build_table_rows(self, rows: Rows) -> Iterator[Row | TableSeparator]: unmerged_rows: dict[int, dict[int, Row]] = {} row_key = 0 while row_key < len(rows): rows = self._fill_next_rows(rows, row_key) # Remove any new line breaks and replace it with a new line for column, cell in enumerate(rows[row_key]): colspan = cell.colspan if isinstance(cell, TableCell) else 1 if column in self._column_max_widths and self._column_max_widths[ column ] < len(self._io.remove_format(cell)): assert isinstance(self._io, Output) cell = self._io.formatter.format_and_wrap( cell, self._column_max_widths[column] * colspan ) if "\n" not in cell: continue escaped = "\n".join( Formatter.escape_trailing_backslash(c) for c in cell.split("\n") ) cell = ( TableCell(escaped, colspan=cell.colspan) if isinstance(cell, TableCell) else escaped ) lines = cell.replace("\n", "\n").split("\n") for line_key, line in enumerate(lines): if colspan > 1: line = TableCell(line, colspan=colspan) if line_key == 0: row = rows[row_key] assert not isinstance(row, TableSeparator) row[column] = line else: if row_key not in unmerged_rows: unmerged_rows[row_key] = {} if line_key not in unmerged_rows[row_key]: unmerged_rows[row_key][line_key] = self._copy_row( rows, row_key ) unmerged_rows[row_key][line_key][column] = line row_key += 1 for row_key, row in enumerate(rows): yield self._fill_cells(row) if row_key in unmerged_rows: for unmerged_row in unmerged_rows[row_key].values(): yield self._fill_cells(unmerged_row) def _calculate_row_count(self) -> int: number_of_rows = len( list( self._build_table_rows( [*cast("Rows", self._headers), TableSeparator(), *self._rows] ) ) ) if self._headers: number_of_rows += 1 if self._rows: number_of_rows += 1 return number_of_rows def _fill_next_rows(self, rows: Rows, line: int) -> Rows: """ Fill rows that contains rowspan > 1. """ unmerged_rows: dict[int, dict[int, str | TableCell]] = {} for column, cell in enumerate(rows[line]): if isinstance(cell, TableCell) and cell.rowspan > 1: nb_lines = cell.rowspan - 1 lines: Row = [cell] if "\n" in cell: lines = cell.replace("\n", "\n").split( "\n" ) if len(lines) > nb_lines: nb_lines = cell.count("\n") row = rows[line] assert not isinstance(row, TableSeparator) row[column] = TableCell( lines[0], colspan=cell.colspan, style=cell.style ) # Create a two dimensional dict (rowspan x colspan) placeholder: dict[int, dict[int, str | TableCell]] = { k: {} for k in range(line + 1, line + 1 + nb_lines) } for k, v in unmerged_rows.items(): if k in placeholder: for l, m in unmerged_rows[k].items(): # noqa: E741 placeholder[k][l] = m else: placeholder[k] = v unmerged_rows = placeholder for unmerged_row_key, _ in unmerged_rows.items(): value = "" if unmerged_row_key - line < len(lines): value = lines[unmerged_row_key - line] unmerged_rows[unmerged_row_key][column] = TableCell( value, colspan=cell.colspan, style=cell.style ) if nb_lines == unmerged_row_key - line: break for unmerged_row_key, unmerged_row in unmerged_rows.items(): # we need to know if unmerged_row will be merged or inserted into rows assert self._number_of_columns is not None this_row = None if unmerged_row_key >= len(rows) else rows[unmerged_row_key] if ( this_row is not None and not isinstance(this_row, TableSeparator) and ( ( self._get_number_of_columns(this_row) + self._get_number_of_columns( list(unmerged_rows[unmerged_row_key].values()) ) ) <= self._number_of_columns ) ): # insert cell into row at cell_key position for cell_key, cell in unmerged_row.items(): this_row.insert(cell_key, cell) else: row = self._copy_row(rows, unmerged_row_key - 1) for column, cell in unmerged_row.items(): if len(cell): row[column] = unmerged_row[column] rows.insert(unmerged_row_key, row) return rows def _fill_cells(self, row: Row | TableSeparator) -> Row | TableSeparator: """ Fills cells for a row that contains colspan > 1. """ new_row = [] for cell in row: new_row.append(cell) if isinstance(cell, TableCell) and cell.colspan > 1: # insert empty value at column position new_row.extend(repeat("", cell.colspan - 1)) return new_row or row def _copy_row(self, rows: Rows, line: int) -> Row: """ Copies a row. """ row = list(rows[line]) for cell_key, cell_value in enumerate(row): row[cell_key] = "" if isinstance(cell_value, TableCell): row[cell_key] = TableCell("", colspan=cell_value.colspan) return row def _get_number_of_columns(self, row: Row) -> int: """ Gets number of columns by row. """ columns = len(row) for column in row: if isinstance(column, TableCell): columns += column.colspan - 1 return columns def _get_row_columns(self, row: Row) -> list[int]: """ Gets list of columns for the given row. """ assert self._number_of_columns is not None columns = list(range(self._number_of_columns)) for cell_key, cell in enumerate(row): if isinstance(cell, TableCell) and cell.colspan > 1: # exclude grouped columns. columns = [ column for column in columns if column not in range(cell_key + 1, cell_key + cell.colspan) ] return columns def _calculate_column_widths(self, rows: Rows) -> None: """ Calculates column widths. """ assert self._number_of_columns is not None for column in range(self._number_of_columns): lengths = [0] for row in rows: if isinstance(row, TableSeparator): continue row_ = row.copy() for i, cell in enumerate(row_): if isinstance(cell, TableCell): text_content = self._io.remove_format(cell) text_length = len(text_content) if text_length: length = math.ceil(text_length / cell.colspan) content_columns = [ text_content[i : i + length] for i in range(0, text_length, length) ] for position, content in enumerate(content_columns): try: row_[i + position] = content except IndexError: row_.append(content) lengths.append(self._get_cell_width(row_, column)) self._effective_column_widths[column] = ( max(lengths) + len(self.style.cell_row_content_format) - 2 ) def _get_column_separator_width(self) -> int: return len(self.style.border_format.format(self.style.border_chars[3])) def _get_cell_width(self, row: Row, column: int) -> int: """ Gets cell width. """ cell_width = 0 with suppress(IndexError): cell = row[column] cell_width = len(self._io.remove_format(cell)) column_width = ( self._column_widths[column] if column in self._column_widths else 0 ) cell_width = max(cell_width, column_width) if column in self._column_max_widths: return min(self._column_max_widths[column], cell_width) return cell_width def _cleanup(self) -> None: self._column_widths = {} self._number_of_columns = None @classmethod def _init_styles(cls) -> None: if cls._styles is not None: return borderless = ( TableStyle() .set_horizontal_border_chars("=") .set_vertical_border_chars(" ") .set_default_crossing_char(" ") ) compact = ( TableStyle() .set_horizontal_border_chars("") .set_vertical_border_chars(" ") .set_default_crossing_char("") .set_cell_row_content_format("{}") ) box = ( TableStyle() .set_horizontal_border_chars("─") .set_vertical_border_chars("│") .set_crossing_chars("┼", "┌", "┬", "┐", "┤", "┘", "┴", "└", "├") ) box_double = ( TableStyle() .set_horizontal_border_chars("═", "─") .set_vertical_border_chars("║", "│") .set_crossing_chars( "┼", "╔", "╤", "╗", "╢", "╝", "╧", "╚", "╟", "╠", "╪", "╣" ) ) cls._styles = { "default": TableStyle(), "borderless": borderless, "compact": compact, "box": box, "box-double": box_double, } @classmethod def _resolve_style(cls, name: str | TableStyle) -> TableStyle: if isinstance(name, TableStyle): return name assert cls._styles is not None if name in cls._styles: return deepcopy(cls._styles[name]) raise ValueError(f'Table style "{name}" is not defined.') cleo-2.1.0/src/cleo/ui/table_cell.py000066400000000000000000000015621451777547300172520ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from cleo.ui.table_cell_style import TableCellStyle class TableCell(str): def __new__( cls, value: str = "", rowspan: int = 1, colspan: int = 1, style: TableCellStyle | None = None, ) -> TableCell: return super().__new__(cls, value) def __init__( self, value: str = "", rowspan: int = 1, colspan: int = 1, style: TableCellStyle | None = None, ) -> None: self._rowspan = rowspan self._colspan = colspan self._style = style @property def rowspan(self) -> int: return self._rowspan @property def colspan(self) -> int: return self._colspan @property def style(self) -> TableCellStyle | None: return self._style cleo-2.1.0/src/cleo/ui/table_cell_style.py000066400000000000000000000023011451777547300204620ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: import sys if sys.version_info >= (3, 8): from typing import Literal else: from typing_extensions import Literal _Align = Literal["left", "right"] class TableCellStyle: def __init__( self, fg: str = "default", bg: str = "default", options: list[str] | None = None, align: _Align = "left", cell_format: str | None = None, ) -> None: self._fg = fg self._bg = bg self._options = options self._align = "left" self._cell_format = cell_format @property def cell_format(self) -> str | None: return self._cell_format @property def tag(self) -> str: tag = " str: if self._align == "left": return string.rjust(length, char) if self._align == "right": return string.ljust(length, char) return string.center(length, char) cleo-2.1.0/src/cleo/ui/table_separator.py000066400000000000000000000002551451777547300203310ustar00rootroot00000000000000from __future__ import annotations from cleo.ui.table_cell import TableCell class TableSeparator(TableCell): def __init__(self) -> None: super().__init__("") cleo-2.1.0/src/cleo/ui/table_style.py000066400000000000000000000247671451777547300175070ustar00rootroot00000000000000from __future__ import annotations class TableStyle: """ Defines styles for Table instances. """ def __init__(self) -> None: self._padding_char = " " self._horizontal_outside_border_char = "-" self._horizontal_inside_border_char = "-" self._vertical_outside_border_char = "|" self._vertical_inside_border_char = "|" self._crossing_char = "+" self._crossing_top_right_char = "+" self._crossing_top_mid_char = "+" self._crossing_top_left_char = "+" self._crossing_mid_right_char = "+" self._crossing_bottom_right_char = "+" self._crossing_bottom_mid_char = "+" self._crossing_bottom_left_char = "+" self._crossing_mid_left_char = "+" self._crossing_top_left_bottom_char = "+" self._crossing_top_mid_bottom_char = "+" self._crossing_top_right_bottom_char = "+" self._header_title_format = " {} " self._footer_title_format = " {} " self._cell_header_format = "{}" self._cell_row_format = "{}" self._cell_row_content_format = " {} " self._border_format = "{}" self._pad_type = "right" @property def padding_char(self) -> str: return self._padding_char @property def border_chars(self) -> list[str]: return [ self._horizontal_outside_border_char, self._vertical_outside_border_char, self._horizontal_inside_border_char, self._vertical_inside_border_char, ] @property def crossing_char(self) -> str: return self._crossing_char @property def crossing_chars(self) -> list[str]: return [ self._crossing_char, self._crossing_top_left_char, self._crossing_top_mid_char, self._crossing_top_right_char, self._crossing_mid_right_char, self._crossing_bottom_right_char, self._crossing_bottom_mid_char, self._crossing_bottom_left_char, self._crossing_mid_left_char, self._crossing_top_left_bottom_char, self._crossing_top_mid_bottom_char, self._crossing_top_right_bottom_char, ] @property def cell_header_format(self) -> str: return self._cell_header_format @property def cell_row_format(self) -> str: return self._cell_row_format @property def cell_row_content_format(self) -> str: return self._cell_row_content_format @property def border_format(self) -> str: return self._border_format @property def header_title_format(self) -> str: return self._header_title_format @property def footer_title_format(self) -> str: return self._footer_title_format @property def pad_type(self) -> str: return self._pad_type def set_padding_char(self, padding_char: str) -> TableStyle: """ Sets padding character, used for cell padding. """ if not padding_char: raise ValueError("The padding char must not be empty.") self._padding_char = padding_char return self def set_horizontal_border_chars( self, outside: str, inside: str | None = None ) -> TableStyle: """ Sets horizontal border characters. ╔═══════════════╤══════════════════════════╤══════════════════╗ 1 ISBN 2 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 ║ ╚═══════════════╧══════════════════════════╧══════════════════╝ """ self._horizontal_outside_border_char = outside self._horizontal_inside_border_char = outside if inside is None else inside return self def set_vertical_border_chars( self, outside: str, inside: str | None = None ) -> TableStyle: """ Sets vertical border characters. ╔═══════════════╤══════════════════════════╤══════════════════╗ ║ ISBN │ Title │ Author ║ ╠═══════1═══════╪══════════════════════════╪══════════════════╣ ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ ╟───────2───────┼──────────────────────────┼──────────────────╢ ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ ╚═══════════════╧══════════════════════════╧══════════════════╝ """ self._vertical_outside_border_char = outside self._vertical_inside_border_char = outside if inside is None else inside return self def set_crossing_chars( self, cross: str, top_left: str, top_mid: str, top_right: str, mid_right: str, bottom_right: str, bottom_mid: str, bottom_left: str, mid_left: str, top_left_bottom: str | None = None, top_mid_bottom: str | None = None, top_right_bottom: str | None = None, ) -> TableStyle: """ Sets crossing characters. Example: 1═══════════════2══════════════════════════2══════════════════3 ║ ISBN │ Title │ Author ║ 8'══════════════0'═════════════════════════0'═════════════════4' ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ 8───────────────0──────────────────────────0──────────────────4 ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ 7═══════════════6══════════════════════════6══════════════════5 """ self._crossing_char = cross self._crossing_top_left_char = top_left self._crossing_top_mid_char = top_mid self._crossing_top_right_char = top_right self._crossing_mid_right_char = mid_right self._crossing_bottom_right_char = bottom_right self._crossing_bottom_mid_char = bottom_mid self._crossing_bottom_left_char = bottom_left self._crossing_mid_left_char = mid_left self._crossing_top_left_bottom_char = ( mid_left if top_left_bottom is None else top_left_bottom ) self._crossing_top_mid_bottom_char = ( cross if top_mid_bottom is None else top_mid_bottom ) self._crossing_top_right_bottom_char = ( mid_right if top_right_bottom is None else top_right_bottom ) return self def set_default_crossing_char(self, char: str) -> TableStyle: """ Sets default crossing character used for each cross. """ return self.set_crossing_chars( char, char, char, char, char, char, char, char, char ) def set_cell_header_format(self, cell_header_format: str) -> TableStyle: """ Sets the header cell format. """ self._cell_header_format = cell_header_format return self def set_cell_row_format(self, cell_row_format: str) -> TableStyle: """ Sets the row cell format. """ self._cell_row_format = cell_row_format return self def set_cell_row_content_format(self, cell_row_content_format: str) -> TableStyle: """ Sets the row cell content format. """ self._cell_row_content_format = cell_row_content_format return self def set_border_format(self, border_format: str) -> TableStyle: """ Sets the border format. """ self._border_format = border_format return self def set_header_title_format(self, header_title_format: str) -> TableStyle: """ Sets the header title format. """ self._header_title_format = header_title_format return self def set_footer_title_format(self, footer_title_format: str) -> TableStyle: """ Sets the footer title format. """ self._footer_title_format = footer_title_format return self def set_pad_type(self, pad_type: str) -> TableStyle: """ Sets the padding type. """ if pad_type not in {"left", "right", "center"}: raise ValueError( 'Invalid padding type. Expected one of "left", "right", "center").' ) self._pad_type = pad_type return self def pad(self, string: str, length: int, char: str = " ") -> str: if self._pad_type == "left": return string.rjust(length, char) if self._pad_type == "right": return string.ljust(length, char) return string.center(length, char) cleo-2.1.0/src/cleo/ui/ui.py000066400000000000000000000016361451777547300156030ustar00rootroot00000000000000from __future__ import annotations from cleo.exceptions import CleoValueError from cleo.ui.component import Component class UI: def __init__(self, components: list[Component] | None = None) -> None: self._components: dict[str, Component] = {} for component in components or []: self.register(component) def register(self, component: Component) -> None: if not isinstance(component, Component): raise CleoValueError( "A UI component must inherit from the Component class." ) if not component.name: raise CleoValueError("A UI component cannot be anonymous.") self._components[component.name] = component def component(self, name: str) -> Component: if name not in self._components: raise CleoValueError(f'UI component "{name}" does not exist.') return self._components[name] cleo-2.1.0/tests/000077500000000000000000000000001451777547300136225ustar00rootroot00000000000000cleo-2.1.0/tests/__init__.py000066400000000000000000000000001451777547300157210ustar00rootroot00000000000000cleo-2.1.0/tests/commands/000077500000000000000000000000001451777547300154235ustar00rootroot00000000000000cleo-2.1.0/tests/commands/__init__.py000066400000000000000000000000001451777547300175220ustar00rootroot00000000000000cleo-2.1.0/tests/commands/completion/000077500000000000000000000000001451777547300175745ustar00rootroot00000000000000cleo-2.1.0/tests/commands/completion/__init__.py000066400000000000000000000000001451777547300216730ustar00rootroot00000000000000cleo-2.1.0/tests/commands/completion/fixtures/000077500000000000000000000000001451777547300214455ustar00rootroot00000000000000cleo-2.1.0/tests/commands/completion/fixtures/__init__.py000066400000000000000000000000001451777547300235440ustar00rootroot00000000000000cleo-2.1.0/tests/commands/completion/fixtures/bash.txt000066400000000000000000000030331451777547300231220ustar00rootroot00000000000000_my_function() { local cur script coms opts com COMPREPLY=() _get_comp_words_by_ref -n : cur words # for an alias, get the real script behind it if [[ $(type -t ${words[0]}) == "alias" ]]; then script=$(alias ${words[0]} | sed -E "s/alias ${words[0]}='(.*)'/\1/") else script=${words[0]} fi # lookup for command for word in ${words[@]:1}; do if [[ $word != -* ]]; then com=$word break fi done # completing for an option if [[ ${cur} == --* ]] ; then opts="--ansi --help --no-ansi --no-interaction --quiet --verbose --version" case "$com" in (command:with:colons) opts="${opts} --goodbye" ;; (hello) opts="${opts} --dangerous-option --option-without-description" ;; (help) opts="${opts} " ;; (list) opts="${opts} " ;; ('spaced command') opts="${opts} --goodbye" ;; esac COMPREPLY=($(compgen -W "${opts}" -- ${cur})) __ltrim_colon_completions "$cur" return 0; fi # completing for a command if [[ $cur == $com ]]; then coms="command:with:colons hello help list 'spaced command'" COMPREPLY=($(compgen -W "${coms}" -- ${cur})) __ltrim_colon_completions "$cur" return 0 fi } complete -o default -F _my_function script complete -o default -F _my_function /path/to/my/script cleo-2.1.0/tests/commands/completion/fixtures/command_with_colons.py000066400000000000000000000006061451777547300260470ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.commands.command import Command from cleo.helpers import option if TYPE_CHECKING: from cleo.io.inputs.option import Option class CommandWithColons(Command): name = "command:with:colons" options: ClassVar[list[Option]] = [option("goodbye")] description = "Test." cleo-2.1.0/tests/commands/completion/fixtures/command_with_space_in_name.py000066400000000000000000000011161451777547300273300ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.commands.command import Command from cleo.helpers import argument from cleo.helpers import option if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class SpacedCommand(Command): name = "spaced command" description = "Command with space in name." arguments: ClassVar[list[Argument]] = [ argument("test", description="test argument") ] options: ClassVar[list[Option]] = [option("goodbye")] cleo-2.1.0/tests/commands/completion/fixtures/fish.txt000066400000000000000000000041711451777547300231420ustar00rootroot00000000000000function __fish_my_function_no_subcommand for i in (commandline -opc) if contains -- $i command:with:colons hello help list spaced return 1 end end return 0 end # global options complete -c script -n '__fish_my_function_no_subcommand' -l ansi -d 'Force ANSI output.' complete -c script -n '__fish_my_function_no_subcommand' -l help -d 'Display help for the given command. When no command is given display help for the list command.' complete -c script -n '__fish_my_function_no_subcommand' -l no-ansi -d 'Disable ANSI output.' complete -c script -n '__fish_my_function_no_subcommand' -l no-interaction -d 'Do not ask any interactive question.' complete -c script -n '__fish_my_function_no_subcommand' -l quiet -d 'Do not output any message.' complete -c script -n '__fish_my_function_no_subcommand' -l verbose -d 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug.' complete -c script -n '__fish_my_function_no_subcommand' -l version -d 'Display this application version.' # commands complete -c script -f -n '__fish_my_function_no_subcommand' -a command:with:colons -d 'Test.' complete -c script -f -n '__fish_my_function_no_subcommand' -a hello -d 'Complete me please.' complete -c script -f -n '__fish_my_function_no_subcommand' -a help -d 'Displays help for a command.' complete -c script -f -n '__fish_my_function_no_subcommand' -a list -d 'Lists commands.' complete -c script -f -n '__fish_my_function_no_subcommand' -a spaced complete -c script -f -n '__fish_seen_subcommand_from spaced; and not __fish_seen_subcommand_from command' -a command -d 'Command with space in name.' # command options # command:with:colons complete -c script -n '__fish_seen_subcommand_from command:with:colons' -l goodbye -d '' # hello complete -c script -n '__fish_seen_subcommand_from hello' -l dangerous-option -d 'This $hould be `escaped`.' complete -c script -n '__fish_seen_subcommand_from hello' -l option-without-description -d '' # help # list # spaced command complete -c script -n '__fish_seen_subcommand_from spaced; and __fish_seen_subcommand_from command' -l goodbye -d '' cleo-2.1.0/tests/commands/completion/fixtures/hello_command.py000066400000000000000000000010531451777547300246170ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.commands.command import Command from cleo.helpers import option if TYPE_CHECKING: from cleo.io.inputs.option import Option class HelloCommand(Command): name = "hello" options: ClassVar[list[Option]] = [ option( "dangerous-option", flag=False, description="This $hould be `escaped`.", ), option("option-without-description"), ] description = "Complete me please." cleo-2.1.0/tests/commands/completion/fixtures/zsh.txt000066400000000000000000000033701451777547300230150ustar00rootroot00000000000000#compdef script _my_function() { local state com cur local -a opts local -a coms cur=${words[${#words[@]}]} # lookup for command for word in ${words[@]:1}; do if [[ $word != -* ]]; then com=$word break fi done if [[ ${cur} == --* ]]; then state="option" opts+=("--ansi:Force ANSI output." "--help:Display help for the given command. When no command is given display help for the list command." "--no-ansi:Disable ANSI output." "--no-interaction:Do not ask any interactive question." "--quiet:Do not output any message." "--verbose:Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug." "--version:Display this application version.") elif [[ $cur == $com ]]; then state="command" coms+=("command\:with\:colons:Test." "hello:Complete me please." "help:Displays help for a command." "list:Lists commands." "'spaced command':Command with space in name.") fi case $state in (command) _describe 'command' coms ;; (option) case "$com" in (command:with:colons) opts+=("--goodbye") ;; (hello) opts+=("--dangerous-option:This \$hould be \`escaped\`." "--option-without-description") ;; (help) opts+=() ;; (list) opts+=() ;; ('spaced command') opts+=("--goodbye") ;; esac _describe 'option' opts ;; *) # fallback to file completion _arguments '*:file:_files' esac } _my_function "$@" compdef _my_function /path/to/my/script cleo-2.1.0/tests/commands/completion/test_completions_command.py000066400000000000000000000056261451777547300252500ustar00rootroot00000000000000from __future__ import annotations import os from typing import TYPE_CHECKING import pytest from cleo._compat import WINDOWS from cleo.application import Application from cleo.testers.command_tester import CommandTester from tests.commands.completion.fixtures.command_with_colons import CommandWithColons from tests.commands.completion.fixtures.command_with_space_in_name import SpacedCommand from tests.commands.completion.fixtures.hello_command import HelloCommand if TYPE_CHECKING: from pytest_mock import MockerFixture app = Application() app.add(HelloCommand()) app.add(CommandWithColons()) app.add(SpacedCommand()) def test_invalid_shell() -> None: command = app.find("completions") tester = CommandTester(command) with pytest.raises(ValueError): tester.execute("pomodoro") @pytest.mark.skipif(WINDOWS, reason="Only test linux shells") def test_bash(mocker: MockerFixture) -> None: mocker.patch( "cleo.io.inputs.string_input.StringInput.script_name", new_callable=mocker.PropertyMock, return_value="/path/to/my/script", ) mocker.patch( "cleo.commands.completions_command.CompletionsCommand._generate_function_name", return_value="_my_function", ) command = app.find("completions") tester = CommandTester(command) tester.execute("bash") with open(os.path.join(os.path.dirname(__file__), "fixtures", "bash.txt")) as f: expected = f.read() assert expected == tester.io.fetch_output().replace("\r\n", "\n") @pytest.mark.skipif(WINDOWS, reason="Only test linux shells") def test_zsh(mocker: MockerFixture) -> None: mocker.patch( "cleo.io.inputs.string_input.StringInput.script_name", new_callable=mocker.PropertyMock, return_value="/path/to/my/script", ) mocker.patch( "cleo.commands.completions_command.CompletionsCommand._generate_function_name", return_value="_my_function", ) command = app.find("completions") tester = CommandTester(command) tester.execute("zsh") with open(os.path.join(os.path.dirname(__file__), "fixtures", "zsh.txt")) as f: expected = f.read() assert expected == tester.io.fetch_output().replace("\r\n", "\n") @pytest.mark.skipif(WINDOWS, reason="Only test linux shells") def test_fish(mocker: MockerFixture) -> None: mocker.patch( "cleo.io.inputs.string_input.StringInput.script_name", new_callable=mocker.PropertyMock, return_value="/path/to/my/script", ) mocker.patch( "cleo.commands.completions_command.CompletionsCommand._generate_function_name", return_value="_my_function", ) command = app.find("completions") tester = CommandTester(command) tester.execute("fish") with open(os.path.join(os.path.dirname(__file__), "fixtures", "fish.txt")) as f: expected = f.read() assert expected == tester.io.fetch_output().replace("\r\n", "\n") cleo-2.1.0/tests/commands/test_command.py000066400000000000000000000042531451777547300204560ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.application import Application from cleo.commands.command import Command from cleo.helpers import argument from cleo.testers.command_tester import CommandTester from tests.fixtures.inherited_command import ChildCommand from tests.fixtures.signature_command import SignatureCommand if TYPE_CHECKING: from cleo.io.inputs.argument import Argument class MyCommand(Command): name = "test" arguments: ClassVar[list[Argument]] = [ argument("action", description="The action to execute.") ] def handle(self) -> int: action = self.argument("action") getattr(self, "_" + action)() return 0 def _overwrite(self) -> None: self.write("Processing...") self.overwrite("Done!") class MySecondCommand(Command): name = "test2" description = "Command testing" arguments: ClassVar[list[Argument]] = [argument("foo", "Bar", multiple=True)] def handle(self) -> int: foos = self.argument("foo") self.line(",".join(foos)) return 0 def test_set_application() -> None: application = Application() command = Command() command.set_application(application) assert command.application == application def test_with_signature() -> None: command = SignatureCommand() assert command.name == "signature:command" assert command.description == "description" assert command.help == "help" assert len(command.definition.arguments) == 2 assert len(command.definition.options) == 2 def test_signature_inheritance() -> None: command = ChildCommand() assert command.name == "parent" assert command.description == "Parent Command." def test_overwrite() -> None: command = MyCommand() tester = CommandTester(command) tester.execute("overwrite", decorated=True) expected = "Processing...\x1b[1G\x1b[2KDone!" assert tester.io.fetch_output() == expected def test_explicit_multiple_argument() -> None: command = MySecondCommand() tester = CommandTester(command) tester.execute("1 2 3") assert tester.io.fetch_output() == "1,2,3\n" cleo-2.1.0/tests/conftest.py000066400000000000000000000022471451777547300160260ustar00rootroot00000000000000from __future__ import annotations import os import sys from io import StringIO from typing import TYPE_CHECKING import pytest from cleo.io.buffered_io import BufferedIO from cleo.io.inputs.string_input import StringInput if TYPE_CHECKING: from typing import Callable from typing import Iterator from pytest_mock import MockerFixture @pytest.fixture() def io() -> BufferedIO: input_ = StringInput("") input_.set_stream(StringIO()) return BufferedIO(input_) @pytest.fixture() def ansi_io() -> BufferedIO: input_ = StringInput("") input_.set_stream(StringIO()) return BufferedIO(input_, decorated=True) @pytest.fixture() def environ() -> Iterator[None]: current_environ = dict(os.environ) yield os.environ.clear() os.environ.update(current_environ) @pytest.fixture() def argv() -> Iterator[None]: current_argv = sys.argv yield sys.argv = current_argv @pytest.fixture() def sleep(mocker: MockerFixture) -> Iterator[Callable[[float], None]]: now = 0.0 mocker.patch("time.time", side_effect=lambda: now) def _sleep(secs: float) -> None: nonlocal now now += secs yield _sleep cleo-2.1.0/tests/events/000077500000000000000000000000001451777547300151265ustar00rootroot00000000000000cleo-2.1.0/tests/events/__init__.py000066400000000000000000000000001451777547300172250ustar00rootroot00000000000000cleo-2.1.0/tests/events/test_event.py000066400000000000000000000005061451777547300176610ustar00rootroot00000000000000from __future__ import annotations from cleo.events.event import Event def test_is_propagation_not_stopped() -> None: e = Event() assert not e.is_propagation_stopped() def test_stop_propagation_and_is_propagation_stopped() -> None: e = Event() e.stop_propagation() assert e.is_propagation_stopped() cleo-2.1.0/tests/events/test_event_dispatcher.py000066400000000000000000000077141451777547300220770ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest from cleo.events.event import Event from cleo.events.event_dispatcher import EventDispatcher if TYPE_CHECKING: from typing import Any @pytest.fixture() def dispatcher() -> EventDispatcher: return EventDispatcher() @pytest.fixture() def listener() -> EventListener: return EventListener() PRE_FOO = "pre.foo" POST_FOO = "post.foo" PRE_BAR = "pre.bar" POST_BAR = "post.bar" class EventListener: def __init__(self) -> None: self.pre_foo_invoked = False self.post_foo_invoked = False def pre_foo(self, *_: Any) -> None: self.pre_foo_invoked = True def post_foo(self, e: Event, *_: Any) -> None: self.post_foo_invoked = True e.stop_propagation() def test_initial_state(dispatcher: EventDispatcher) -> None: assert {} == dispatcher.get_listeners() assert not dispatcher.has_listeners(PRE_FOO) assert not dispatcher.has_listeners(POST_FOO) def test_add_listener(dispatcher: EventDispatcher, listener: EventListener) -> None: 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 len(dispatcher.get_listeners(PRE_FOO)) == 1 assert len(dispatcher.get_listeners(POST_FOO)) == 1 assert len(dispatcher.get_listeners()) == 2 def test_get_listeners_sorts_by_priority(dispatcher: EventDispatcher) -> None: 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: EventDispatcher) -> None: 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 dispatcher.get_listeners() == expected def test_get_listener_priority(dispatcher: EventDispatcher) -> None: listener1 = EventListener() listener2 = EventListener() dispatcher.add_listener(PRE_FOO, listener1.pre_foo, -10) dispatcher.add_listener(PRE_FOO, listener2.pre_foo) assert dispatcher.get_listener_priority(PRE_FOO, listener1.pre_foo) == -10 assert dispatcher.get_listener_priority(PRE_FOO, listener2.pre_foo) == 0 assert dispatcher.get_listener_priority(PRE_BAR, listener2.pre_foo) is None def test_dispatch(dispatcher: EventDispatcher, listener: EventListener) -> None: dispatcher.add_listener(PRE_FOO, listener.pre_foo) dispatcher.add_listener(POST_FOO, listener.post_foo) dispatcher.dispatch(Event(), PRE_FOO) assert listener.pre_foo_invoked assert not listener.post_foo_invoked def test_stop_event_propagation( dispatcher: EventDispatcher, listener: EventListener ) -> None: other_listener = EventListener() dispatcher.add_listener(POST_FOO, listener.post_foo, 10) dispatcher.add_listener(POST_FOO, other_listener.post_foo) dispatcher.dispatch(Event(), POST_FOO) assert listener.post_foo_invoked assert not other_listener.post_foo_invoked cleo-2.1.0/tests/fixtures/000077500000000000000000000000001451777547300154735ustar00rootroot00000000000000cleo-2.1.0/tests/fixtures/__init__.py000066400000000000000000000000001451777547300175720ustar00rootroot00000000000000cleo-2.1.0/tests/fixtures/application_exception1.txt000066400000000000000000000000431451777547300226730ustar00rootroot00000000000000 The command "foo" does not exist. cleo-2.1.0/tests/fixtures/application_help.txt000066400000000000000000000000161451777547300215440ustar00rootroot00000000000000Consolecleo-2.1.0/tests/fixtures/application_run1.txt000066400000000000000000000012021451777547300214770ustar00rootroot00000000000000Console Usage: command [options] [arguments] Options: -h, --help Display help for the given command. When no command is given display help for the list command. -q, --quiet Do not output any message. -V, --version Display this application version. --ansi Force ANSI output. --no-ansi Disable ANSI output. -n, --no-interaction Do not ask any interactive question. -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug. Available commands: help Displays help for a command. list Lists commands. cleo-2.1.0/tests/fixtures/application_run2.txt000066400000000000000000000014421451777547300215060ustar00rootroot00000000000000 Description: Lists commands. Usage: list [options] [--] [] Arguments: namespace The namespace name Options: -h, --help Display help for the given command. When no command is given display help for the list command. -q, --quiet Do not output any message. -V, --version Display this application version. --ansi Force ANSI output. --no-ansi Disable ANSI output. -n, --no-interaction Do not ask any interactive question. -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug. Help: The list command lists all commands: console list You can also display the commands for a specific namespace: console list test cleo-2.1.0/tests/fixtures/application_run3.txt000066400000000000000000000014421451777547300215070ustar00rootroot00000000000000 Description: Lists commands. Usage: list [options] [--] [] Arguments: namespace The namespace name Options: -h, --help Display help for the given command. When no command is given display help for the list command. -q, --quiet Do not output any message. -V, --version Display this application version. --ansi Force ANSI output. --no-ansi Disable ANSI output. -n, --no-interaction Do not ask any interactive question. -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug. Help: The list command lists all commands: console list You can also display the commands for a specific namespace: console list test cleo-2.1.0/tests/fixtures/application_run4.txt000066400000000000000000000000101451777547300214760ustar00rootroot00000000000000Console cleo-2.1.0/tests/fixtures/application_run5.txt000066400000000000000000000015111451777547300215060ustar00rootroot00000000000000 Description: Displays help for a command. Usage: help [options] [--] [] Arguments: command_name The command name [default: "help"] Options: -h, --help Display help for the given command. When no command is given display help for the list command. -q, --quiet Do not output any message. -V, --version Display this application version. --ansi Force ANSI output. --no-ansi Disable ANSI output. -n, --no-interaction Do not ask any interactive question. -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug. Help: The help command displays help for a given command: console help list To display the list of available commands, please use the list command. cleo-2.1.0/tests/fixtures/exceptions/000077500000000000000000000000001451777547300176545ustar00rootroot00000000000000cleo-2.1.0/tests/fixtures/exceptions/__init__.py000066400000000000000000000000001451777547300217530ustar00rootroot00000000000000cleo-2.1.0/tests/fixtures/exceptions/nested1.py000066400000000000000000000001321451777547300215650ustar00rootroot00000000000000def outer() -> None: def inner() -> None: raise Exception("Foo") inner() cleo-2.1.0/tests/fixtures/exceptions/nested2.py000066400000000000000000000001741451777547300215740ustar00rootroot00000000000000from tests.fixtures.exceptions.nested1 import outer def call() -> None: def run() -> None: outer() run() cleo-2.1.0/tests/fixtures/exceptions/recursion.py000066400000000000000000000000651451777547300222400ustar00rootroot00000000000000def recursion_error() -> None: recursion_error() cleo-2.1.0/tests/fixtures/exceptions/simple.py000066400000000000000000000000761451777547300215220ustar00rootroot00000000000000def simple_exception() -> None: raise Exception("Failed") cleo-2.1.0/tests/fixtures/exceptions/solution.py000066400000000000000000000010241451777547300220770ustar00rootroot00000000000000from crashtest.contracts.base_solution import BaseSolution from crashtest.contracts.provides_solution import ProvidesSolution class CustomError(ProvidesSolution, Exception): @property def solution(self) -> BaseSolution: solution = BaseSolution("Solution Title.", "Solution Description") solution.documentation_links.append("https://example.com") solution.documentation_links.append("https://example2.com") return solution def call() -> None: raise CustomError("Error with solution") cleo-2.1.0/tests/fixtures/foo1_command.py000066400000000000000000000004511451777547300204070ustar00rootroot00000000000000from __future__ import annotations from typing import ClassVar from cleo.commands.command import Command class Foo1Command(Command): name = "foo bar1" description = "The foo bar1 command" aliases: ClassVar[list[str]] = ["afoobar1"] def handle(self) -> int: return 0 cleo-2.1.0/tests/fixtures/foo2_command.py000066400000000000000000000004511451777547300204100ustar00rootroot00000000000000from __future__ import annotations from typing import ClassVar from cleo.commands.command import Command class Foo2Command(Command): name = "foo1 bar" description = "The foo1 bar command" aliases: ClassVar[list[str]] = ["afoobar2"] def handle(self) -> int: return 0 cleo-2.1.0/tests/fixtures/foo3_command.py000066400000000000000000000005731451777547300204160ustar00rootroot00000000000000from __future__ import annotations from typing import ClassVar from cleo.commands.command import Command class Foo3Command(Command): name = "foo3" description = "The foo3 bar command" aliases: ClassVar[list[str]] = ["foo3"] def handle(self) -> int: question = self.ask("echo:", default="default input") self.line(question) return 0 cleo-2.1.0/tests/fixtures/foo_command.py000066400000000000000000000010251451777547300203240ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.commands.command import Command if TYPE_CHECKING: from cleo.io.io import IO class FooCommand(Command): name = "foo bar" description = "The foo bar command" aliases: ClassVar[list[str]] = ["afoobar"] def interact(self, io: IO) -> None: io.write_line("interact called") def handle(self) -> int: assert self._io is not None self._io.write_line("called") return 0 cleo-2.1.0/tests/fixtures/foo_sub_namespaced1_command.py000066400000000000000000000004751451777547300234460ustar00rootroot00000000000000from __future__ import annotations from typing import ClassVar from cleo.commands.command import Command class FooSubNamespaced1Command(Command): name = "foo bar baz" description = "The foo bar baz command" aliases: ClassVar[list[str]] = ["foobarbaz"] def handle(self) -> int: return 0 cleo-2.1.0/tests/fixtures/foo_sub_namespaced2_command.py000066400000000000000000000004751451777547300234470ustar00rootroot00000000000000from __future__ import annotations from typing import ClassVar from cleo.commands.command import Command class FooSubNamespaced2Command(Command): name = "foo baz bam" description = "The foo baz bam command" aliases: ClassVar[list[str]] = ["foobazbam"] def handle(self) -> int: return 0 cleo-2.1.0/tests/fixtures/foo_sub_namespaced3_command.py000066400000000000000000000006071451777547300234450ustar00rootroot00000000000000from __future__ import annotations from typing import ClassVar from cleo.commands.command import Command class FooSubNamespaced3Command(Command): name = "foo bar" description = "The foo bar command" aliases: ClassVar[list[str]] = ["foobar"] def handle(self) -> int: question = self.ask("", default="default input") self.line(question) return 0 cleo-2.1.0/tests/fixtures/inherited_command.py000066400000000000000000000003241451777547300215150ustar00rootroot00000000000000from __future__ import annotations from cleo.commands.command import Command class ParentCommand(Command): name = "parent" description = "Parent Command." class ChildCommand(ParentCommand): pass cleo-2.1.0/tests/fixtures/signature_command.py000066400000000000000000000014551451777547300215510ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar from cleo.commands.command import Command from cleo.helpers import argument from cleo.helpers import option if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class SignatureCommand(Command): name = "signature:command" options: ClassVar[list[Option]] = [ option("baz", "z", description="Baz"), option("bazz", "Z", description="Bazz"), ] arguments: ClassVar[list[Argument]] = [ argument("foo", description="Foo"), argument("bar", description="Bar", optional=True), ] help = "help" description = "description" def handle(self) -> int: self.line("handle called") return 0 cleo-2.1.0/tests/formatters/000077500000000000000000000000001451777547300160105ustar00rootroot00000000000000cleo-2.1.0/tests/formatters/__init__.py000066400000000000000000000000001451777547300201070ustar00rootroot00000000000000cleo-2.1.0/tests/formatters/test_formatter.py000066400000000000000000000064421451777547300214320ustar00rootroot00000000000000from __future__ import annotations import pytest from cleo.formatters.formatter import Formatter @pytest.mark.parametrize( ["text", "width", "expected"], [ ( "foobar baz", 2, "fo\no\x1b[31;1mb\x1b[39;22m\n\x1b[31;1mar\x1b[39;22m\nba\nz", ), ( "pre foo bar baz post", 2, ( "pr\ne \x1b[31;1m\x1b[39;22m\n\x1b[31;1mfo\x1b[39;22m\n\x1b[31;1mo " "\x1b[39;22m\n\x1b[31;1mba\x1b[39;22m\n\x1b[31;1mr " "\x1b[39;22m\n\x1b[31;1mba" "\x1b[39;22m\n\x1b[31;1mz\x1b[39;22m \npo\nst" ), ), ( "pre foo bar baz post", 3, ( "pre\x1b[31;1m\x1b[39;22m\n\x1b[31;1mfoo\x1b[39;22m\n\x1b" "[31;1mbar\x1b[39;22m\n\x1b[31;1mbaz\x1b[39;22m\npos\nt" ), ), ( "pre foo bar baz post", 4, ( "pre \x1b[31;1m\x1b[39;22m\n\x1b[31;1mfoo \x1b[39;22m\n\x1b" "[31;1mbar \x1b[39;22m\n\x1b[31;1mbaz\x1b[39;22m \npost" ), ), ( "pre foo bar baz post", 5, ( "pre \x1b[31;1mf\x1b[39;22m\n\x1b[31;1moo ba\x1b" "[39;22m\n\x1b[31;1mr baz\x1b[39;22m\npost" ), ), ( "Lorem ipsum dolor sit amet", 4, ( "Lore\nm \x1b[31;1mip\x1b[39;22m\n\x1b[31;1msum\x1b[39;22m " "\ndolo\nr \x1b[34msi\x1b[39m\n\x1b[34mt\x1b[39m am\net" ), ), ( "Lorem ipsum dolor sit amet", 8, ( "Lorem \x1b[31;1mip\x1b[39;22m\n\x1b[31;1msum\x1b" "[39;22m dolo\nr \x1b[34msit\x1b[39m am\net" ), ), ( ( "Lorem ipsum dolor sit, " "amet et laudantium architecto" ), 18, ( "Lorem \x1b[31;1mipsum\x1b[39;22m dolor \x1b[34m\x1b[39m\n\x1b" "[34msit\x1b[39m, \x1b[31;1mamet\x1b[39;22m et \x1b[34mlauda\x1b" "[39m\n\x1b[34mntium\x1b[39m architecto" ), ), ], ) def test_format_and_wrap(text: str, width: int, expected: str) -> None: formatter = Formatter(True) assert formatter.format_and_wrap(text, width) == expected @pytest.mark.parametrize( ["text", "width", "expected"], [ ("foobar baz", 2, "fo\nob\nar\nba\nz"), ( "pre foo bar baz post", 2, "pr\ne \nfo\no \nba\nr \nba\nz \npo\nst", ), ("pre foo bar baz post", 3, "pre\nfoo\nbar\nbaz\npos\nt"), ("pre foo bar baz post", 4, "pre \nfoo \nbar \nbaz \npost"), ("pre foo bar baz post", 5, "pre f\noo ba\nr baz\npost"), ], ) def test_format_and_wrap_undecorated(text: str, width: int, expected: str) -> None: formatter = Formatter(False) assert formatter.format_and_wrap(text, width) == expected cleo-2.1.0/tests/io/000077500000000000000000000000001451777547300142315ustar00rootroot00000000000000cleo-2.1.0/tests/io/__init__.py000066400000000000000000000000001451777547300163300ustar00rootroot00000000000000cleo-2.1.0/tests/io/inputs/000077500000000000000000000000001451777547300155535ustar00rootroot00000000000000cleo-2.1.0/tests/io/inputs/__init__.py000066400000000000000000000000001451777547300176520ustar00rootroot00000000000000cleo-2.1.0/tests/io/inputs/test_argument.py000066400000000000000000000033531451777547300210120ustar00rootroot00000000000000from __future__ import annotations import pytest from cleo.exceptions import CleoLogicError from cleo.io.inputs.argument import Argument def test_optional_non_list_argument() -> None: argument = Argument( "foo", required=False, is_list=False, description="Foo description", default="bar", ) assert argument.name == "foo" assert not argument.is_required() assert not argument.is_list() assert argument.description == "Foo description" assert argument.default == "bar" def test_required_non_list_argument() -> None: argument = Argument("foo", is_list=False, description="Foo description") assert argument.name == "foo" assert argument.is_required() assert not argument.is_list() assert argument.description == "Foo description" assert argument.default is None def test_list_argument() -> None: argument = Argument("foo", is_list=True, description="Foo description") assert argument.name == "foo" assert argument.is_required() assert argument.is_list() assert argument.description == "Foo description" assert argument.default == [] def test_required_arguments_do_not_support_default_values() -> None: with pytest.raises( CleoLogicError, match="Cannot set a default value for required arguments" ): Argument("foo", description="Foo description", default="bar") def test_list_arguments_do_not_support_non_list_default_values() -> None: with pytest.raises( CleoLogicError, match="A default value for a list argument must be a list" ): Argument( "foo", required=False, is_list=True, description="Foo description", default="bar", ) cleo-2.1.0/tests/io/inputs/test_argv_input.py000066400000000000000000000077771451777547300213640ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING import pytest from cleo.io.inputs.argument import Argument from cleo.io.inputs.argv_input import ArgvInput from cleo.io.inputs.definition import Definition from cleo.io.inputs.option import Option if TYPE_CHECKING: from typing import Iterator @pytest.fixture() def argv() -> Iterator[None]: original = sys.argv[:] yield sys.argv = original def test_it_uses_argv_by_default(argv: Iterator[None]) -> None: sys.argv = ["cli.py", "foo"] i = ArgvInput() assert ["foo"] == i._tokens def test_parse_arguments() -> None: i = ArgvInput(["cli.py", "foo"]) i.bind(Definition([Argument("name")])) assert i.arguments == {"name": "foo"} @pytest.mark.parametrize( ["args", "options", "expected_options"], [ (["cli.py", "--foo"], [Option("--foo")], {"foo": True}), ( ["cli.py", "--foo=bar"], [Option("--foo", "-f", flag=False, requires_value=True)], {"foo": "bar"}, ), ( ["cli.py", "--foo", "bar"], [Option("--foo", "-f", flag=False, requires_value=True)], {"foo": "bar"}, ), ( ["cli.py", "--foo="], [Option("--foo", "-f", flag=False, requires_value=False)], {"foo": ""}, ), ( ["cli.py", "--foo=", "bar"], [Option("--foo", "-f", flag=False, requires_value=False), Argument("name")], {"foo": ""}, ), ( ["cli.py", "bar", "--foo="], [Option("--foo", "-f", flag=False, requires_value=False), Argument("name")], {"foo": ""}, ), ( ["cli.py", "--foo"], [Option("--foo", "-f", flag=False, requires_value=False)], {"foo": None}, ), ( ["cli.py", "-f"], [Option("--foo", "-f")], {"foo": True}, ), ( ["cli.py", "-fbar"], [Option("--foo", "-f", flag=False, requires_value=True)], {"foo": "bar"}, ), ( ["cli.py", "-f", "bar"], [Option("--foo", "-f", flag=False, requires_value=True)], {"foo": "bar"}, ), ( ["cli.py", "-f", ""], [Option("--foo", "-f", flag=False, requires_value=False)], {"foo": ""}, ), ( ["cli.py", "-f", "", "foo"], [Option("--foo", "-f", flag=False, requires_value=False), Argument("name")], {"foo": ""}, ), ( ["cli.py", "-f", "", "-b"], [ Option("--foo", "-f", flag=False, requires_value=False), Option("--bar", "-b"), ], {"foo": "", "bar": True}, ), ( ["cli.py", "-f", "-b", "foo"], [ Option("--foo", "-f", flag=False, requires_value=False), Option("--bar", "-b"), Argument("name"), ], {"foo": None, "bar": True}, ), ( ["cli.py", "-fb"], [ Option("--foo", "-f"), Option("--bar", "-b"), ], {"foo": True, "bar": True}, ), ( ["cli.py", "-fb", "bar"], [ Option("--foo", "-f"), Option("--bar", "-b", flag=False, requires_value=True), ], {"foo": True, "bar": "bar"}, ), ( ["cli.py", "-fbbar"], [ Option("--foo", "-f", flag=False, requires_value=False), Option("--bar", "-b", flag=False, requires_value=False), ], {"foo": "bbar", "bar": None}, ), ], ) def test_parse_options( args: list[str], options: list[Option], expected_options: dict[str, str | bool | None], ) -> None: i = ArgvInput(args) i.bind(Definition(options)) assert i.options == expected_options cleo-2.1.0/tests/io/inputs/test_option.py000066400000000000000000000057061451777547300205040ustar00rootroot00000000000000from __future__ import annotations import pytest from cleo.exceptions import CleoLogicError from cleo.exceptions import CleoValueError from cleo.io.inputs.option import Option def test_create() -> None: opt = Option("option") assert opt.name == "option" assert opt.shortcut is None assert opt.is_flag() assert not opt.accepts_value() assert not opt.requires_value() assert not opt.is_list() assert not opt.default def test_dashed_name() -> None: opt = Option("--option") assert opt.name == "option" def test_fail_if_name_is_empty() -> None: with pytest.raises(CleoValueError): Option("") def test_fail_if_default_value_provided_for_flag() -> None: with pytest.raises(CleoLogicError): Option("option", flag=True, default="default") def test_fail_if_wrong_default_value_for_list_option() -> None: with pytest.raises(CleoLogicError): Option("option", flag=False, is_list=True, default="default") def test_shortcut() -> None: opt = Option("option", "o") assert opt.shortcut == "o" def test_dashed_shortcut() -> None: opt = Option("option", "-o") assert opt.shortcut == "o" def test_multiple_shortcuts() -> None: opt = Option("option", "-o|oo|-ooo") assert opt.shortcut == "o|oo|ooo" def test_fail_if_shortcut_is_empty() -> None: with pytest.raises(CleoValueError): Option("option", "") def test_optional_value() -> None: opt = Option("option", flag=False, requires_value=False) assert not opt.is_flag() assert opt.accepts_value() assert not opt.requires_value() assert not opt.is_list() assert opt.default is None def test_optional_value_with_default() -> None: opt = Option("option", flag=False, requires_value=False, default="Default") assert not opt.is_flag() assert opt.accepts_value() assert not opt.requires_value() assert not opt.is_list() assert opt.default == "Default" def test_required_value() -> None: opt = Option("option", flag=False, requires_value=True) assert not opt.is_flag() assert opt.accepts_value() assert opt.requires_value() assert not opt.is_list() assert opt.default is None def test_required_value_with_default() -> None: opt = Option("option", flag=False, requires_value=True, default="Default") assert not opt.is_flag() assert opt.accepts_value() assert opt.requires_value() assert not opt.is_list() assert opt.default == "Default" def test_list() -> None: opt = Option("option", flag=False, is_list=True) assert not opt.is_flag() assert opt.accepts_value() assert opt.requires_value() assert opt.is_list() assert [] == opt.default def test_multi_valued_with_default() -> None: opt = Option("option", flag=False, is_list=True, default=["foo", "bar"]) assert not opt.is_flag() assert opt.accepts_value() assert opt.requires_value() assert opt.is_list() assert ["foo", "bar"] == opt.default cleo-2.1.0/tests/io/inputs/test_token_parser.py000066400000000000000000000033261451777547300216640ustar00rootroot00000000000000from __future__ import annotations import pytest from cleo.io.inputs.token_parser import TokenParser @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: str, tokens: list[str]) -> None: assert TokenParser().parse(string) == tokens cleo-2.1.0/tests/io/outputs/000077500000000000000000000000001451777547300157545ustar00rootroot00000000000000cleo-2.1.0/tests/io/outputs/__init__.py000066400000000000000000000000001451777547300200530ustar00rootroot00000000000000cleo-2.1.0/tests/io/outputs/test_section_output.py000066400000000000000000000051501451777547300224520ustar00rootroot00000000000000from __future__ import annotations from io import StringIO import pytest from cleo.io.outputs.section_output import SectionOutput @pytest.fixture() def stream() -> StringIO: return StringIO() @pytest.fixture() def sections() -> list[SectionOutput]: return [] @pytest.fixture() def output(stream: StringIO, sections: list[SectionOutput]) -> SectionOutput: return SectionOutput(stream, sections, decorated=True) @pytest.fixture() def output2(stream: StringIO, sections: list[SectionOutput]) -> SectionOutput: return SectionOutput(stream, sections, decorated=True) def test_clear_all(output: SectionOutput, stream: StringIO) -> None: output.write_line("Foo\nBar") output.clear() stream.seek(0) assert stream.read() == "Foo\nBar\n\x1b[2A\x1b[0J" def test_clear_with_number_of_lines(output: SectionOutput, stream: StringIO) -> None: output.write_line("Foo\nBar\nBaz\nFooBar") output.clear(2) stream.seek(0) assert stream.read() == "Foo\nBar\nBaz\nFooBar\n\x1b[2A\x1b[0J" def test_clear_with_number_of_lines_and_multiple_sections( output: SectionOutput, output2: SectionOutput, stream: StringIO ) -> None: output2.write_line("Foo") output2.write_line("Bar") output2.clear(1) output.write_line("Baz") stream.seek(0) assert stream.read() == "Foo\nBar\n\x1b[1A\x1b[0J\x1b[1A\x1b[0JBaz\nFoo\n" def test_clear_preserves_empty_lines( output: SectionOutput, output2: SectionOutput, stream: StringIO ) -> None: output2.write_line("\nFoo") output2.clear(1) output.write_line("Bar") stream.seek(0) assert stream.read() == "\nFoo\n\x1b[1A\x1b[0J\x1b[1A\x1b[0JBar\n\n" def test_overwrite(output: SectionOutput, stream: StringIO) -> None: output.write_line("Foo") output.overwrite("Bar") stream.seek(0) assert stream.read() == "Foo\n\x1b[1A\x1b[0JBar\n" def test_overwrite_multiple_lines(output: SectionOutput, stream: StringIO) -> None: output.write_line("Foo\nBar\nBaz") output.overwrite("Bar") stream.seek(0) assert stream.read() == "Foo\nBar\nBaz\n\x1b[3A\x1b[0JBar\n" def test_add_multiple_sections( output: SectionOutput, output2: SectionOutput, sections: list[SectionOutput] ) -> None: assert len(sections) == 2 def test_multiple_sections_output( output: SectionOutput, output2: SectionOutput, stream: StringIO ) -> None: output.write_line("Foo") output2.write_line("Bar") output.overwrite("Baz") output2.overwrite("Foobar") stream.seek(0) assert ( stream.read() == "Foo\nBar\n\x1b[2A\x1b[0JBar\n\x1b[1A\x1b[0JBaz\nBar\n\x1b[1A\x1b[0JFoobar\n" ) cleo-2.1.0/tests/loaders/000077500000000000000000000000001451777547300152535ustar00rootroot00000000000000cleo-2.1.0/tests/loaders/__init__.py000066400000000000000000000000001451777547300173520ustar00rootroot00000000000000cleo-2.1.0/tests/loaders/test_factory_command_loader.py000066400000000000000000000023031451777547300233550ustar00rootroot00000000000000from __future__ import annotations import pytest from cleo.commands.command import Command from cleo.exceptions import CleoCommandNotFoundError from cleo.loaders.factory_command_loader import FactoryCommandLoader def command(name: str) -> Command: command_ = Command() command_.name = name return command_ def test_has() -> None: loader = FactoryCommandLoader( {"foo": lambda: command("foo"), "bar": lambda: command("bar")} ) assert loader.has("foo") assert loader.has("bar") assert not loader.has("baz") def test_get() -> None: loader = FactoryCommandLoader( {"foo": lambda: command("foo"), "bar": lambda: command("bar")} ) assert isinstance(loader.get("foo"), Command) assert isinstance(loader.get("bar"), Command) def test_get_invalid_command_raises_error() -> None: loader = FactoryCommandLoader( {"foo": lambda: command("foo"), "bar": lambda: command("bar")} ) with pytest.raises(CleoCommandNotFoundError): loader.get("baz") def test_names() -> None: loader = FactoryCommandLoader( {"foo": lambda: command("foo"), "bar": lambda: command("bar")} ) assert loader.names == ["foo", "bar"] cleo-2.1.0/tests/test_application.py000066400000000000000000000227571451777547300175530ustar00rootroot00000000000000from __future__ import annotations import os import sys from pathlib import Path import pytest from cleo.application import Application from cleo.commands.command import Command from cleo.exceptions import CleoCommandNotFoundError from cleo.exceptions import CleoNamespaceNotFoundError from cleo.io.io import IO from cleo.io.outputs.stream_output import StreamOutput from cleo.testers.application_tester import ApplicationTester from tests.fixtures.foo1_command import Foo1Command from tests.fixtures.foo2_command import Foo2Command from tests.fixtures.foo3_command import Foo3Command from tests.fixtures.foo_command import FooCommand from tests.fixtures.foo_sub_namespaced1_command import FooSubNamespaced1Command from tests.fixtures.foo_sub_namespaced2_command import FooSubNamespaced2Command from tests.fixtures.foo_sub_namespaced3_command import FooSubNamespaced3Command FIXTURES_PATH = Path(__file__).parent.joinpath("fixtures") @pytest.fixture() def app() -> Application: return Application() @pytest.fixture() def tester(app: Application) -> ApplicationTester: app.catch_exceptions(False) return ApplicationTester(app) def test_name_version_getters() -> None: app = Application("foo", "bar") assert app.name == "foo" assert app.display_name == "Foo" assert app.version == "bar" def test_name_version_setter() -> None: app = Application("foo", "bar") app.set_name("bar") app.set_version("foo") assert app.name == "bar" assert app.display_name == "Bar" assert app.version == "foo" app.set_display_name("Baz") assert app.display_name == "Baz" def test_long_version() -> None: app = Application("foo", "bar") assert app.long_version == "Foo (version bar)" def test_help(app: Application) -> None: assert app.help == FIXTURES_PATH.joinpath("application_help.txt").read_text() def test_all(app: Application) -> None: commands = app.all() assert isinstance(commands["help"], Command) app.add(FooCommand()) assert len(app.all("foo")) == 1 def test_add(app: Application) -> None: foo = FooCommand() app.add(foo) commands = app.all() assert [commands["foo bar"]] == [foo] foo1 = Foo1Command() app.add(foo1) commands = app.all() assert [commands["foo bar"], commands["foo bar1"]] == [foo, foo1] def test_has_get(app: Application) -> None: assert app.has("list") assert not app.has("afoobar") foo = FooCommand() app.add(foo) assert app.has("foo bar") assert app.has("afoobar") assert app.get("foo bar") == foo assert app.get("afoobar") == foo def test_silent_help(app: Application) -> None: app.catch_exceptions(False) tester = ApplicationTester(app) tester.execute("-h -q", decorated=False) assert tester.io.fetch_output() == "" def test_get_namespaces(app: Application) -> None: app.add(FooCommand()) app.add(Foo1Command()) assert app.get_namespaces() == ["foo"] def test_find_namespace(app: Application) -> None: app.add(FooCommand()) assert app.find_namespace("foo") == "foo" def test_find_namespace_with_sub_namespaces(app: Application) -> None: app.add(FooSubNamespaced1Command()) app.add(FooSubNamespaced2Command()) assert app.find_namespace("foo") == "foo" def test_find_ambiguous_namespace(app: Application) -> None: app.add(FooCommand()) app.add(Foo2Command()) with pytest.raises( CleoNamespaceNotFoundError, match=( r'There are no commands in the "f" namespace\.\n\n' r"Did you mean one of these\?\n foo\n foo1" ), ): app.find_namespace("f") def test_find_invalid_namespace(app: Application) -> None: app.add(FooCommand()) app.add(Foo2Command()) with pytest.raises( CleoNamespaceNotFoundError, match=r'There are no commands in the "bar" namespace\.', ): app.find_namespace("bar") def test_find_unique_name_but_namespace_name(app: Application) -> None: app.add(FooCommand()) app.add(Foo1Command()) app.add(Foo2Command()) with pytest.raises( CleoCommandNotFoundError, match=r'The command "foo1" does not exist\.', ): app.find("foo1") def test_find(app: Application) -> None: app.add(FooCommand()) assert isinstance(app.find("foo bar"), FooCommand) assert isinstance(app.find("afoobar"), FooCommand) def test_find_ambiguous_command(app: Application) -> None: app.add(FooCommand()) with pytest.raises( CleoCommandNotFoundError, match=( r'The command "foo b" does not exist\.\n\nDid you mean this\?\n foo bar' ), ): app.find("foo b") def test_find_ambiguous_command_hidden(app: Application) -> None: foo = FooCommand() foo.hidden = True app.add(foo) with pytest.raises( CleoCommandNotFoundError, match=r'The command "foo b" does not exist\.$', ): app.find("foo b") def test_set_catch_exceptions(app: Application, environ: dict[str, str]) -> None: app.auto_exits(False) os.environ["COLUMNS"] = "120" tester = ApplicationTester(app) app.catch_exceptions(True) assert app.are_exceptions_caught() tester.execute("foo", decorated=False) assert tester.io.fetch_output() == "" assert ( tester.io.fetch_error() == FIXTURES_PATH.joinpath("application_exception1.txt").read_text() ) app.catch_exceptions(False) with pytest.raises(CleoCommandNotFoundError): tester.execute("foo", decorated=False) def test_auto_exit(app: Application) -> None: app.auto_exits(False) assert not app.is_auto_exit_enabled() app.auto_exits() assert app.is_auto_exit_enabled() def test_run(app: Application, argv: list[str]) -> None: app.catch_exceptions(False) app.auto_exits(False) command = Foo1Command() app.add(command) sys.argv = ["console", "foo bar1"] app.run() assert isinstance(command.io, IO) assert isinstance(command.io.output, StreamOutput) assert isinstance(command.io.error_output, StreamOutput) assert command.io.output.stream == sys.stdout assert command.io.error_output.stream == sys.stderr def test_run_runs_the_list_command_without_arguments(tester: ApplicationTester) -> None: tester.execute("", decorated=False) assert ( tester.io.fetch_output() == FIXTURES_PATH.joinpath("application_run1.txt").read_text() ) def test_run_runs_help_command_if_required(tester: ApplicationTester) -> None: tester.execute("--help", decorated=False) assert ( tester.io.fetch_output() == FIXTURES_PATH.joinpath("application_run2.txt").read_text() ) tester.execute("-h", decorated=False) assert ( tester.io.fetch_output() == FIXTURES_PATH.joinpath("application_run2.txt").read_text() ) def test_run_runs_help_command_with_command(tester: ApplicationTester) -> None: tester.execute("--help list", decorated=False) assert ( tester.io.fetch_output() == FIXTURES_PATH.joinpath("application_run3.txt").read_text() ) tester.execute("list -h", decorated=False) assert ( tester.io.fetch_output() == FIXTURES_PATH.joinpath("application_run3.txt").read_text() ) def test_run_removes_all_output_if_quiet(tester: ApplicationTester) -> None: tester.execute("list --quiet") assert tester.io.fetch_output() == "" tester.execute("list -q") assert tester.io.fetch_output() == "" def test_run_with_verbosity(tester: ApplicationTester) -> None: tester.execute("list --verbose") assert tester.io.is_verbose() tester.execute("list -v") assert tester.io.is_verbose() tester.execute("list -vv") assert tester.io.is_very_verbose() tester.execute("list -vvv") assert tester.io.is_debug() def test_run_with_version(tester: ApplicationTester) -> None: tester.execute("--version") assert ( tester.io.fetch_output() == FIXTURES_PATH.joinpath("application_run4.txt").read_text() ) tester.execute("-V") assert ( tester.io.fetch_output() == FIXTURES_PATH.joinpath("application_run4.txt").read_text() ) def test_run_with_help(tester: ApplicationTester) -> None: tester.execute("help --help") assert ( tester.io.fetch_output() == FIXTURES_PATH.joinpath("application_run5.txt").read_text() ) tester.execute("-h help") assert ( tester.io.fetch_output() == FIXTURES_PATH.joinpath("application_run5.txt").read_text() ) def test_run_with_input() -> None: app = Application() command = Foo3Command() app.add(command) tester = ApplicationTester(app) status_code = tester.execute("foo3", inputs="Hello world!") assert status_code == 0 assert tester.io.fetch_output() == "Hello world!\n" def test_run_namespaced_with_input() -> None: app = Application() command = FooSubNamespaced3Command() app.add(command) tester = ApplicationTester(app) status_code = tester.execute("foo bar", inputs="Hello world!") assert status_code == 0 assert tester.io.fetch_output() == "Hello world!\n" @pytest.mark.parametrize("cmd", (Foo3Command(), FooSubNamespaced3Command())) def test_run_with_input_and_non_interactive(cmd: Command) -> None: app = Application() app.add(cmd) tester = ApplicationTester(app) status_code = tester.execute(f"--no-interaction {cmd.name}", inputs="Hello world!") assert status_code == 0 assert tester.io.fetch_output() == "default input\n" cleo-2.1.0/tests/test_color.py000066400000000000000000000031201451777547300163450ustar00rootroot00000000000000from __future__ import annotations import os import pytest from cleo.color import Color @pytest.mark.parametrize( ["foreground", "background", "options", "expected"], [ ("", "", [], " "), ("red", "yellow", [], "\033[31;43m \033[39;49m"), ("red", "yellow", ["underline"], "\033[31;43;4m \033[39;49;24m"), ], ) def test_ansi_colors( foreground: str, background: str, options: list[str], expected: str ) -> None: color = Color(foreground, background, options) assert color.apply(" ") == expected @pytest.mark.skipif( os.getenv("COLORTERM") != "truecolor", reason="True color not supported" ) @pytest.mark.parametrize( ["foreground", "background", "options", "expected"], [ ("#fff", "#000", [], "\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m"), ("#ffffff", "#000000", [], "\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m"), ], ) def test_true_color_support( foreground: str, background: str, options: list[str], expected: str ) -> None: color = Color(foreground, background, options) assert color.apply(" ") == expected @pytest.mark.parametrize( ["foreground", "background", "options", "expected"], [ ("#f00", "#ff0", [], "\033[31;43m \033[39;49m"), ("#c0392b", "#f1c40f", [], "\033[31;43m \033[39;49m"), ], ) def test_degrade_true_colors( foreground: str, background: str, options: list[str], expected: str, environ: dict[str, str], ) -> None: os.environ["COLORTERM"] = "" color = Color(foreground, background, options) assert color.apply(" ") == expected cleo-2.1.0/tests/test_helpers.py000066400000000000000000000034701451777547300167010ustar00rootroot00000000000000from __future__ import annotations from cleo.helpers import argument from cleo.helpers import option def test_argument() -> None: arg = argument("foo", "Foo") assert arg.description == "Foo" assert arg.is_required() assert not arg.is_list() assert arg.default is None arg = argument("foo", "Foo", optional=True, default="bar") assert not arg.is_required() assert not arg.is_list() assert arg.default == "bar" arg = argument("foo", "Foo", multiple=True) assert arg.is_required() assert arg.is_list() assert [] == arg.default arg = argument("foo", "Foo", optional=True, multiple=True, default=["bar"]) assert not arg.is_required() assert arg.is_list() assert ["bar"] == arg.default def test_option() -> None: opt = option("foo", "f", "Foo") assert opt.description == "Foo" assert not opt.accepts_value() assert not opt.requires_value() assert not opt.is_list() assert opt.default is False opt = option("foo", "f", "Foo", flag=False) assert opt.description == "Foo" assert opt.accepts_value() assert opt.requires_value() assert not opt.is_list() assert opt.default is None opt = option("foo", "f", "Foo", flag=False, value_required=False) assert opt.description == "Foo" assert opt.accepts_value() assert not opt.requires_value() assert not opt.is_list() opt = option("foo", "f", "Foo", flag=False, multiple=True) assert opt.description == "Foo" assert opt.accepts_value() assert opt.requires_value() assert opt.is_list() assert opt.default == [] opt = option("foo", "f", "Foo", flag=False, default="bar") assert opt.description == "Foo" assert opt.accepts_value() assert opt.requires_value() assert not opt.is_list() assert opt.default == "bar" cleo-2.1.0/tests/test_terminal.py000066400000000000000000000025161451777547300170520ustar00rootroot00000000000000from __future__ import annotations import os from typing import TYPE_CHECKING import pytest from cleo.terminal import Terminal if TYPE_CHECKING: from pytest_mock import MockerFixture def test_size() -> None: terminal = Terminal() w, h = terminal.size assert terminal.width == w terminal = Terminal(width=99, height=101) w, h = terminal.size assert w == 99 and h == 101 @pytest.mark.parametrize( "columns_env_value, init_value, expected", ( ("314", None, 314), ("200", 40, 40), ("random", 40, 40), ("random", None, 80), ), ) def test_columns_env( mocker: MockerFixture, columns_env_value: str, init_value: int | None, expected: int ) -> None: mocker.patch.dict(os.environ, {"COLUMNS": columns_env_value}, clear=False) console = Terminal(width=init_value) assert console.width == expected @pytest.mark.parametrize( "lines_env_value, init_value, expected", ( ("314", None, 314), ("200", 40, 40), ("random", 40, 40), ("random", None, 25), ), ) def test_lines_env( mocker: MockerFixture, lines_env_value: str, init_value: int | None, expected: int ) -> None: mocker.patch.dict(os.environ, {"LINES": lines_env_value}, clear=False) console = Terminal(height=init_value) assert console.height == expected cleo-2.1.0/tests/test_utils.py000066400000000000000000000021721451777547300163750ustar00rootroot00000000000000from __future__ import annotations import pytest from cleo._utils import find_similar_names from cleo._utils import format_time @pytest.mark.parametrize( ["input_secs", "expected"], [ (0.1, "< 1 sec"), (1.0, "1 sec"), (2.0, "2 secs"), (59.0, "59 secs"), (60.0, "1 min"), (120.0, "2 mins"), (3600.0, "1 hr"), (7200.0, "2 hrs"), (129600.0, "1 day"), (129601.0, "2 days"), (700000.0, "9 days"), ], ) def test_format_time(input_secs: float, expected: str) -> None: assert format_time(input_secs) == expected @pytest.mark.parametrize( ["name", "expected"], [ ("", ["help", "foo1", "foo2", "bar1", "bar2", "foo bar1", "foo bar2"]), ("hellp", ["help"]), ("bar2", ["bar2", "bar1", "foo bar2"]), ("bar1", ["bar1", "bar2", "foo bar1"]), ("foo", ["foo1", "foo2", "foo bar1", "foo bar2"]), ], ) def test_find_similar_names(name: str, expected: list[str]) -> None: names = ["help", "foo1", "foo2", "bar1", "bar2", "foo bar1", "foo bar2"] assert find_similar_names(name, names) == expected cleo-2.1.0/tests/testers/000077500000000000000000000000001451777547300153135ustar00rootroot00000000000000cleo-2.1.0/tests/testers/__init__.py000066400000000000000000000000001451777547300174120ustar00rootroot00000000000000cleo-2.1.0/tests/testers/test_application_tester.py000066400000000000000000000036401451777547300226200ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar import pytest from cleo.application import Application from cleo.commands.command import Command from cleo.helpers import argument from cleo.helpers import option from cleo.testers.application_tester import ApplicationTester if TYPE_CHECKING: from cleo.io.inputs.argument import Argument from cleo.io.inputs.option import Option class FooCommand(Command): """ Foo command """ name = "foo" description = "Foo command" arguments: ClassVar[list[Argument]] = [argument("foo")] options: ClassVar[list[Option]] = [option("--bar")] def handle(self) -> int: self.line(self.argument("foo")) if self.option("bar"): self.line("--bar activated") return 0 class FooBarCommand(Command): """ Foo Bar command """ name = "foo bar" description = "Foo Bar command" arguments: ClassVar[list[Argument]] = [argument("foo")] options: ClassVar[list[Option]] = [option("--baz")] def handle(self) -> int: self.line(self.argument("foo")) if self.option("baz"): self.line("--baz activated") return 0 @pytest.fixture() def app() -> Application: app = Application() app.add(FooCommand()) app.add(FooBarCommand()) return app @pytest.fixture() def tester(app: Application) -> ApplicationTester: return ApplicationTester(app) def test_execute(tester: ApplicationTester) -> None: assert tester.execute("foo baz --bar") == 0 assert tester.status_code == 0 assert tester.io.fetch_output() == "baz\n--bar activated\n" def test_execute_namespace_command(tester: ApplicationTester) -> None: tester.application.catch_exceptions(False) assert tester.execute("foo bar baz --baz") == 0 assert tester.status_code == 0 assert tester.io.fetch_output() == "baz\n--baz activated\n" cleo-2.1.0/tests/testers/test_command_tester.py000066400000000000000000000023771451777547300217410ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from typing import ClassVar import pytest from cleo.application import Application from cleo.commands.command import Command from cleo.helpers import argument from cleo.testers.command_tester import CommandTester if TYPE_CHECKING: from cleo.io.inputs.argument import Argument class FooCommand(Command): name = "foo" description = "Foo command" arguments: ClassVar[list[Argument]] = [argument("foo", description="Foo argument")] def handle(self) -> int: self.line(self.argument("foo")) return 0 class FooBarCommand(Command): name = "foo bar" def handle(self) -> int: self.line("foo bar called") return 0 @pytest.fixture() def tester() -> CommandTester: return CommandTester(FooCommand()) def test_execute(tester: CommandTester) -> None: assert tester.execute("bar") == 0 assert tester.status_code == 0 assert tester.io.fetch_output() == "bar\n" def test_execute_namespace_command() -> None: app = Application() app.add(FooBarCommand()) tester = CommandTester(app.find("foo bar")) assert tester.execute() == 0 assert tester.status_code == 0 assert tester.io.fetch_output() == "foo bar called\n" cleo-2.1.0/tests/ui/000077500000000000000000000000001451777547300142375ustar00rootroot00000000000000cleo-2.1.0/tests/ui/__init__.py000066400000000000000000000000001451777547300163360ustar00rootroot00000000000000cleo-2.1.0/tests/ui/test_choice_question.py000066400000000000000000000052771451777547300210440ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest from cleo.exceptions import CleoValueError from cleo.ui.choice_question import ChoiceQuestion if TYPE_CHECKING: from cleo.io.buffered_io import BufferedIO def test_ask_choice(io: BufferedIO) -> None: io.set_user_input( "\n" "1\n" " 1 \n" "John\n" "1\n" "\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 question.ask(io) == "Spiderman" question = ChoiceQuestion("What is your favorite superhero?", heroes) question.set_max_attempts(1) assert question.ask(io) == "Batman" assert question.ask(io) == "Batman" 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 question.ask(io) == "Batman" assert 'Input "John" is not a superhero!' in io.fetch_error() # Empty answer and no default is None assert question.ask(io) is None question = ChoiceQuestion("What is your favorite superhero?", heroes, "1") question.set_max_attempts(1) with pytest.raises(Exception) as e: question.ask(io) assert str(e.value) == 'Value "John" is invalid' question = ChoiceQuestion("What is your favorite superhero?", heroes) question.set_max_attempts(1) question.set_multi_select(True) assert question.ask(io) == ["Batman"] assert question.ask(io) == ["Superman", "Spiderman"] assert question.ask(io) == ["Superman", "Spiderman"] question = ChoiceQuestion("What is your favorite superhero?", heroes, "0,1") question.set_max_attempts(1) question.set_multi_select(True) assert question.ask(io) == ["Superman", "Batman"] question = ChoiceQuestion("What is your favorite superhero?", heroes, " 0 , 1 ") question.set_max_attempts(1) question.set_multi_select(True) assert question.ask(io) == ["Superman", "Batman"] question = ChoiceQuestion("What is your favourite superhero?", heroes) question.set_max_attempts(1) with pytest.raises(CleoValueError) as e: question.ask(io) assert str(e.value) == 'Value "4" is invalid' assert question.ask(io) == "Superman" with pytest.raises(CleoValueError) as e: question.ask(io) assert str(e.value) == 'Value "-2" is invalid' cleo-2.1.0/tests/ui/test_confirmation_question.py000066400000000000000000000017711451777547300222750ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest from cleo.ui.confirmation_question import ConfirmationQuestion if TYPE_CHECKING: from cleo.io.buffered_io import BufferedIO @pytest.mark.parametrize( ("input", "expected", "default"), [ ("", True, True), ("", False, False), ("y", True, True), ("yes", True, True), ("n", False, True), ("no", False, True), ], ) def test_ask(io: BufferedIO, input: str, expected: bool, default: bool) -> None: io.set_user_input(f"{input}\n") question = ConfirmationQuestion("Do you like French fries?", default) assert question.ask(io) == expected def test_ask_with_custom_answer(io: BufferedIO) -> None: io.set_user_input("j\ny\n") question = ConfirmationQuestion("Do you like French fries?", False, r"(?i)^(j|y)") assert question.ask(io) question = ConfirmationQuestion("Do you like French fries?", False, r"(?i)^(j|y)") assert question.ask(io) cleo-2.1.0/tests/ui/test_exception_trace.py000066400000000000000000000247721451777547300210400ustar00rootroot00000000000000# NOTE: these tests reference line numbers from code in this file, # so it's sensitive to refactoring from __future__ import annotations import re import pytest from cleo.io.buffered_io import BufferedIO from cleo.io.outputs.output import Verbosity from cleo.ui.exception_trace import ExceptionTrace from tests.fixtures.exceptions import nested1 from tests.fixtures.exceptions import nested2 from tests.fixtures.exceptions import recursion from tests.fixtures.exceptions import simple from tests.fixtures.exceptions import solution def test_render_better_error_message() -> None: io = BufferedIO() try: simple.simple_exception() except Exception as e: trace = ExceptionTrace(e) trace.render(io) expected = f"""\ Exception Failed at {trace._get_relative_file_path(simple.__file__)}:2 in simple_exception 1│ def simple_exception() -> None: → 2│ raise Exception("Failed") 3│ """ assert expected == io.fetch_output() def test_render_debug_better_error_message() -> None: io = BufferedIO() io.set_verbosity(Verbosity.DEBUG) try: simple.simple_exception() except Exception as e: # Exception trace = ExceptionTrace(e) trace.render(io) lineno = 48 expected = f""" Stack trace: 1 {trace._get_relative_file_path(__file__)}:{lineno} in \ test_render_debug_better_error_message {lineno - 2}│ {lineno - 1}│ try: → {lineno + 0}│ simple.simple_exception() {lineno + 1}│ except Exception as e: # Exception {lineno + 2}│ trace = ExceptionTrace(e) Exception Failed at {trace._get_relative_file_path(simple.__file__)}:2 in simple_exception 1│ def simple_exception() -> None: → 2│ raise Exception("Failed") 3│ """ assert io.fetch_output() == expected def test_render_debug_better_error_message_recursion_error() -> None: io = BufferedIO() io.set_verbosity(Verbosity.DEBUG) try: recursion.recursion_error() except RecursionError as e: trace = ExceptionTrace(e) lineno = 84 trace.render(io) expected = rf"""^ Stack trace: \d+ {re.escape(trace._get_relative_file_path(__file__))}:{lineno} in test_render_debug_better_error_message_recursion_error {lineno - 2}\│ {lineno - 1}\│ try: → {lineno + 0}\│ recursion.recursion_error\(\) {lineno + 1}\│ except RecursionError as e: {lineno + 2}\│ trace = ExceptionTrace\(e\) ... Previous frame repeated \d+ times \s*\d+ {re.escape(trace._get_relative_file_path(recursion.__file__))}:2 in recursion_error 1\│ def recursion_error\(\) -> None: → 2\│ recursion_error\(\) 3\│ RecursionError maximum recursion depth exceeded at {re.escape(trace._get_relative_file_path(recursion.__file__))}:2 in recursion_error 1\│ def recursion_error\(\) -> None: → 2\│ recursion_error\(\) 3\│ """ assert re.match(expected, io.fetch_output()) is not None def test_render_very_verbose_better_error_message() -> None: io = BufferedIO() io.set_verbosity(Verbosity.VERY_VERBOSE) try: simple.simple_exception() except Exception as e: # Exception trace = ExceptionTrace(e) trace.render(io) expected = f""" Stack trace: 1 {trace._get_relative_file_path(__file__)}:126 in \ test_render_very_verbose_better_error_message simple.simple_exception() Exception Failed at {trace._get_relative_file_path(simple.__file__)}:2 in simple_exception 1│ def simple_exception() -> None: → 2│ raise Exception("Failed") 3│ """ assert expected == io.fetch_output() def test_render_debug_better_error_message_recursion_error_with_multiple_duplicated_frames() -> ( None ): def first() -> None: def second() -> None: first() second() io = BufferedIO() io.set_verbosity(Verbosity.VERY_VERBOSE) with pytest.raises(RecursionError) as e: first() trace = ExceptionTrace(e.value) trace.render(io) expected = r"... Previous 2 frames repeated \d+ times" assert re.search(expected, io.fetch_output()) is not None def test_render_can_ignore_given_files() -> None: io = BufferedIO() io.set_verbosity(Verbosity.VERY_VERBOSE) with pytest.raises(Exception) as e: nested2.call() trace = ExceptionTrace(e.value) trace.ignore_files_in(rf"^{re.escape(nested1.__file__)}$") trace.render(io) lineno = 181 expected = f""" Stack trace: 2 {trace._get_relative_file_path(__file__)}:{lineno} in \ test_render_can_ignore_given_files nested2.call() 1 {trace._get_relative_file_path(nested2.__file__)}:8 in call run() Exception Foo at {trace._get_relative_file_path(nested1.__file__)}:3 in inner 1│ def outer() -> None: 2│ def inner() -> None: → 3│ raise Exception("Foo") 4│ 5│ inner() 6│ """ assert io.fetch_output() == expected def test_render_shows_ignored_files_if_in_debug_mode() -> None: io = BufferedIO() io.set_verbosity(Verbosity.DEBUG) with pytest.raises(Exception) as e: nested2.call() trace = ExceptionTrace(e.value) trace.ignore_files_in(rf"^{re.escape(nested1.__file__)}$") trace.render(io) lineno = 219 expected = f""" Stack trace: 4 {trace._get_relative_file_path(__file__)}:{lineno} in \ test_render_shows_ignored_files_if_in_debug_mode {lineno - 2}│ {lineno - 1}│ with pytest.raises(Exception) as e: → {lineno + 0}│ nested2.call() {lineno + 1}│ {lineno + 2}│ trace = ExceptionTrace(e.value) 3 {trace._get_relative_file_path(nested2.__file__)}:8 in call 6│ outer() 7│ → 8│ run() 9│ 2 {trace._get_relative_file_path(nested2.__file__)}:6 in run 4│ def call() -> None: 5│ def run() -> None: → 6│ outer() 7│ 8│ run() 1 {trace._get_relative_file_path(nested1.__file__)}:5 in outer 3│ raise Exception("Foo") 4│ → 5│ inner() 6│ Exception Foo at {trace._get_relative_file_path(nested1.__file__)}:3 in inner 1│ def outer() -> None: 2│ def inner() -> None: → 3│ raise Exception("Foo") 4│ 5│ inner() 6│ """ assert io.fetch_output() == expected def test_render_supports_solutions() -> None: from crashtest.solution_providers.solution_provider_repository import ( SolutionProviderRepository, ) io = BufferedIO() with pytest.raises(solution.CustomError) as e: solution.call() trace = ExceptionTrace( e.value, solution_provider_repository=SolutionProviderRepository() ) trace.render(io) expected = f""" CustomError Error with solution at {trace._get_relative_file_path(solution.__file__)}:16 in call 12│ return solution 13│ 14│ 15│ def call() -> None: → 16│ raise CustomError("Error with solution") 17│ • Solution Title: Solution Description https://example.com, https://example2.com """ assert io.fetch_output() == expected def test_render_falls_back_on_ascii_symbols() -> None: from crashtest.solution_providers.solution_provider_repository import ( SolutionProviderRepository, ) io = BufferedIO(supports_utf8=False) with pytest.raises(solution.CustomError) as e: solution.call() trace = ExceptionTrace( e.value, solution_provider_repository=SolutionProviderRepository() ) trace.render(io) expected = f""" CustomError Error with solution at {trace._get_relative_file_path(solution.__file__)}:16 in call 12| return solution 13| 14| 15| def call() -> None: > 16| raise CustomError("Error with solution") 17| * Solution Title: Solution Description https://example.com, https://example2.com """ assert io.fetch_output() == expected def test_empty_source_file_do_not_break_highlighter() -> None: from cleo.ui.exception_trace import Highlighter highlighter = Highlighter() highlighter.highlighted_lines("") def test_doctrings_are_corrrectly_rendered() -> None: from cleo.formatters.formatter import Formatter from cleo.ui.exception_trace import Highlighter source = ''' def test(): """ Doctring """ ... ''' formatter = Formatter() highlighter = Highlighter() lines = highlighter.highlighted_lines(source) assert [formatter.format(line) for line in lines] == [*source.splitlines(), ""] def test_simple_render() -> None: io = BufferedIO() with pytest.raises(Exception) as e: simple.simple_exception() trace = ExceptionTrace(e.value) trace.render(io, simple=True) expected = """ Failed """ assert io.fetch_output() == expected def test_simple_render_supports_solutions() -> None: from crashtest.solution_providers.solution_provider_repository import ( SolutionProviderRepository, ) io = BufferedIO() with pytest.raises(solution.CustomError) as e: solution.call() trace = ExceptionTrace( e.value, solution_provider_repository=SolutionProviderRepository() ) trace.render(io, simple=True) expected = """ Error with solution • Solution Title: Solution Description https://example.com, https://example2.com """ assert io.fetch_output() == expected def test_simple_render_aborts_if_no_message() -> None: io = BufferedIO() with pytest.raises(Exception) as e: raise AssertionError trace = ExceptionTrace(e.value) trace.render(io, simple=True) lineno = 419 expected = f""" AssertionError at {trace._get_relative_file_path(__file__)}:{lineno} in \ test_simple_render_aborts_if_no_message {lineno - 4}│ def test_simple_render_aborts_if_no_message() -> None: {lineno - 3}│ io = BufferedIO() {lineno - 2}│ {lineno - 1}│ with pytest.raises(Exception) as e: → {lineno + 0}│ raise AssertionError {lineno + 1}│ {lineno + 2}│ trace = ExceptionTrace(e.value) {lineno + 3}│ {lineno + 4}│ trace.render(io, simple=True) """ assert expected == io.fetch_output() cleo-2.1.0/tests/ui/test_progress_bar.py000066400000000000000000000310521451777547300203410ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest from cleo.io.outputs.output import Verbosity from cleo.ui.progress_bar import ProgressBar if TYPE_CHECKING: from typing import Callable from cleo.io.buffered_io import BufferedIO @pytest.fixture() def bar(io: BufferedIO) -> ProgressBar: return ProgressBar(io, min_seconds_between_redraws=0) @pytest.fixture() def ansi_bar(ansi_io: BufferedIO) -> ProgressBar: return ProgressBar(ansi_io, min_seconds_between_redraws=0) def generate_output(expected: list[str]) -> str: output = "" for i, line in enumerate(expected): if i: count = line.count("\n") if count: output += f"\x1B[{count}A\x1B[1G\x1b[2K" else: output += "\x1b[1G\x1b[2K" output += line return output def test_multiple_start(ansi_bar: ProgressBar, ansi_io: BufferedIO) -> None: ansi_bar.start() ansi_bar.advance() ansi_bar.start() output = [ " 0 [>---------------------------]", " 1 [->--------------------------]", " 0 [>---------------------------]", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_advance(ansi_bar: ProgressBar, ansi_io: BufferedIO) -> None: ansi_bar.start() ansi_bar.advance() output = [ " 0 [>---------------------------]", " 1 [->--------------------------]", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_advance_with_step(ansi_bar: ProgressBar, ansi_io: BufferedIO) -> None: ansi_bar.start() ansi_bar.advance(5) output = [ " 0 [>---------------------------]", " 5 [----->----------------------]", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_advance_multiple_times(ansi_bar: ProgressBar, ansi_io: BufferedIO) -> None: ansi_bar.start() ansi_bar.advance(3) ansi_bar.advance(2) output = [ " 0 [>---------------------------]", " 3 [--->------------------------]", " 5 [----->----------------------]", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_advance_over_max(ansi_io: BufferedIO) -> None: bar = ProgressBar(ansi_io, 10) bar.set_progress(9) bar.advance() bar.advance() output = [ " 9/10 [=========================>--] 90%", " 10/10 [============================] 100%", " 11/11 [============================] 100%", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_format(ansi_io: BufferedIO) -> None: output = [ " 0/10 [>---------------------------] 0%", " 10/10 [============================] 100%", ] expected = generate_output(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: BufferedIO) -> None: 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 = generate_output(output) assert expected == ansi_io.fetch_error() def test_display_without_start(ansi_io: BufferedIO) -> None: bar = ProgressBar(ansi_io, 50, 0) bar.display() expected = " 0/50 [>---------------------------] 0%" assert ansi_io.fetch_error() == expected def test_display_with_quiet_verbosity(ansi_io: BufferedIO) -> None: ansi_io.set_verbosity(Verbosity.QUIET) bar = ProgressBar(ansi_io, 50, 0) bar.display() assert ansi_io.fetch_error() == "" def test_finish_without_start(ansi_io: BufferedIO) -> None: bar = ProgressBar(ansi_io, 50, 0) bar.finish() expected = " 50/50 [============================] 100%" assert ansi_io.fetch_error() == expected def test_percent(ansi_io: BufferedIO) -> None: bar = ProgressBar(ansi_io, 50, 0) bar.start() bar.display() bar.advance() bar.advance() output = [ " 0/50 [>---------------------------] 0%", " 1/50 [>---------------------------] 2%", " 2/50 [=>--------------------------] 4%", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_overwrite_with_shorter_line(ansi_io: BufferedIO) -> None: 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%", " 1/50 [>---------------------------] 2%", " 2/50 [=>--------------------------]", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_set_current_progress(ansi_io: BufferedIO) -> None: bar = ProgressBar(ansi_io, 50, 0) bar.start() bar.display() bar.advance() bar.set_progress(15) bar.set_progress(25) output = [ " 0/50 [>---------------------------] 0%", " 1/50 [>---------------------------] 2%", " 15/50 [========>-------------------] 30%", " 25/50 [==============>-------------] 50%", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_multibyte_support(ansi_bar: ProgressBar, ansi_io: BufferedIO) -> None: ansi_bar.start() ansi_bar.set_bar_character("■") ansi_bar.advance(3) output = [ " 0 [>---------------------------]", " 3 [■■■>------------------------]", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_clear(ansi_io: BufferedIO) -> None: bar = ProgressBar(ansi_io, 50, 0) bar.start() bar.set_progress(25) bar.clear() output = [ " 0/50 [>---------------------------] 0%", " 25/50 [==============>-------------] 50%", "", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_percent_not_hundred_before_complete(ansi_io: BufferedIO) -> None: bar = ProgressBar(ansi_io, 200, 0) bar.start() bar.display() bar.advance(199) bar.advance() output = [ " 0/200 [>---------------------------] 0%", " 199/200 [===========================>] 99%", " 200/200 [============================] 100%", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_non_decorated_output(io: BufferedIO) -> None: bar = ProgressBar(io, 200, 0) bar.start() for _ 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: BufferedIO) -> None: bar = ProgressBar(ansi_io, 3, 0) bar.set_format("%bar%\nfoobar") bar.start() bar.advance() bar.clear() bar.finish() output = [ ">---------------------------\nfoobar", "=========>------------------\nfoobar", "\n", "============================\nfoobar", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_regress(ansi_bar: ProgressBar, ansi_io: BufferedIO) -> None: ansi_bar.start() ansi_bar.advance() ansi_bar.advance() ansi_bar.advance(-1) output = [ " 0 [>---------------------------]", " 1 [->--------------------------]", " 2 [-->-------------------------]", " 1 [->--------------------------]", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_regress_with_steps(ansi_bar: ProgressBar, ansi_io: BufferedIO) -> None: ansi_bar.start() ansi_bar.advance(4) ansi_bar.advance(4) ansi_bar.advance(-2) output = [ " 0 [>---------------------------]", " 4 [---->-----------------------]", " 8 [-------->-------------------]", " 6 [------>---------------------]", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_regress_multiple_times(ansi_bar: ProgressBar, ansi_io: BufferedIO) -> None: 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 = generate_output(output) assert expected == ansi_io.fetch_error() def test_regress_below_min(ansi_io: BufferedIO) -> None: bar = ProgressBar(ansi_io, 10, 0) bar.set_progress(1) bar.advance(-1) bar.advance(-1) output = [ " 1/10 [==>-------------------------] 10%", " 0/10 [>---------------------------] 0%", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() def test_overwrite_with_section_output(ansi_io: BufferedIO) -> None: bar = ProgressBar(ansi_io.section(), 50, 0) bar.start() bar.display() bar.advance() bar.advance() output = [ " 0/50 [>---------------------------] 0%", " 1/50 [>---------------------------] 2%", " 2/50 [=>--------------------------] 4%", ] expected = "\n\x1b[1A\x1b[0J".join(output) + "\n" assert expected == ansi_io.fetch_output() def test_overwrite_multiple_progress_bars_with_section_outputs( ansi_io: BufferedIO, ) -> None: 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_output() def test_min_and_max_seconds_between_redraws( ansi_bar: ProgressBar, ansi_io: BufferedIO, sleep: Callable[[float], None] ) -> None: ansi_bar.min_seconds_between_redraws(0.5) ansi_bar.max_seconds_between_redraws(2 - 1) ansi_bar.start() ansi_bar.set_progress(1) sleep(1) ansi_bar.set_progress(2) sleep(2) ansi_bar.set_progress(3) output = [ " 0 [>---------------------------]", " 2 [->--------------------------]", " 3 [-->-------------------------]", ] expected = generate_output(output) assert expected == ansi_io.fetch_error() cleo-2.1.0/tests/ui/test_progress_indicator.py000066400000000000000000000056631451777547300215620ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from cleo.ui.progress_indicator import ProgressIndicator if TYPE_CHECKING: from typing import Callable from cleo.io.buffered_io import BufferedIO def test_default_indicator(ansi_io: BufferedIO, sleep: Callable[[float], None]) -> None: bar = ProgressIndicator(ansi_io) bar.start("Starting...") sleep(0.101) bar.advance() sleep(0.101) bar.advance() sleep(0.101) bar.advance() sleep(0.101) bar.advance() sleep(0.101) bar.advance() sleep(0.101) bar.set_message("Advancing...") bar.advance() bar.finish("Done...") bar.start("Starting Again...") sleep(0.101) bar.advance() bar.finish("Done Again...") bar.start("Starting Again...") 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: BufferedIO, sleep: Callable[[float], None]) -> None: bar = ProgressIndicator(ansi_io, ProgressIndicator.NORMAL) bar.start("Starting...") sleep(0.101) bar.advance() sleep(0.101) bar.advance() sleep(0.101) bar.advance() sleep(0.101) bar.advance() sleep(0.101) bar.advance() sleep(0.101) bar.set_message("Advancing...") bar.advance() bar.finish("Done...") bar.start("Starting Again...") sleep(0.101) bar.advance() bar.finish("Done Again...") bar.start("Starting Again...") 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() cleo-2.1.0/tests/ui/test_question.py000066400000000000000000000041661451777547300175260ustar00rootroot00000000000000from __future__ import annotations import os import subprocess from typing import TYPE_CHECKING import pytest from cleo.ui.question import Question if TYPE_CHECKING: from cleo.io.buffered_io import BufferedIO def has_tty_available() -> bool: with open(os.devnull, "w") as devnull: exit_code = subprocess.call(["stty", "2"], stdout=devnull, stderr=devnull) return exit_code == 0 TTY_AVAILABLE = has_tty_available() def test_ask(io: BufferedIO) -> None: question = Question("What time is it?", "2PM") io.set_user_input("\n8AM\n") assert question.ask(io) == "2PM" io.clear_error() assert question.ask(io) == "8AM" assert io.fetch_error() == "What time is it? " @pytest.mark.skipif( not TTY_AVAILABLE, reason="`stty` is required to test hidden response functionality" ) def test_ask_hidden_response(io: BufferedIO) -> None: question = Question("What time is it?", "2PM") question.hide() io.set_user_input("8AM\n") assert question.ask(io) == "8AM" assert io.fetch_error() == "What time is it? " def test_ask_and_validate(io: BufferedIO) -> None: error = "This is not a color!" def validator(color: str) -> str: 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_user_input("\nblack\n") assert question.ask(io) == "white" assert question.ask(io) == "black" io.set_user_input("green\nyellow\norange\n") with pytest.raises(Exception) as e: question.ask(io) assert str(e.value) == error def test_no_interaction(io: BufferedIO) -> None: io.interactive(False) question = Question("Do you have a job?", "not yet") assert question.ask(io) == "not yet" def test_ask_question_with_special_characters(io: BufferedIO) -> None: question = Question("What time is it, Sébastien?", "2PMë") io.set_user_input("\n") assert question.ask(io) == "2PMë" assert io.fetch_error() == "What time is it, Sébastien? " cleo-2.1.0/tests/ui/test_table.py000066400000000000000000000446241451777547300167510ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest from cleo.ui.table import Table from cleo.ui.table_cell import TableCell from cleo.ui.table_separator import TableSeparator from cleo.ui.table_style import TableStyle if TYPE_CHECKING: from cleo.io.buffered_io import BufferedIO from cleo.ui.table import Rows books = [ ["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"], ["9782070409341", "Le Père Goriot", "Honoré de Balzac"], ] @pytest.mark.parametrize( ["headers", "rows", "style", "expected"], [ ( ["ISBN", "Title", "Author"], books, "default", """\ +---------------+--------------------------+------------------+ | 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 | | 9782070409341 | Le Père Goriot | Honoré de Balzac | +---------------+--------------------------+------------------+ """, ), ( ["ISBN", "Title", "Author"], books, "compact", """\ 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 9782070409341 Le Père Goriot Honoré de Balzac """, ), ( ["ISBN", "Title", "Author"], books, "borderless", """\ =============== ========================== ================== 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 9782070409341 Le Père Goriot Honoré de Balzac =============== ========================== ================== """, ), ( ["ISBN", "Title", "Author"], books, "box", """\ ┌───────────────┬──────────────────────────┬──────────────────┐ │ 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 │ │ 9782070409341 │ Le Père Goriot │ Honoré de Balzac │ └───────────────┴──────────────────────────┴──────────────────┘ """, ), ( ["ISBN", "Title", "Author"], [ ["99921-58-10-7", "Divine Comedy", "Dante Alighieri"], ["9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens"], TableSeparator(), ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ], "box-double", """\ ╔═══════════════╤══════════════════════════╤══════════════════╗ ║ 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 ║ ╚═══════════════╧══════════════════════════╧══════════════════╝ """, ), ( ["ISBN", "Title"], [ ["99921-58-10-7", "Divine Comedy", "Dante Alighieri"], ["9971-5-0210-0"], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ], "default", """\ +---------------+--------------------------+------------------+ | ISBN | Title | | +---------------+--------------------------+------------------+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri | | 9971-5-0210-0 | | | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------------------------+------------------+ """, ), ( [], [ ["99921-58-10-7", "Divine Comedy", "Dante Alighieri"], ["9971-5-0210-0"], ["960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"], ["80-902734-1-6", "And Then There Were None", "Agatha Christie"], ], "default", """\ +---------------+--------------------------+------------------+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri | | 9971-5-0210-0 | | | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------------------------+------------------+ """, ), ( ["ISBN", "Title"], [], "default", """\ +------+-------+ | ISBN | Title | +------+-------+ """, ), ([], [], "default", ""), ( ["ISBN", "Title", "Author"], [ ["99921-58-10-7", "Divine\nComedy", "Dante Alighieri"], [ "9971-5-0210-2", "Harry Potter\nand the Chamber of Secrets", "Rowling\nJoanne K.", ], [ "9971-5-0210-2", "Harry Potter\nand the Chamber of Secrets", "Rowling\nJoanne K.", ], ["960-425-059-0", "The Lord of the Rings", "J. R. R.\nTolkien"], ], "default", """\ +---------------+----------------------------+-----------------+ | ISBN | Title | Author | +---------------+----------------------------+-----------------+ | 99921-58-10-7 | Divine | Dante Alighieri | | | Comedy | | | 9971-5-0210-2 | Harry Potter | Rowling | | | and the Chamber of Secrets | Joanne K. | | 9971-5-0210-2 | Harry Potter | Rowling | | | and the Chamber of Secrets | Joanne K. | | 960-425-059-0 | The Lord of the Rings | J. R. R. | | | | Tolkien | +---------------+----------------------------+-----------------+ """, ), ( ["ISBN", "Title", "Author"], [ [ "99921-58-10-7", "Divine Comedy", "Dante Alighieri", ], ["9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens"], ], "default", """\ +---------------+----------------------+-----------------+ | ISBN | Title | Author | +---------------+----------------------+-----------------+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | +---------------+----------------------+-----------------+ """, ), ( ["ISBN", "Title", "Author"], [ ["99921-58-10-7", "Divine Comedy", "Dante Alighieri"], TableSeparator(), [TableCell("Divine Comedy(Dante Alighieri)", colspan=3)], TableSeparator(), [TableCell("Arduino: A Quick-Start Guide", colspan=2), "Mark Schmidt"], TableSeparator(), ["9971-5-0210-0", TableCell("A Tale of \nTwo Cities", colspan=2)], ], "default", """\ +----------------+----------------+-----------------+ | ISBN | Title | Author | +----------------+----------------+-----------------+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri | +----------------+----------------+-----------------+ | Divine Comedy(Dante Alighieri) | +----------------+----------------+-----------------+ | Arduino: A Quick-Start Guide | Mark Schmidt | +----------------+----------------+-----------------+ | 9971-5-0210-0 | A Tale of | | | Two Cities | +----------------+----------------+-----------------+ """, ), ( ["ISBN", "Title", "Author"], [ [ TableCell("9971-5-0210-0", rowspan=3), "Divine Comedy", "Dante Alighieri", ], ["A Tale of Two Cities", "Charles Dickens"], ["The Lord of \nthe Rings", "J. R. \nR. Tolkien"], TableSeparator(), [ "80-902734-1-6", TableCell("And Then \nThere \nWere None", rowspan=3), "Agatha Christie", ], ["80-902734-1-7", "Test"], ], "default", """\ +---------------+----------------------+-----------------+ | ISBN | Title | Author | +---------------+----------------------+-----------------+ | 9971-5-0210-0 | Divine Comedy | Dante Alighieri | | | A Tale of Two Cities | Charles Dickens | | | The Lord of | J. R. | | | the Rings | R. Tolkien | +---------------+----------------------+-----------------+ | 80-902734-1-6 | And Then | Agatha Christie | | 80-902734-1-7 | There | Test | | | Were None | | +---------------+----------------------+-----------------+ """, ), ( ["ISBN", "Title", "Author"], [ [TableCell("9971-5-0210-0", rowspan=2, colspan=2), "Dante Alighieri"], ["Charles Dickens"], TableSeparator(), ["Dante Alighieri", TableCell("9971-5-0210-0", rowspan=3, colspan=2)], ["J. R. R. Tolkien"], ["J. R. R"], ], "default", """\ +------------------+---------+-----------------+ | ISBN | Title | Author | +------------------+---------+-----------------+ | 9971-5-0210-0 | Dante Alighieri | | | Charles Dickens | +------------------+---------+-----------------+ | Dante Alighieri | 9971-5-0210-0 | | J. R. R. Tolkien | | | J. R. R | | +------------------+---------+-----------------+ """, ), ( ["ISBN", "Title", "Author"], [ [ TableCell("9971\n-5-\n021\n0-0", rowspan=2, colspan=2), "Dante Alighieri", ], ["Charles Dickens"], TableSeparator(), [ "Dante Alighieri", TableCell("9971\n-5-\n021\n0-0", rowspan=2, colspan=2), ], ["Charles Dickens"], TableSeparator(), [ TableCell("9971\n-5-\n021\n0-0", rowspan=2, colspan=2), TableCell("Dante \nAlighieri", rowspan=2, colspan=1), ], ], "default", """\ +-----------------+-------+-----------------+ | ISBN | Title | Author | +-----------------+-------+-----------------+ | 9971 | Dante Alighieri | | -5- | Charles Dickens | | 021 | | | 0-0 | | +-----------------+-------+-----------------+ | Dante Alighieri | 9971 | | Charles Dickens | -5- | | | 021 | | | 0-0 | +-----------------+-------+-----------------+ | 9971 | Dante | | -5- | Alighieri | | 021 | | | 0-0 | | +-----------------+-------+-----------------+ """, ), ( ["ISBN", "Title", "Author"], [ [ TableCell("9971\n-5-\n021\n0-0", rowspan=2, colspan=2), "Dante Alighieri", ], ["Charles Dickens"], [ "Dante Alighieri", TableCell("9971\n-5-\n021\n0-0", rowspan=2, colspan=2), ], ["Charles Dickens"], ], "default", """\ +-----------------+-------+-----------------+ | ISBN | Title | Author | +-----------------+-------+-----------------+ | 9971 | Dante Alighieri | | -5- | Charles Dickens | | 021 | | | 0-0 | | | Dante Alighieri | 9971 | | Charles Dickens | -5- | | | 021 | | | 0-0 | +-----------------+-------+-----------------+ """, ), ( ["ISBN", "Author"], [ [TableCell("9971-5-0210-0", rowspan=3, colspan=1), "Dante Alighieri"], [TableSeparator()], ["Charles Dickens"], ], "default", """\ +---------------+-----------------+ | ISBN | Author | +---------------+-----------------+ | 9971-5-0210-0 | Dante Alighieri | | |-----------------| | | Charles Dickens | +---------------+-----------------+ """, ), ( [[TableCell("Main title", colspan=3)], ["ISBN", "Title", "Author"]], [], "default", """\ +------+-------+--------+ | Main title | +------+-------+--------+ | ISBN | Title | Author | +------+-------+--------+ """, ), ( [], [ [ TableCell("1", colspan=3), TableCell("2", colspan=2), TableCell("3", colspan=2), TableCell("4", colspan=2), ] ], "default", """\ +---+--+--+---+--+---+--+---+--+ | 1 | 2 | 3 | 4 | +---+--+--+---+--+---+--+---+--+ """, ), ], ) def test_render( io: BufferedIO, headers: list[str], rows: Rows, style: str, expected: str ) -> None: table = Table(io, style=style) table.set_headers(headers) table.set_rows(rows) table.render() assert io.fetch_output() == expected def test_column_style(io: BufferedIO) -> None: table = Table(io) table.set_headers(["ISBN", "Title", "Author", "Price"]) table.set_rows( [ ["99921-58-10-7", "Divine Comedy", "Dante Alighieri", "9.95"], ["9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens", "139.25"], ] ) style = TableStyle() style.set_pad_type("left") table.set_column_style(3, style) table.render() expected = """\ +---------------+----------------------+-----------------+--------+ | ISBN | Title | Author | Price | +---------------+----------------------+-----------------+--------+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri | 9.95 | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 139.25 | +---------------+----------------------+-----------------+--------+ """ assert io.fetch_output() == expected def test_style_for_side_effects(io: BufferedIO) -> None: headers = ["Type", "Class", "Name"] rows: Rows = [ ["GSV", "Range", "Bora Horza Gobuchul"], ["GSV", "Plate", "Sleeper Service"], ["GCU", "Ridge", "Grey Area"], ["PS", "Abominator", "Falling Outside the Normal Moral Constraints"], ] table1 = Table(io) table1.set_headers(headers) table1.set_rows(rows) table1.style.set_vertical_border_chars("x", "y") table1.render() output1 = io.fetch_output() table2 = Table(io) table2.set_headers(headers) table2.set_rows(rows) table2.render() output2 = io.fetch_output() assert output1 != output2