New upstream version 8.0.2
This commit is contained in:
parent
4e974d1c0d
commit
46bc5bd117
13
.editorconfig
Normal file
13
.editorconfig
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
max_line_length = 88
|
||||||
|
|
||||||
|
[*.{yml,yaml,json,js,css,html}]
|
||||||
|
indent_size = 2
|
27
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Report a bug in Click (not other projects which depend on Click)
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This issue tracker is a tool to address bugs in Click itself. Please use
|
||||||
|
Pallets Discord or Stack Overflow for questions about your own code.
|
||||||
|
|
||||||
|
Replace this comment with a clear outline of what the bug is.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe how to replicate the bug.
|
||||||
|
|
||||||
|
Include a minimal reproducible example that demonstrates the bug.
|
||||||
|
Include the full traceback if there was an exception.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe the expected behavior that should have happened but didn't.
|
||||||
|
-->
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
|
||||||
|
- Python version:
|
||||||
|
- Click version:
|
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Security issue
|
||||||
|
url: security@palletsprojects.com
|
||||||
|
about: Do not report security issues publicly. Email our security contact.
|
||||||
|
- name: Questions
|
||||||
|
url: https://stackoverflow.com/questions/tagged/python-click?tab=Frequent
|
||||||
|
about: Search for and ask questions about your code on Stack Overflow.
|
||||||
|
- name: Questions and discussions
|
||||||
|
url: https://discord.gg/pallets
|
||||||
|
about: Discuss questions about your code on our Discord chat.
|
15
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
15
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest a new feature for Click
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Replace this comment with a description of what the feature should do.
|
||||||
|
Include details such as links relevant specs or previous discussions.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Replace this comment with an example of the problem which this feature
|
||||||
|
would resolve. Is this problem solvable without changes to Click,
|
||||||
|
such as by subclassing or using an extension?
|
||||||
|
-->
|
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: pip
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
time: "08:00"
|
||||||
|
open-pull-requests-limit: 99
|
30
.github/pull_request_template.md
vendored
Normal file
30
.github/pull_request_template.md
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<!--
|
||||||
|
Before opening a PR, open a ticket describing the issue or feature the
|
||||||
|
PR will address. Follow the steps in CONTRIBUTING.rst.
|
||||||
|
|
||||||
|
Replace this comment with a description of the change. Describe how it
|
||||||
|
addresses the linked ticket.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Link to relevant issues or previous PRs, one per line. Use "fixes" to
|
||||||
|
automatically close an issue.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- fixes #<issue number>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to
|
||||||
|
each box below.
|
||||||
|
|
||||||
|
If only docs were changed, these aren't relevant and can be removed.
|
||||||
|
-->
|
||||||
|
|
||||||
|
Checklist:
|
||||||
|
|
||||||
|
- [ ] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
|
||||||
|
- [ ] Add or update relevant docs, in the docs folder and in code.
|
||||||
|
- [ ] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
|
||||||
|
- [ ] Add `.. versionchanged::` entries in any relevant code docs.
|
||||||
|
- [ ] Run `pre-commit` hooks and fix any issues.
|
||||||
|
- [ ] Run `pytest` and `tox`, no tests failed.
|
15
.github/workflows/lock.yaml
vendored
Normal file
15
.github/workflows/lock.yaml
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
name: 'Lock threads'
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lock:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v2
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
issue-lock-inactive-days: 14
|
||||||
|
pr-lock-inactive-days: 14
|
60
.github/workflows/tests.yaml
vendored
Normal file
60
.github/workflows/tests.yaml
vendored
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
name: Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- '*.x'
|
||||||
|
paths-ignore:
|
||||||
|
- 'docs/**'
|
||||||
|
- '*.md'
|
||||||
|
- '*.rst'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- '*.x'
|
||||||
|
paths-ignore:
|
||||||
|
- 'docs/**'
|
||||||
|
- '*.md'
|
||||||
|
- '*.rst'
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: ${{ matrix.name }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- {name: Linux, python: '3.9', os: ubuntu-latest, tox: py39}
|
||||||
|
- {name: Windows, python: '3.9', os: windows-latest, tox: py39}
|
||||||
|
- {name: Mac, python: '3.9', os: macos-latest, tox: py39}
|
||||||
|
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
|
||||||
|
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
|
||||||
|
- {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36}
|
||||||
|
- {name: 'PyPy', python: pypy3, os: ubuntu-latest, tox: pypy3}
|
||||||
|
- {name: Typing, python: '3.9', os: ubuntu-latest, tox: typing}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python }}
|
||||||
|
- name: update pip
|
||||||
|
run: |
|
||||||
|
pip install -U wheel
|
||||||
|
pip install -U setuptools
|
||||||
|
python -m pip install -U pip
|
||||||
|
- name: get pip cache dir
|
||||||
|
id: pip-cache
|
||||||
|
run: echo "::set-output name=dir::$(pip cache dir)"
|
||||||
|
- name: cache pip
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pip-cache.outputs.dir }}
|
||||||
|
key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}|${{ hashFiles('requirements/*.txt') }}
|
||||||
|
- name: cache mypy
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ./.mypy_cache
|
||||||
|
key: mypy|${{ matrix.python }}|${{ hashFiles('setup.cfg') }}
|
||||||
|
if: matrix.tox == 'typing'
|
||||||
|
- run: pip install tox
|
||||||
|
- run: tox -e ${{ matrix.tox }}
|
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/.idea/
|
||||||
|
/.vscode/
|
||||||
|
/env/
|
||||||
|
/venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.egg-info/
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
/.pytest_cache/
|
||||||
|
/.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
/htmlcov/
|
||||||
|
/docs/_build/
|
30
.pre-commit-config.yaml
Normal file
30
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
ci:
|
||||||
|
autoupdate_schedule: monthly
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v2.29.0
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: ["--py36-plus"]
|
||||||
|
- repo: https://github.com/asottile/reorder_python_imports
|
||||||
|
rev: v2.6.0
|
||||||
|
hooks:
|
||||||
|
- id: reorder-python-imports
|
||||||
|
args: ["--application-directories", "src"]
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 21.9b0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
rev: 3.9.2
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
additional_dependencies:
|
||||||
|
- flake8-bugbear
|
||||||
|
- flake8-implicit-str-concat
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.0.1
|
||||||
|
hooks:
|
||||||
|
- id: fix-byte-order-marker
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
9
.readthedocs.yaml
Normal file
9
.readthedocs.yaml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
version: 2
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: requirements/docs.txt
|
||||||
|
- method: pip
|
||||||
|
path: .
|
||||||
|
sphinx:
|
||||||
|
builder: dirhtml
|
||||||
|
fail_on_warning: true
|
293
CHANGES.rst
293
CHANGES.rst
|
@ -1,5 +1,287 @@
|
||||||
.. currentmodule:: click
|
.. currentmodule:: click
|
||||||
|
|
||||||
|
Version 8.0.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2021-10-08
|
||||||
|
|
||||||
|
- ``is_bool_flag`` is not set to ``True`` if ``is_flag`` is ``False``.
|
||||||
|
:issue:`1925`
|
||||||
|
- Bash version detection is locale independent. :issue:`1940`
|
||||||
|
- Empty ``default`` value is not shown for ``multiple=True``.
|
||||||
|
:issue:`1969`
|
||||||
|
- Fix shell completion for arguments that start with a forward slash
|
||||||
|
such as absolute file paths. :issue:`1929`
|
||||||
|
- ``Path`` type with ``resolve_path=True`` resolves relative symlinks
|
||||||
|
to be relative to the containing directory. :issue:`1921`
|
||||||
|
- Completion does not skip Python's resource cleanup when exiting,
|
||||||
|
avoiding some unexpected warning output. :issue:`1738, 2017`
|
||||||
|
- Fix type annotation for ``type`` argument in ``prompt`` function.
|
||||||
|
:issue:`2062`
|
||||||
|
- Fix overline and italic styles, which were incorrectly added when
|
||||||
|
adding underline. :pr:`2058`
|
||||||
|
- An option with ``count=True`` will not show "[x>=0]" in help text.
|
||||||
|
:issue:`2072`
|
||||||
|
- Default values are not cast to the parameter type twice during
|
||||||
|
processing. :issue:`2085`
|
||||||
|
- Options with ``multiple`` and ``flag_value`` use the flag value
|
||||||
|
instead of leaving an internal placeholder. :issue:`2001`
|
||||||
|
|
||||||
|
|
||||||
|
Version 8.0.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2021-05-19
|
||||||
|
|
||||||
|
- Mark top-level names as exported so type checking understand imports
|
||||||
|
in user projects. :issue:`1879`
|
||||||
|
- Annotate ``Context.obj`` as ``Any`` so type checking allows all
|
||||||
|
operations on the arbitrary object. :issue:`1885`
|
||||||
|
- Fix some types that weren't available in Python 3.6.0. :issue:`1882`
|
||||||
|
- Fix type checking for iterating over ``ProgressBar`` object.
|
||||||
|
:issue:`1892`
|
||||||
|
- The ``importlib_metadata`` backport package is installed on Python <
|
||||||
|
3.8. :issue:`1889`
|
||||||
|
- Arguments with ``nargs=-1`` only use env var value if no command
|
||||||
|
line values are given. :issue:`1903`
|
||||||
|
- Flag options guess their type from ``flag_value`` if given, like
|
||||||
|
regular options do from ``default``. :issue:`1886`
|
||||||
|
- Added documentation that custom parameter types may be passed
|
||||||
|
already valid values in addition to strings. :issue:`1898`
|
||||||
|
- Resolving commands returns the name that was given, not
|
||||||
|
``command.name``, fixing an unintended change to help text and
|
||||||
|
``default_map`` lookups. When using patterns like ``AliasedGroup``,
|
||||||
|
override ``resolve_command`` to change the name that is returned if
|
||||||
|
needed. :issue:`1895`
|
||||||
|
- If a default value is invalid, it does not prevent showing help
|
||||||
|
text. :issue:`1889`
|
||||||
|
- Pass ``windows_expand_args=False`` when calling the main command to
|
||||||
|
disable pattern expansion on Windows. There is no way to escape
|
||||||
|
patterns in CMD, so if the program needs to pass them on as-is then
|
||||||
|
expansion must be disabled. :issue:`1901`
|
||||||
|
|
||||||
|
|
||||||
|
Version 8.0.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2021-05-11
|
||||||
|
|
||||||
|
- Drop support for Python 2 and 3.5.
|
||||||
|
- Colorama is always installed on Windows in order to provide style
|
||||||
|
and color support. :pr:`1784`
|
||||||
|
- Adds a repr to Command, showing the command name for friendlier
|
||||||
|
debugging. :issue:`1267`, :pr:`1295`
|
||||||
|
- Add support for distinguishing the source of a command line
|
||||||
|
parameter. :issue:`1264`, :pr:`1329`
|
||||||
|
- Add an optional parameter to ``ProgressBar.update`` to set the
|
||||||
|
``current_item``. :issue:`1226`, :pr:`1332`
|
||||||
|
- ``version_option`` uses ``importlib.metadata`` (or the
|
||||||
|
``importlib_metadata`` backport) instead of ``pkg_resources``. The
|
||||||
|
version is detected based on the package name, not the entry point
|
||||||
|
name. The Python package name must match the installed package
|
||||||
|
name, or be passed with ``package_name=``. :issue:`1582`
|
||||||
|
- If validation fails for a prompt with ``hide_input=True``, the value
|
||||||
|
is not shown in the error message. :issue:`1460`
|
||||||
|
- An ``IntRange`` or ``FloatRange`` option shows the accepted range in
|
||||||
|
its help text. :issue:`1525`, :pr:`1303`
|
||||||
|
- ``IntRange`` and ``FloatRange`` bounds can be open (``<``) instead
|
||||||
|
of closed (``<=``) by setting ``min_open`` and ``max_open``. Error
|
||||||
|
messages have changed to reflect this. :issue:`1100`
|
||||||
|
- An option defined with duplicate flag names (``"--foo/--foo"``)
|
||||||
|
raises a ``ValueError``. :issue:`1465`
|
||||||
|
- ``echo()`` will not fail when using pytest's ``capsys`` fixture on
|
||||||
|
Windows. :issue:`1590`
|
||||||
|
- Resolving commands returns the canonical command name instead of the
|
||||||
|
matched name. This makes behavior such as help text and
|
||||||
|
``Context.invoked_subcommand`` consistent when using patterns like
|
||||||
|
``AliasedGroup``. :issue:`1422`
|
||||||
|
- The ``BOOL`` type accepts the values "on" and "off". :issue:`1629`
|
||||||
|
- A ``Group`` with ``invoke_without_command=True`` will always invoke
|
||||||
|
its result callback. :issue:`1178`
|
||||||
|
- ``nargs == -1`` and ``nargs > 1`` is parsed and validated for
|
||||||
|
values from environment variables and defaults. :issue:`729`
|
||||||
|
- Detect the program name when executing a module or package with
|
||||||
|
``python -m name``. :issue:`1603`
|
||||||
|
- Include required parent arguments in help synopsis of subcommands.
|
||||||
|
:issue:`1475`
|
||||||
|
- Help for boolean flags with ``show_default=True`` shows the flag
|
||||||
|
name instead of ``True`` or ``False``. :issue:`1538`
|
||||||
|
- Non-string objects passed to ``style()`` and ``secho()`` will be
|
||||||
|
converted to string. :pr:`1146`
|
||||||
|
- ``edit(require_save=True)`` will detect saves for editors that exit
|
||||||
|
very fast on filesystems with 1 second resolution. :pr:`1050`
|
||||||
|
- New class attributes make it easier to use custom core objects
|
||||||
|
throughout an entire application. :pr:`938`
|
||||||
|
|
||||||
|
- ``Command.context_class`` controls the context created when
|
||||||
|
running the command.
|
||||||
|
- ``Context.invoke`` creates new contexts of the same type, so a
|
||||||
|
custom type will persist to invoked subcommands.
|
||||||
|
- ``Context.formatter_class`` controls the formatter used to
|
||||||
|
generate help and usage.
|
||||||
|
- ``Group.command_class`` changes the default type for
|
||||||
|
subcommands with ``@group.command()``.
|
||||||
|
- ``Group.group_class`` changes the default type for subgroups
|
||||||
|
with ``@group.group()``. Setting it to ``type`` will create
|
||||||
|
subgroups of the same type as the group itself.
|
||||||
|
- Core objects use ``super()`` consistently for better support of
|
||||||
|
subclassing.
|
||||||
|
|
||||||
|
- Use ``Context.with_resource()`` to manage resources that would
|
||||||
|
normally be used in a ``with`` statement, allowing them to be used
|
||||||
|
across subcommands and callbacks, then cleaned up when the context
|
||||||
|
ends. :pr:`1191`
|
||||||
|
- The result object returned by the test runner's ``invoke()`` method
|
||||||
|
has a ``return_value`` attribute with the value returned by the
|
||||||
|
invoked command. :pr:`1312`
|
||||||
|
- Required arguments with the ``Choice`` type show the choices in
|
||||||
|
curly braces to indicate that one is required (``{a|b|c}``).
|
||||||
|
:issue:`1272`
|
||||||
|
- If only a name is passed to ``option()``, Click suggests renaming it
|
||||||
|
to ``--name``. :pr:`1355`
|
||||||
|
- A context's ``show_default`` parameter defaults to the value from
|
||||||
|
the parent context. :issue:`1565`
|
||||||
|
- ``click.style()`` can output 256 and RGB color codes. Most modern
|
||||||
|
terminals support these codes. :pr:`1429`
|
||||||
|
- When using ``CliRunner.invoke()``, the replaced ``stdin`` file has
|
||||||
|
``name`` and ``mode`` attributes. This lets ``File`` options with
|
||||||
|
the ``-`` value match non-testing behavior. :issue:`1064`
|
||||||
|
- When creating a ``Group``, allow passing a list of commands instead
|
||||||
|
of a dict. :issue:`1339`
|
||||||
|
- When a long option name isn't valid, use ``difflib`` to make better
|
||||||
|
suggestions for possible corrections. :issue:`1446`
|
||||||
|
- Core objects have a ``to_info_dict()`` method. This gathers
|
||||||
|
information about the object's structure that could be useful for a
|
||||||
|
tool generating user-facing documentation. To get the structure of
|
||||||
|
an entire CLI, use ``Context(cli).to_info_dict()``. :issue:`461`
|
||||||
|
- Redesign the shell completion system. :issue:`1484`, :pr:`1622`
|
||||||
|
|
||||||
|
- Support Bash >= 4.4, Zsh, and Fish, with the ability for
|
||||||
|
extensions to add support for other shells.
|
||||||
|
- Allow commands, groups, parameters, and types to override their
|
||||||
|
completions suggestions.
|
||||||
|
- Groups complete the names commands were registered with, which
|
||||||
|
can differ from the name they were created with.
|
||||||
|
- The ``autocompletion`` parameter for options and arguments is
|
||||||
|
renamed to ``shell_complete``. The function must take
|
||||||
|
``ctx, param, incomplete``, must do matching rather than return
|
||||||
|
all values, and must return a list of strings or a list of
|
||||||
|
``CompletionItem``. The old name and behavior is deprecated and
|
||||||
|
will be removed in 8.1.
|
||||||
|
- The env var values used to start completion have changed order.
|
||||||
|
The shell now comes first, such as ``{shell}_source`` rather
|
||||||
|
than ``source_{shell}``, and is always required.
|
||||||
|
|
||||||
|
- Completion correctly parses command line strings with incomplete
|
||||||
|
quoting or escape sequences. :issue:`1708`
|
||||||
|
- Extra context settings (``obj=...``, etc.) are passed on to the
|
||||||
|
completion system. :issue:`942`
|
||||||
|
- Include ``--help`` option in completion. :pr:`1504`
|
||||||
|
- ``ParameterSource`` is an ``enum.Enum`` subclass. :issue:`1530`
|
||||||
|
- Boolean and UUID types strip surrounding space before converting.
|
||||||
|
:issue:`1605`
|
||||||
|
- Adjusted error message from parameter type validation to be more
|
||||||
|
consistent. Quotes are used to distinguish the invalid value.
|
||||||
|
:issue:`1605`
|
||||||
|
- The default value for a parameter with ``nargs`` > 1 and
|
||||||
|
``multiple=True`` must be a list of tuples. :issue:`1649`
|
||||||
|
- When getting the value for a parameter, the default is tried in the
|
||||||
|
same section as other sources to ensure consistent processing.
|
||||||
|
:issue:`1649`
|
||||||
|
- All parameter types accept a value that is already the correct type.
|
||||||
|
:issue:`1649`
|
||||||
|
- For shell completion, an argument is considered incomplete if its
|
||||||
|
value did not come from the command line args. :issue:`1649`
|
||||||
|
- Added ``ParameterSource.PROMPT`` to track parameter values that were
|
||||||
|
prompted for. :issue:`1649`
|
||||||
|
- Options with ``nargs`` > 1 no longer raise an error if a default is
|
||||||
|
not given. Parameters with ``nargs`` > 1 default to ``None``, and
|
||||||
|
parameters with ``multiple=True`` or ``nargs=-1`` default to an
|
||||||
|
empty tuple. :issue:`472`
|
||||||
|
- Handle empty env vars as though the option were not passed. This
|
||||||
|
extends the change introduced in 7.1 to be consistent in more cases.
|
||||||
|
:issue:`1285`
|
||||||
|
- ``Parameter.get_default()`` checks ``Context.default_map`` to
|
||||||
|
handle overrides consistently in help text, ``invoke()``, and
|
||||||
|
prompts. :issue:`1548`
|
||||||
|
- Add ``prompt_required`` param to ``Option``. When set to ``False``,
|
||||||
|
the user will only be prompted for an input if no value was passed.
|
||||||
|
:issue:`736`
|
||||||
|
- Providing the value to an option can be made optional through
|
||||||
|
``is_flag=False``, and the value can instead be prompted for or
|
||||||
|
passed in as a default value.
|
||||||
|
:issue:`549, 736, 764, 921, 1015, 1618`
|
||||||
|
- Fix formatting when ``Command.options_metavar`` is empty. :pr:`1551`
|
||||||
|
- Revert adding space between option help text that wraps.
|
||||||
|
:issue:`1831`
|
||||||
|
- The default value passed to ``prompt`` will be cast to the correct
|
||||||
|
type like an input value would be. :pr:`1517`
|
||||||
|
- Automatically generated short help messages will stop at the first
|
||||||
|
ending of a phrase or double linebreak. :issue:`1082`
|
||||||
|
- Skip progress bar render steps for efficiency with very fast
|
||||||
|
iterators by setting ``update_min_steps``. :issue:`676`
|
||||||
|
- Respect ``case_sensitive=False`` when doing shell completion for
|
||||||
|
``Choice`` :issue:`1692`
|
||||||
|
- Use ``mkstemp()`` instead of ``mktemp()`` in pager implementation.
|
||||||
|
:issue:`1752`
|
||||||
|
- If ``Option.show_default`` is a string, it is displayed even if
|
||||||
|
``default`` is ``None``. :issue:`1732`
|
||||||
|
- ``click.get_terminal_size()`` is deprecated and will be removed in
|
||||||
|
8.1. Use :func:`shutil.get_terminal_size` instead. :issue:`1736`
|
||||||
|
- Control the location of the temporary directory created by
|
||||||
|
``CLIRunner.isolated_filesystem`` by passing ``temp_dir``. A custom
|
||||||
|
directory will not be removed automatically. :issue:`395`
|
||||||
|
- ``click.confirm()`` will prompt until input is given if called with
|
||||||
|
``default=None``. :issue:`1381`
|
||||||
|
- Option prompts validate the value with the option's callback in
|
||||||
|
addition to its type. :issue:`457`
|
||||||
|
- ``confirmation_prompt`` can be set to a custom string. :issue:`723`
|
||||||
|
- Allow styled output in Jupyter on Windows. :issue:`1271`
|
||||||
|
- ``style()`` supports the ``strikethrough``, ``italic``, and
|
||||||
|
``overline`` styles. :issue:`805, 1821`
|
||||||
|
- Multiline marker is removed from short help text. :issue:`1597`
|
||||||
|
- Restore progress bar behavior of echoing only the label if the file
|
||||||
|
is not a TTY. :issue:`1138`
|
||||||
|
- Progress bar output is shown even if execution time is less than 0.5
|
||||||
|
seconds. :issue:`1648`
|
||||||
|
- Progress bar ``item_show_func`` shows the current item, not the
|
||||||
|
previous item. :issue:`1353`
|
||||||
|
- The ``Path`` param type can be passed ``path_type=pathlib.Path`` to
|
||||||
|
return a path object instead of a string. :issue:`405`
|
||||||
|
- ``TypeError`` is raised when parameter with ``multiple=True`` or
|
||||||
|
``nargs > 1`` has non-iterable default. :issue:`1749`
|
||||||
|
- Add a ``pass_meta_key`` decorator for passing a key from
|
||||||
|
``Context.meta``. This is useful for extensions using ``meta`` to
|
||||||
|
store information. :issue:`1739`
|
||||||
|
- ``Path`` ``resolve_path`` resolves symlinks on Windows Python < 3.8.
|
||||||
|
:issue:`1813`
|
||||||
|
- Command deprecation notice appears at the start of the help text, as
|
||||||
|
well as in the short help. The notice is not in all caps.
|
||||||
|
:issue:`1791`
|
||||||
|
- When taking arguments from ``sys.argv`` on Windows, glob patterns,
|
||||||
|
user dir, and env vars are expanded. :issue:`1096`
|
||||||
|
- Marked messages shown by the CLI with ``gettext()`` to allow
|
||||||
|
applications to translate Click's built-in strings. :issue:`303`
|
||||||
|
- Writing invalid characters to ``stderr`` when using the test runner
|
||||||
|
does not raise a ``UnicodeEncodeError``. :issue:`848`
|
||||||
|
- Fix an issue where ``readline`` would clear the entire ``prompt()``
|
||||||
|
line instead of only the input when pressing backspace. :issue:`665`
|
||||||
|
- Add all kwargs passed to ``Context.invoke()`` to ``ctx.params``.
|
||||||
|
Fixes an inconsistency when nesting ``Context.forward()`` calls.
|
||||||
|
:issue:`1568`
|
||||||
|
- The ``MultiCommand.resultcallback`` decorator is renamed to
|
||||||
|
``result_callback``. The old name is deprecated. :issue:`1160`
|
||||||
|
- Fix issues with ``CliRunner`` output when using ``echo_stdin=True``.
|
||||||
|
:issue:`1101`
|
||||||
|
- Fix a bug of ``click.utils.make_default_short_help`` for which the
|
||||||
|
returned string could be as long as ``max_width + 3``. :issue:`1849`
|
||||||
|
- When defining a parameter, ``default`` is validated with
|
||||||
|
``multiple`` and ``nargs``. More validation is done for values being
|
||||||
|
processed as well. :issue:`1806`
|
||||||
|
- ``HelpFormatter.write_text`` uses the full line width when wrapping
|
||||||
|
text. :issue:`1871`
|
||||||
|
|
||||||
|
|
||||||
Version 7.1.2
|
Version 7.1.2
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
@ -46,7 +328,7 @@ Released 2020-03-09
|
||||||
:issue:`1277`, :pr:`1318`
|
:issue:`1277`, :pr:`1318`
|
||||||
- Add ``no_args_is_help`` option to ``click.Command``, defaults to
|
- Add ``no_args_is_help`` option to ``click.Command``, defaults to
|
||||||
False :pr:`1167`
|
False :pr:`1167`
|
||||||
- Add ``show_defaults`` parameter to ``Context`` to enable showing
|
- Add ``show_default`` parameter to ``Context`` to enable showing
|
||||||
defaults globally. :issue:`1018`
|
defaults globally. :issue:`1018`
|
||||||
- Handle ``env MYPATH=''`` as though the option were not passed.
|
- Handle ``env MYPATH=''`` as though the option were not passed.
|
||||||
:issue:`1196`
|
:issue:`1196`
|
||||||
|
@ -90,6 +372,8 @@ Released 2020-03-09
|
||||||
- Make the warning about old 2-arg parameter callbacks a deprecation
|
- Make the warning about old 2-arg parameter callbacks a deprecation
|
||||||
warning, to be removed in 8.0. This has been a warning since Click
|
warning, to be removed in 8.0. This has been a warning since Click
|
||||||
2.0. :pr:`1492`
|
2.0. :pr:`1492`
|
||||||
|
- Adjust error messages to standardize the types of quotes used so
|
||||||
|
they match error messages from Python.
|
||||||
|
|
||||||
|
|
||||||
Version 7.0
|
Version 7.0
|
||||||
|
@ -480,15 +764,16 @@ Version 3.0
|
||||||
|
|
||||||
Released 2014-08-12, codename "clonk clonk"
|
Released 2014-08-12, codename "clonk clonk"
|
||||||
|
|
||||||
- Formatter now no longer attempts to accomodate for terminals smaller
|
- Formatter now no longer attempts to accommodate for terminals
|
||||||
than 50 characters. If that happens it just assumes a minimal width.
|
smaller than 50 characters. If that happens it just assumes a
|
||||||
|
minimal width.
|
||||||
- Added a way to not swallow exceptions in the test system.
|
- Added a way to not swallow exceptions in the test system.
|
||||||
- Added better support for colors with pagers and ways to override the
|
- Added better support for colors with pagers and ways to override the
|
||||||
autodetection.
|
autodetection.
|
||||||
- The CLI runner's result object now has a traceback attached.
|
- The CLI runner's result object now has a traceback attached.
|
||||||
- Improved automatic short help detection to work better with dots
|
- Improved automatic short help detection to work better with dots
|
||||||
that do not terminate sentences.
|
that do not terminate sentences.
|
||||||
- When definining options without actual valid option strings now,
|
- When defining options without actual valid option strings now,
|
||||||
Click will give an error message instead of silently passing. This
|
Click will give an error message instead of silently passing. This
|
||||||
should catch situations where users wanted to created arguments
|
should catch situations where users wanted to created arguments
|
||||||
instead of options.
|
instead of options.
|
||||||
|
|
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at report@palletsprojects.com. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
222
CONTRIBUTING.rst
Normal file
222
CONTRIBUTING.rst
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
How to contribute to Click
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Thank you for considering contributing to Click!
|
||||||
|
|
||||||
|
|
||||||
|
Support questions
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Please don't use the issue tracker for this. The issue tracker is a tool
|
||||||
|
to address bugs and feature requests in Click itself. Use one of the
|
||||||
|
following resources for questions about using Click or issues with your
|
||||||
|
own code:
|
||||||
|
|
||||||
|
- The ``#get-help`` channel on our Discord chat:
|
||||||
|
https://discord.gg/pallets
|
||||||
|
- The mailing list flask@python.org for long term discussion or larger
|
||||||
|
issues.
|
||||||
|
- Ask on `Stack Overflow`_. Search with Google first using:
|
||||||
|
``site:stackoverflow.com python click {search term, exception message, etc.}``
|
||||||
|
|
||||||
|
.. _Stack Overflow: https://stackoverflow.com/questions/tagged/python-click?tab=Frequent
|
||||||
|
|
||||||
|
|
||||||
|
Reporting issues
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Include the following information in your post:
|
||||||
|
|
||||||
|
- Describe what you expected to happen.
|
||||||
|
- If possible, include a `minimal reproducible example`_ to help us
|
||||||
|
identify the issue. This also helps check that the issue is not with
|
||||||
|
your own code.
|
||||||
|
- Describe what actually happened. Include the full traceback if there
|
||||||
|
was an exception.
|
||||||
|
- List your Python and Click versions. If possible, check if this
|
||||||
|
issue is already fixed in the latest releases or the latest code in
|
||||||
|
the repository.
|
||||||
|
|
||||||
|
.. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example
|
||||||
|
|
||||||
|
|
||||||
|
Submitting patches
|
||||||
|
------------------
|
||||||
|
|
||||||
|
If there is not an open issue for what you want to submit, prefer
|
||||||
|
opening one for discussion before working on a PR. You can work on any
|
||||||
|
issue that doesn't have an open PR linked to it or a maintainer assigned
|
||||||
|
to it. These show up in the sidebar. No need to ask if you can work on
|
||||||
|
an issue that interests you.
|
||||||
|
|
||||||
|
Include the following in your patch:
|
||||||
|
|
||||||
|
- Use `Black`_ to format your code. This and other tools will run
|
||||||
|
automatically if you install `pre-commit`_ using the instructions
|
||||||
|
below.
|
||||||
|
- Include tests if your patch adds or changes code. Make sure the test
|
||||||
|
fails without your patch.
|
||||||
|
- Update any relevant docs pages and docstrings. Docs pages and
|
||||||
|
docstrings should be wrapped at 72 characters.
|
||||||
|
- Add an entry in ``CHANGES.rst``. Use the same style as other
|
||||||
|
entries. Also include ``.. versionchanged::`` inline changelogs in
|
||||||
|
relevant docstrings.
|
||||||
|
|
||||||
|
.. _Black: https://black.readthedocs.io
|
||||||
|
.. _pre-commit: https://pre-commit.com
|
||||||
|
|
||||||
|
|
||||||
|
First time setup
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- Download and install the `latest version of git`_.
|
||||||
|
- Configure git with your `username`_ and `email`_.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ git config --global user.name 'your name'
|
||||||
|
$ git config --global user.email 'your email'
|
||||||
|
|
||||||
|
- Make sure you have a `GitHub account`_.
|
||||||
|
- Fork Click to your GitHub account by clicking the `Fork`_ button.
|
||||||
|
- `Clone`_ the main repository locally.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ git clone https://github.com/pallets/click
|
||||||
|
$ cd click
|
||||||
|
|
||||||
|
- Add your fork as a remote to push your work to. Replace
|
||||||
|
``{username}`` with your username. This names the remote "fork", the
|
||||||
|
default Pallets remote is "origin".
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ git remote add fork https://github.com/{username}/click
|
||||||
|
|
||||||
|
- Create a virtualenv.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ python3 -m venv env
|
||||||
|
$ . env/bin/activate
|
||||||
|
|
||||||
|
On Windows, activating is different.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
> env\Scripts\activate
|
||||||
|
|
||||||
|
- Upgrade pip and setuptools.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ python -m pip install --upgrade pip setuptools
|
||||||
|
|
||||||
|
- Install the development dependencies, then install Click in
|
||||||
|
editable mode.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ pip install -r requirements/dev.txt && pip install -e .
|
||||||
|
|
||||||
|
- Install the pre-commit hooks.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ pre-commit install
|
||||||
|
|
||||||
|
.. _latest version of git: https://git-scm.com/downloads
|
||||||
|
.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git
|
||||||
|
.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address
|
||||||
|
.. _GitHub account: https://github.com/join
|
||||||
|
.. _Fork: https://github.com/pallets/click/fork
|
||||||
|
.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork
|
||||||
|
|
||||||
|
|
||||||
|
Start coding
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- Create a branch to identify the issue you would like to work on. If
|
||||||
|
you're submitting a bug or documentation fix, branch off of the
|
||||||
|
latest ".x" branch.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ git fetch origin
|
||||||
|
$ git checkout -b your-branch-name origin/8.0.x
|
||||||
|
|
||||||
|
If you're submitting a feature addition or change, branch off of the
|
||||||
|
"main" branch.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ git fetch origin
|
||||||
|
$ git checkout -b your-branch-name origin/main
|
||||||
|
|
||||||
|
- Using your favorite editor, make your changes,
|
||||||
|
`committing as you go`_.
|
||||||
|
- Include tests that cover any code changes you make. Make sure the
|
||||||
|
test fails without your patch. Run the tests as described below.
|
||||||
|
- Push your commits to your fork on GitHub and
|
||||||
|
`create a pull request`_. Link to the issue being addressed with
|
||||||
|
``fixes #123`` in the pull request.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ git push --set-upstream fork your-branch-name
|
||||||
|
|
||||||
|
.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes
|
||||||
|
.. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
|
||||||
|
|
||||||
|
|
||||||
|
Running the tests
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Run the basic test suite with pytest.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ pytest
|
||||||
|
|
||||||
|
This runs the tests for the current environment, which is usually
|
||||||
|
sufficient. CI will run the full suite when you submit your pull
|
||||||
|
request. You can run the full test suite with tox if you don't want to
|
||||||
|
wait.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ tox
|
||||||
|
|
||||||
|
|
||||||
|
Running test coverage
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Generating a report of lines that do not have test coverage can indicate
|
||||||
|
where to start contributing. Run ``pytest`` using ``coverage`` and
|
||||||
|
generate a report.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ pip install coverage
|
||||||
|
$ coverage run -m pytest
|
||||||
|
$ coverage html
|
||||||
|
|
||||||
|
Open ``htmlcov/index.html`` in your browser to explore the report.
|
||||||
|
|
||||||
|
Read more about `coverage <https://coverage.readthedocs.io>`__.
|
||||||
|
|
||||||
|
|
||||||
|
Building the docs
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Build the docs in the ``docs`` directory using Sphinx.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ cd docs
|
||||||
|
$ make html
|
||||||
|
|
||||||
|
Open ``_build/html/index.html`` in your browser to view the docs.
|
||||||
|
|
||||||
|
Read more about `Sphinx <https://www.sphinx-doc.org/en/stable/>`__.
|
|
@ -1,8 +1,10 @@
|
||||||
include CHANGES.rst
|
include CHANGES.rst
|
||||||
include tox.ini
|
include tox.ini
|
||||||
|
include requirements/*.txt
|
||||||
graft artwork
|
graft artwork
|
||||||
graft docs
|
graft docs
|
||||||
prune docs/_build
|
prune docs/_build
|
||||||
graft examples
|
graft examples
|
||||||
graft tests
|
graft tests
|
||||||
|
include src/click/py.typed
|
||||||
global-exclude *.pyc
|
global-exclude *.pyc
|
||||||
|
|
100
PKG-INFO
100
PKG-INFO
|
@ -1,100 +0,0 @@
|
||||||
Metadata-Version: 1.2
|
|
||||||
Name: click
|
|
||||||
Version: 7.1.2
|
|
||||||
Summary: Composable command line interface toolkit
|
|
||||||
Home-page: https://palletsprojects.com/p/click/
|
|
||||||
Maintainer: Pallets
|
|
||||||
Maintainer-email: contact@palletsprojects.com
|
|
||||||
License: BSD-3-Clause
|
|
||||||
Project-URL: Documentation, https://click.palletsprojects.com/
|
|
||||||
Project-URL: Code, https://github.com/pallets/click
|
|
||||||
Project-URL: Issue tracker, https://github.com/pallets/click/issues
|
|
||||||
Description: \$ click\_
|
|
||||||
==========
|
|
||||||
|
|
||||||
Click is a Python package for creating beautiful command line interfaces
|
|
||||||
in a composable way with as little code as necessary. It's the "Command
|
|
||||||
Line Interface Creation Kit". It's highly configurable but comes with
|
|
||||||
sensible defaults out of the box.
|
|
||||||
|
|
||||||
It aims to make the process of writing command line tools quick and fun
|
|
||||||
while also preventing any frustration caused by the inability to
|
|
||||||
implement an intended CLI API.
|
|
||||||
|
|
||||||
Click in three points:
|
|
||||||
|
|
||||||
- Arbitrary nesting of commands
|
|
||||||
- Automatic help page generation
|
|
||||||
- Supports lazy loading of subcommands at runtime
|
|
||||||
|
|
||||||
|
|
||||||
Installing
|
|
||||||
----------
|
|
||||||
|
|
||||||
Install and update using `pip`_:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
$ pip install -U click
|
|
||||||
|
|
||||||
.. _pip: https://pip.pypa.io/en/stable/quickstart/
|
|
||||||
|
|
||||||
|
|
||||||
A Simple Example
|
|
||||||
----------------
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import click
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option("--count", default=1, help="Number of greetings.")
|
|
||||||
@click.option("--name", prompt="Your name", help="The person to greet.")
|
|
||||||
def hello(count, name):
|
|
||||||
"""Simple program that greets NAME for a total of COUNT times."""
|
|
||||||
for _ in range(count):
|
|
||||||
click.echo(f"Hello, {name}!")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
hello()
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
$ python hello.py --count=3
|
|
||||||
Your name: Click
|
|
||||||
Hello, Click!
|
|
||||||
Hello, Click!
|
|
||||||
Hello, Click!
|
|
||||||
|
|
||||||
|
|
||||||
Donate
|
|
||||||
------
|
|
||||||
|
|
||||||
The Pallets organization develops and supports Click and other popular
|
|
||||||
packages. In order to grow the community of contributors and users, and
|
|
||||||
allow the maintainers to devote more time to the projects, `please
|
|
||||||
donate today`_.
|
|
||||||
|
|
||||||
.. _please donate today: https://palletsprojects.com/donate
|
|
||||||
|
|
||||||
|
|
||||||
Links
|
|
||||||
-----
|
|
||||||
|
|
||||||
- Website: https://palletsprojects.com/p/click/
|
|
||||||
- Documentation: https://click.palletsprojects.com/
|
|
||||||
- Releases: https://pypi.org/project/click/
|
|
||||||
- Code: https://github.com/pallets/click
|
|
||||||
- Issue tracker: https://github.com/pallets/click/issues
|
|
||||||
- Test status: https://dev.azure.com/pallets/click/_build
|
|
||||||
- Official chat: https://discord.gg/t6rrQZH
|
|
||||||
|
|
||||||
Platform: UNKNOWN
|
|
||||||
Classifier: Development Status :: 5 - Production/Stable
|
|
||||||
Classifier: Intended Audience :: Developers
|
|
||||||
Classifier: License :: OSI Approved :: BSD License
|
|
||||||
Classifier: Operating System :: OS Independent
|
|
||||||
Classifier: Programming Language :: Python
|
|
||||||
Classifier: Programming Language :: Python :: 2
|
|
||||||
Classifier: Programming Language :: Python :: 3
|
|
||||||
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
|
|
15
README.rst
15
README.rst
|
@ -26,7 +26,7 @@ Install and update using `pip`_:
|
||||||
|
|
||||||
$ pip install -U click
|
$ pip install -U click
|
||||||
|
|
||||||
.. _pip: https://pip.pypa.io/en/stable/quickstart/
|
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||||
|
|
||||||
|
|
||||||
A Simple Example
|
A Simple Example
|
||||||
|
@ -70,10 +70,11 @@ donate today`_.
|
||||||
Links
|
Links
|
||||||
-----
|
-----
|
||||||
|
|
||||||
- Website: https://palletsprojects.com/p/click/
|
|
||||||
- Documentation: https://click.palletsprojects.com/
|
- Documentation: https://click.palletsprojects.com/
|
||||||
- Releases: https://pypi.org/project/click/
|
- Changes: https://click.palletsprojects.com/changes/
|
||||||
- Code: https://github.com/pallets/click
|
- PyPI Releases: https://pypi.org/project/click/
|
||||||
- Issue tracker: https://github.com/pallets/click/issues
|
- Source Code: https://github.com/pallets/click
|
||||||
- Test status: https://dev.azure.com/pallets/click/_build
|
- Issue Tracker: https://github.com/pallets/click/issues
|
||||||
- Official chat: https://discord.gg/t6rrQZH
|
- Website: https://palletsprojects.com/p/click
|
||||||
|
- Twitter: https://twitter.com/PalletsTeam
|
||||||
|
- Chat: https://discord.gg/pallets
|
||||||
|
|
BIN
docs/.DS_Store
vendored
Normal file
BIN
docs/.DS_Store
vendored
Normal file
Binary file not shown.
|
@ -13,7 +13,7 @@ Command Aliases
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
Many tools support aliases for commands (see `Command alias example
|
Many tools support aliases for commands (see `Command alias example
|
||||||
<https://github.com/pallets/click/tree/master/examples/aliases>`_).
|
<https://github.com/pallets/click/tree/main/examples/aliases>`_).
|
||||||
For instance, you can configure ``git`` to accept ``git ci`` as alias for
|
For instance, you can configure ``git`` to accept ``git ci`` as alias for
|
||||||
``git commit``. Other tools also support auto-discovery for aliases by
|
``git commit``. Other tools also support auto-discovery for aliases by
|
||||||
automatically shortening them.
|
automatically shortening them.
|
||||||
|
@ -35,7 +35,6 @@ it would accept ``pus`` as an alias (so long as it was unique):
|
||||||
.. click:example::
|
.. click:example::
|
||||||
|
|
||||||
class AliasedGroup(click.Group):
|
class AliasedGroup(click.Group):
|
||||||
|
|
||||||
def get_command(self, ctx, cmd_name):
|
def get_command(self, ctx, cmd_name):
|
||||||
rv = click.Group.get_command(self, ctx, cmd_name)
|
rv = click.Group.get_command(self, ctx, cmd_name)
|
||||||
if rv is not None:
|
if rv is not None:
|
||||||
|
@ -46,7 +45,12 @@ it would accept ``pus`` as an alias (so long as it was unique):
|
||||||
return None
|
return None
|
||||||
elif len(matches) == 1:
|
elif len(matches) == 1:
|
||||||
return click.Group.get_command(self, ctx, matches[0])
|
return click.Group.get_command(self, ctx, matches[0])
|
||||||
ctx.fail('Too many matches: %s' % ', '.join(sorted(matches)))
|
ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")
|
||||||
|
|
||||||
|
def resolve_command(self, ctx, args):
|
||||||
|
# always return the full command name
|
||||||
|
_, cmd, args = super().resolve_command(ctx, args)
|
||||||
|
return cmd.name, cmd, args
|
||||||
|
|
||||||
And it can then be used like this:
|
And it can then be used like this:
|
||||||
|
|
||||||
|
@ -92,7 +96,7 @@ it's good to know that the system works this way.
|
||||||
@click.option('--url', callback=open_url)
|
@click.option('--url', callback=open_url)
|
||||||
def cli(url, fp=None):
|
def cli(url, fp=None):
|
||||||
if fp is not None:
|
if fp is not None:
|
||||||
click.echo('%s: %s' % (url, fp.code))
|
click.echo(f"{url}: {fp.code}")
|
||||||
|
|
||||||
In this case the callback returns the URL unchanged but also passes a
|
In this case the callback returns the URL unchanged but also passes a
|
||||||
second ``fp`` value to the callback. What's more recommended is to pass
|
second ``fp`` value to the callback. What's more recommended is to pass
|
||||||
|
@ -116,7 +120,7 @@ the information in a wrapper however:
|
||||||
@click.option('--url', callback=open_url)
|
@click.option('--url', callback=open_url)
|
||||||
def cli(url):
|
def cli(url):
|
||||||
if url is not None:
|
if url is not None:
|
||||||
click.echo('%s: %s' % (url.url, url.fp.code))
|
click.echo(f"{url.url}: {url.fp.code}")
|
||||||
|
|
||||||
|
|
||||||
Token Normalization
|
Token Normalization
|
||||||
|
@ -140,7 +144,7 @@ function that converts the token to lowercase:
|
||||||
@click.command(context_settings=CONTEXT_SETTINGS)
|
@click.command(context_settings=CONTEXT_SETTINGS)
|
||||||
@click.option('--name', default='Pete')
|
@click.option('--name', default='Pete')
|
||||||
def cli(name):
|
def cli(name):
|
||||||
click.echo('Name: %s' % name)
|
click.echo(f"Name: {name}")
|
||||||
|
|
||||||
And how it works on the command line:
|
And how it works on the command line:
|
||||||
|
|
||||||
|
@ -171,7 +175,7 @@ Example:
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--count', default=1)
|
@click.option('--count', default=1)
|
||||||
def test(count):
|
def test(count):
|
||||||
click.echo('Count: %d' % count)
|
click.echo(f'Count: {count}')
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--count', default=1)
|
@click.option('--count', default=1)
|
||||||
|
@ -300,7 +304,7 @@ In the end you end up with something like this:
|
||||||
"""A fake wrapper around Python's timeit."""
|
"""A fake wrapper around Python's timeit."""
|
||||||
cmdline = ['echo', 'python', '-mtimeit'] + list(timeit_args)
|
cmdline = ['echo', 'python', '-mtimeit'] + list(timeit_args)
|
||||||
if verbose:
|
if verbose:
|
||||||
click.echo('Invoking: %s' % ' '.join(cmdline))
|
click.echo(f"Invoking: {' '.join(cmdline)}")
|
||||||
call(cmdline)
|
call(cmdline)
|
||||||
|
|
||||||
And what it looks like:
|
And what it looks like:
|
||||||
|
@ -379,3 +383,106 @@ do. However if you do use this for threading you need to be very careful
|
||||||
as the vast majority of the context is not thread safe! You are only
|
as the vast majority of the context is not thread safe! You are only
|
||||||
allowed to read from the context, but not to perform any modifications on
|
allowed to read from the context, but not to perform any modifications on
|
||||||
it.
|
it.
|
||||||
|
|
||||||
|
|
||||||
|
Detecting the Source of a Parameter
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
In some situations it's helpful to understand whether or not an option
|
||||||
|
or parameter came from the command line, the environment, the default
|
||||||
|
value, or :attr:`Context.default_map`. The
|
||||||
|
:meth:`Context.get_parameter_source` method can be used to find this
|
||||||
|
out. It will return a member of the :class:`~click.core.ParameterSource`
|
||||||
|
enum.
|
||||||
|
|
||||||
|
.. click:example::
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument('port', nargs=1, default=8080, envvar="PORT")
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, port):
|
||||||
|
source = ctx.get_parameter_source("port")
|
||||||
|
click.echo(f"Port came from {source.name}")
|
||||||
|
|
||||||
|
.. click:run::
|
||||||
|
|
||||||
|
invoke(cli, prog_name='cli', args=['8080'])
|
||||||
|
println()
|
||||||
|
invoke(cli, prog_name='cli', args=[], env={"PORT": "8080"})
|
||||||
|
println()
|
||||||
|
invoke(cli, prog_name='cli', args=[])
|
||||||
|
println()
|
||||||
|
|
||||||
|
|
||||||
|
Managing Resources
|
||||||
|
------------------
|
||||||
|
|
||||||
|
It can be useful to open a resource in a group, to be made available to
|
||||||
|
subcommands. Many types of resources need to be closed or otherwise
|
||||||
|
cleaned up after use. The standard way to do this in Python is by using
|
||||||
|
a context manager with the ``with`` statement.
|
||||||
|
|
||||||
|
For example, the ``Repo`` class from :doc:`complex` might actually be
|
||||||
|
defined as a context manager:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class Repo:
|
||||||
|
def __init__(self, home=None):
|
||||||
|
self.home = os.path.abspath(home or ".")
|
||||||
|
self.db = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
path = os.path.join(self.home, "repo.db")
|
||||||
|
self.db = open_database(path)
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, tb):
|
||||||
|
self.db.close()
|
||||||
|
|
||||||
|
Ordinarily, it would be used with the ``with`` statement:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
with Repo() as repo:
|
||||||
|
repo.db.query(...)
|
||||||
|
|
||||||
|
However, a ``with`` block in a group would exit and close the database
|
||||||
|
before it could be used by a subcommand.
|
||||||
|
|
||||||
|
Instead, use the context's :meth:`~click.Context.with_resource` method
|
||||||
|
to enter the context manager and return the resource. When the group and
|
||||||
|
any subcommands finish, the context's resources are cleaned up.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option("--repo-home", default=".repo")
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, repo_home):
|
||||||
|
ctx.obj = ctx.with_resource(Repo(repo_home))
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_obj
|
||||||
|
def log(obj):
|
||||||
|
# obj is the repo opened in the cli group
|
||||||
|
for entry in obj.db.query(...):
|
||||||
|
click.echo(entry)
|
||||||
|
|
||||||
|
If the resource isn't a context manager, usually it can be wrapped in
|
||||||
|
one using something from :mod:`contextlib`. If that's not possible, use
|
||||||
|
the context's :meth:`~click.Context.call_on_close` method to register a
|
||||||
|
cleanup function.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option("--name", default="repo.db")
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, repo_home):
|
||||||
|
ctx.obj = db = open_db(repo_home)
|
||||||
|
|
||||||
|
@ctx.call_on_close
|
||||||
|
def close_db():
|
||||||
|
db.record_use()
|
||||||
|
db.save()
|
||||||
|
db.close()
|
||||||
|
|
30
docs/api.rst
30
docs/api.rst
|
@ -31,6 +31,9 @@ Decorators
|
||||||
|
|
||||||
.. autofunction:: make_pass_decorator
|
.. autofunction:: make_pass_decorator
|
||||||
|
|
||||||
|
.. autofunction:: click.decorators.pass_meta_key
|
||||||
|
|
||||||
|
|
||||||
Utilities
|
Utilities
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
@ -108,6 +111,11 @@ Context
|
||||||
|
|
||||||
.. autofunction:: get_current_context
|
.. autofunction:: get_current_context
|
||||||
|
|
||||||
|
.. autoclass:: click.core.ParameterSource
|
||||||
|
:members:
|
||||||
|
:member-order: bysource
|
||||||
|
|
||||||
|
|
||||||
Types
|
Types
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -131,6 +139,10 @@ Types
|
||||||
|
|
||||||
.. autoclass:: IntRange
|
.. autoclass:: IntRange
|
||||||
|
|
||||||
|
.. autoclass:: FloatRange
|
||||||
|
|
||||||
|
.. autoclass:: DateTime
|
||||||
|
|
||||||
.. autoclass:: Tuple
|
.. autoclass:: Tuple
|
||||||
|
|
||||||
.. autoclass:: ParamType
|
.. autoclass:: ParamType
|
||||||
|
@ -169,6 +181,24 @@ Parsing
|
||||||
.. autoclass:: OptionParser
|
.. autoclass:: OptionParser
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
Shell Completion
|
||||||
|
----------------
|
||||||
|
|
||||||
|
See :doc:`/shell-completion` for information about enabling and
|
||||||
|
customizing Click's shell completion system.
|
||||||
|
|
||||||
|
.. currentmodule:: click.shell_completion
|
||||||
|
|
||||||
|
.. autoclass:: CompletionItem
|
||||||
|
|
||||||
|
.. autoclass:: ShellComplete
|
||||||
|
:members:
|
||||||
|
:member-order: bysource
|
||||||
|
|
||||||
|
.. autofunction:: add_completion_class
|
||||||
|
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ Example:
|
||||||
def copy(src, dst):
|
def copy(src, dst):
|
||||||
"""Move file SRC to DST."""
|
"""Move file SRC to DST."""
|
||||||
for fn in src:
|
for fn in src:
|
||||||
click.echo('move %s to folder %s' % (fn, dst))
|
click.echo(f"move {fn} to folder {dst}")
|
||||||
|
|
||||||
And what it looks like:
|
And what it looks like:
|
||||||
|
|
||||||
|
|
|
@ -1,163 +0,0 @@
|
||||||
Shell Completion
|
|
||||||
================
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
|
|
||||||
Click can provide tab completion for commands, options, and choice
|
|
||||||
values. Bash, Zsh, and Fish are supported
|
|
||||||
|
|
||||||
Completion is only available if a script is installed and invoked
|
|
||||||
through an entry point, not through the ``python`` command. See
|
|
||||||
:ref:`setuptools-integration`.
|
|
||||||
|
|
||||||
|
|
||||||
What it Completes
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
Generally, the shell completion support will complete commands,
|
|
||||||
options, and any option or argument values where the type is
|
|
||||||
:class:`click.Choice`. Options are only listed if at least a dash has
|
|
||||||
been entered.
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
$ repo <TAB><TAB>
|
|
||||||
clone commit copy delete setuser
|
|
||||||
$ repo clone -<TAB><TAB>
|
|
||||||
--deep --help --rev --shallow -r
|
|
||||||
|
|
||||||
Custom completions can be provided for argument and option values by
|
|
||||||
providing an ``autocompletion`` function that returns a list of strings.
|
|
||||||
This is useful when the suggestions need to be dynamically generated
|
|
||||||
completion time. The callback function will be passed 3 keyword
|
|
||||||
arguments:
|
|
||||||
|
|
||||||
- ``ctx`` - The current command context.
|
|
||||||
- ``args`` - The list of arguments passed in.
|
|
||||||
- ``incomplete`` - The partial word that is being completed. May
|
|
||||||
be an empty string if no characters have been entered yet.
|
|
||||||
|
|
||||||
Here is an example of using a callback function to generate dynamic
|
|
||||||
suggestions:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
def get_env_vars(ctx, args, incomplete):
|
|
||||||
return [k for k in os.environ.keys() if incomplete in k]
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.argument("envvar", type=click.STRING, autocompletion=get_env_vars)
|
|
||||||
def cmd1(envvar):
|
|
||||||
click.echo('Environment variable: %s' % envvar)
|
|
||||||
click.echo('Value: %s' % os.environ[envvar])
|
|
||||||
|
|
||||||
|
|
||||||
Completion help strings
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
ZSH and fish support showing documentation strings for completions.
|
|
||||||
These are taken from the help parameters of options and subcommands. For
|
|
||||||
dynamically generated completions a help string can be provided by
|
|
||||||
returning a tuple instead of a string. The first element of the tuple is
|
|
||||||
the completion and the second is the help string to display.
|
|
||||||
|
|
||||||
Here is an example of using a callback function to generate dynamic
|
|
||||||
suggestions with help strings:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
def get_colors(ctx, args, incomplete):
|
|
||||||
colors = [('red', 'a warm color'),
|
|
||||||
('blue', 'a cool color'),
|
|
||||||
('green', 'the other starter color')]
|
|
||||||
return [c for c in colors if incomplete in c[0]]
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.argument("color", type=click.STRING, autocompletion=get_colors)
|
|
||||||
def cmd1(color):
|
|
||||||
click.echo('Chosen color is %s' % color)
|
|
||||||
|
|
||||||
|
|
||||||
Activation
|
|
||||||
----------
|
|
||||||
|
|
||||||
In order to activate shell completion, you need to inform your shell
|
|
||||||
that completion is available for your script. Any Click application
|
|
||||||
automatically provides support for that. If the program is executed with
|
|
||||||
a special ``_<PROG_NAME>_COMPLETE`` variable, the completion mechanism
|
|
||||||
is triggered instead of the normal command. ``<PROG_NAME>`` is the
|
|
||||||
executable name in uppercase with dashes replaced by underscores.
|
|
||||||
|
|
||||||
If your tool is called ``foo-bar``, then the variable is called
|
|
||||||
``_FOO_BAR_COMPLETE``. By exporting it with the ``source_{shell}``
|
|
||||||
value it will output the activation script to evaluate.
|
|
||||||
|
|
||||||
Here are examples for a ``foo-bar`` script.
|
|
||||||
|
|
||||||
For Bash, add this to ``~/.bashrc``:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
eval "$(_FOO_BAR_COMPLETE=source_bash foo-bar)"
|
|
||||||
|
|
||||||
For Zsh, add this to ``~/.zshrc``:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
eval "$(_FOO_BAR_COMPLETE=source_zsh foo-bar)"
|
|
||||||
|
|
||||||
For Fish, add this to ``~/.config/fish/completions/foo-bar.fish``:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
eval (env _FOO_BAR_COMPLETE=source_fish foo-bar)
|
|
||||||
|
|
||||||
Open a new shell to enable completion. Or run the ``eval`` command
|
|
||||||
directly in your current shell to enable it temporarily.
|
|
||||||
|
|
||||||
|
|
||||||
Activation Script
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
The above ``eval`` examples will invoke your application every time a
|
|
||||||
shell is started. This may slow down shell startup time significantly.
|
|
||||||
|
|
||||||
Alternatively, export the generated completion code as a static script
|
|
||||||
to be executed. You can ship this file with your builds; tools like Git
|
|
||||||
do this. At least Zsh will also cache the results of completion files,
|
|
||||||
but not ``eval`` scripts.
|
|
||||||
|
|
||||||
For Bash:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
_FOO_BAR_COMPLETE=source_bash foo-bar > foo-bar-complete.sh
|
|
||||||
|
|
||||||
For Zsh:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
_FOO_BAR_COMPLETE=source_zsh foo-bar > foo-bar-complete.sh
|
|
||||||
|
|
||||||
For Fish:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
_FOO_BAR_COMPLETE=source_zsh foo-bar > foo-bar-complete.sh
|
|
||||||
|
|
||||||
In ``.bashrc`` or ``.zshrc``, source the script instead of the ``eval``
|
|
||||||
command:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
. /path/to/foo-bar-complete.sh
|
|
||||||
|
|
||||||
For Fish, add the file to the completions directory:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
_FOO_BAR_COMPLETE=source_fish foo-bar > ~/.config/fish/completions/foo-bar-complete.fish
|
|
|
@ -25,7 +25,7 @@ when an inner command runs:
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.option('--debug/--no-debug', default=False)
|
@click.option('--debug/--no-debug', default=False)
|
||||||
def cli(debug):
|
def cli(debug):
|
||||||
click.echo('Debug mode is %s' % ('on' if debug else 'off'))
|
click.echo(f"Debug mode is {'on' if debug else 'off'}")
|
||||||
|
|
||||||
@cli.command() # @cli, not @click!
|
@cli.command() # @cli, not @click!
|
||||||
def sync():
|
def sync():
|
||||||
|
@ -96,7 +96,7 @@ script like this:
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def sync(ctx):
|
def sync(ctx):
|
||||||
click.echo('Debug is %s' % (ctx.obj['DEBUG'] and 'on' or 'off'))
|
click.echo(f"Debug is {'on' if ctx.obj['DEBUG'] else 'off'}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
cli(obj={})
|
cli(obj={})
|
||||||
|
@ -166,7 +166,7 @@ Example:
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
click.echo('I was invoked without subcommand')
|
click.echo('I was invoked without subcommand')
|
||||||
else:
|
else:
|
||||||
click.echo('I am about to invoke %s' % ctx.invoked_subcommand)
|
click.echo(f"I am about to invoke {ctx.invoked_subcommand}")
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def sync():
|
def sync():
|
||||||
|
@ -202,7 +202,7 @@ A custom multi command just needs to implement a list and load method:
|
||||||
def list_commands(self, ctx):
|
def list_commands(self, ctx):
|
||||||
rv = []
|
rv = []
|
||||||
for filename in os.listdir(plugin_folder):
|
for filename in os.listdir(plugin_folder):
|
||||||
if filename.endswith('.py'):
|
if filename.endswith('.py') and filename != '__init__.py':
|
||||||
rv.append(filename[:-3])
|
rv.append(filename[:-3])
|
||||||
rv.sort()
|
rv.sort()
|
||||||
return rv
|
return rv
|
||||||
|
@ -287,7 +287,7 @@ Multi Command Chaining
|
||||||
Sometimes it is useful to be allowed to invoke more than one subcommand in
|
Sometimes it is useful to be allowed to invoke more than one subcommand in
|
||||||
one go. For instance if you have installed a setuptools package before
|
one go. For instance if you have installed a setuptools package before
|
||||||
you might be familiar with the ``setup.py sdist bdist_wheel upload``
|
you might be familiar with the ``setup.py sdist bdist_wheel upload``
|
||||||
command chain which invokes ``dist`` before ``bdist_wheel`` before
|
command chain which invokes ``sdist`` before ``bdist_wheel`` before
|
||||||
``upload``. Starting with Click 3.0 this is very simple to implement.
|
``upload``. Starting with Click 3.0 this is very simple to implement.
|
||||||
All you have to do is to pass ``chain=True`` to your multicommand:
|
All you have to do is to pass ``chain=True`` to your multicommand:
|
||||||
|
|
||||||
|
@ -351,7 +351,7 @@ how to do its processing. At that point it then returns a processing
|
||||||
function and returns.
|
function and returns.
|
||||||
|
|
||||||
Where do the returned functions go? The chained multicommand can register
|
Where do the returned functions go? The chained multicommand can register
|
||||||
a callback with :meth:`MultiCommand.resultcallback` that goes over all
|
a callback with :meth:`MultiCommand.result_callback` that goes over all
|
||||||
these functions and then invoke them.
|
these functions and then invoke them.
|
||||||
|
|
||||||
To make this a bit more concrete consider this example:
|
To make this a bit more concrete consider this example:
|
||||||
|
@ -363,7 +363,7 @@ To make this a bit more concrete consider this example:
|
||||||
def cli(input):
|
def cli(input):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@cli.resultcallback()
|
@cli.result_callback()
|
||||||
def process_pipeline(processors, input):
|
def process_pipeline(processors, input):
|
||||||
iterator = (x.rstrip('\r\n') for x in input)
|
iterator = (x.rstrip('\r\n') for x in input)
|
||||||
for processor in processors:
|
for processor in processors:
|
||||||
|
@ -422,7 +422,7 @@ to not use the file type and manually open the file through
|
||||||
|
|
||||||
For a more complex example that also improves upon handling of the
|
For a more complex example that also improves upon handling of the
|
||||||
pipelines have a look at the `imagepipe multi command chaining demo
|
pipelines have a look at the `imagepipe multi command chaining demo
|
||||||
<https://github.com/pallets/click/tree/master/examples/imagepipe>`__ in
|
<https://github.com/pallets/click/tree/main/examples/imagepipe>`__ in
|
||||||
the Click repository. It implements a pipeline based image editing tool
|
the Click repository. It implements a pipeline based image editing tool
|
||||||
that has a nice internal structure for the pipelines.
|
that has a nice internal structure for the pipelines.
|
||||||
|
|
||||||
|
@ -466,7 +466,7 @@ Example usage:
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--port', default=8000)
|
@click.option('--port', default=8000)
|
||||||
def runserver(port):
|
def runserver(port):
|
||||||
click.echo('Serving on http://127.0.0.1:%d/' % port)
|
click.echo(f"Serving on http://127.0.0.1:{port}/")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
cli(default_map={
|
cli(default_map={
|
||||||
|
@ -512,7 +512,7 @@ This example does the same as the previous example:
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--port', default=8000)
|
@click.option('--port', default=8000)
|
||||||
def runserver(port):
|
def runserver(port):
|
||||||
click.echo('Serving on http://127.0.0.1:%d/' % port)
|
click.echo(f"Serving on http://127.0.0.1:{port}/")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
cli()
|
cli()
|
||||||
|
|
23
docs/conf.py
23
docs/conf.py
|
@ -1,12 +1,17 @@
|
||||||
from pallets_sphinx_themes import get_version
|
from pallets_sphinx_themes import get_version
|
||||||
from pallets_sphinx_themes import ProjectLink
|
from pallets_sphinx_themes import ProjectLink
|
||||||
|
|
||||||
|
import click._compat
|
||||||
|
|
||||||
|
# compat until pallets-sphinx-themes is updated
|
||||||
|
click._compat.text_type = str
|
||||||
|
|
||||||
# Project --------------------------------------------------------------
|
# Project --------------------------------------------------------------
|
||||||
|
|
||||||
project = "Click"
|
project = "Click"
|
||||||
copyright = "2014 Pallets"
|
copyright = "2014 Pallets"
|
||||||
author = "Pallets"
|
author = "Pallets"
|
||||||
release, version = get_version("Click", version_length=1)
|
release, version = get_version("Click")
|
||||||
|
|
||||||
# General --------------------------------------------------------------
|
# General --------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -17,7 +22,9 @@ extensions = [
|
||||||
"sphinxcontrib.log_cabinet",
|
"sphinxcontrib.log_cabinet",
|
||||||
"pallets_sphinx_themes",
|
"pallets_sphinx_themes",
|
||||||
"sphinx_issues",
|
"sphinx_issues",
|
||||||
|
"sphinx_tabs.tabs",
|
||||||
]
|
]
|
||||||
|
autodoc_typehints = "description"
|
||||||
intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
|
intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
|
||||||
issues_github_path = "pallets/click"
|
issues_github_path = "pallets/click"
|
||||||
|
|
||||||
|
@ -27,18 +34,20 @@ html_theme = "click"
|
||||||
html_theme_options = {"index_sidebar_logo": False}
|
html_theme_options = {"index_sidebar_logo": False}
|
||||||
html_context = {
|
html_context = {
|
||||||
"project_links": [
|
"project_links": [
|
||||||
ProjectLink("Donate to Pallets", "https://palletsprojects.com/donate"),
|
ProjectLink("Donate", "https://palletsprojects.com/donate"),
|
||||||
ProjectLink("Click Website", "https://palletsprojects.com/p/click/"),
|
ProjectLink("PyPI Releases", "https://pypi.org/project/click/"),
|
||||||
ProjectLink("PyPI releases", "https://pypi.org/project/click/"),
|
|
||||||
ProjectLink("Source Code", "https://github.com/pallets/click/"),
|
ProjectLink("Source Code", "https://github.com/pallets/click/"),
|
||||||
ProjectLink("Issue Tracker", "https://github.com/pallets/click/issues/"),
|
ProjectLink("Issue Tracker", "https://github.com/pallets/click/issues/"),
|
||||||
|
ProjectLink("Website", "https://palletsprojects.com/p/click"),
|
||||||
|
ProjectLink("Twitter", "https://twitter.com/PalletsTeam"),
|
||||||
|
ProjectLink("Chat", "https://discord.gg/pallets"),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
html_sidebars = {
|
html_sidebars = {
|
||||||
"index": ["project.html", "localtoc.html", "searchbox.html"],
|
"index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"],
|
||||||
"**": ["localtoc.html", "relations.html", "searchbox.html"],
|
"**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"],
|
||||||
}
|
}
|
||||||
singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]}
|
singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]}
|
||||||
html_static_path = ["_static"]
|
html_static_path = ["_static"]
|
||||||
html_favicon = "_static/click-icon.png"
|
html_favicon = "_static/click-icon.png"
|
||||||
html_logo = "_static/click-logo-sidebar.png"
|
html_logo = "_static/click-logo-sidebar.png"
|
||||||
|
|
|
@ -24,7 +24,7 @@ Simple example:
|
||||||
def hello(count, name):
|
def hello(count, name):
|
||||||
"""This script prints hello NAME COUNT times."""
|
"""This script prints hello NAME COUNT times."""
|
||||||
for x in range(count):
|
for x in range(count):
|
||||||
click.echo('Hello %s!' % name)
|
click.echo(f"Hello {name}!")
|
||||||
|
|
||||||
And what it looks like:
|
And what it looks like:
|
||||||
|
|
||||||
|
@ -173,7 +173,7 @@ desired. This can be customized at all levels:
|
||||||
def hello(count, name):
|
def hello(count, name):
|
||||||
"""This script prints hello <name> <int> times."""
|
"""This script prints hello <name> <int> times."""
|
||||||
for x in range(count):
|
for x in range(count):
|
||||||
click.echo('Hello %s!' % name)
|
click.echo(f"Hello {name}!")
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ What does it look like? Here is an example of a simple Click program:
|
||||||
def hello(count, name):
|
def hello(count, name):
|
||||||
"""Simple program that greets NAME for a total of COUNT times."""
|
"""Simple program that greets NAME for a total of COUNT times."""
|
||||||
for x in range(count):
|
for x in range(count):
|
||||||
click.echo('Hello %s!' % name)
|
click.echo(f"Hello {name}!")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
hello()
|
hello()
|
||||||
|
@ -79,9 +79,9 @@ usage patterns.
|
||||||
advanced
|
advanced
|
||||||
testing
|
testing
|
||||||
utils
|
utils
|
||||||
bashcomplete
|
shell-completion
|
||||||
exceptions
|
exceptions
|
||||||
python3
|
unicode-support
|
||||||
wincmd
|
wincmd
|
||||||
|
|
||||||
API Reference
|
API Reference
|
||||||
|
@ -102,6 +102,6 @@ Miscellaneous Pages
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
contrib
|
contrib
|
||||||
changelog
|
|
||||||
upgrading
|
upgrading
|
||||||
license
|
license
|
||||||
|
changes
|
||||||
|
|
206
docs/options.rst
206
docs/options.rst
|
@ -85,7 +85,7 @@ simply pass in `required=True` as an argument to the decorator.
|
||||||
@click.option('--from', '-f', 'from_')
|
@click.option('--from', '-f', 'from_')
|
||||||
@click.option('--to', '-t')
|
@click.option('--to', '-t')
|
||||||
def reserved_param_name(from_, to):
|
def reserved_param_name(from_, to):
|
||||||
click.echo('from %s to %s' % (from_, to))
|
click.echo(f"from {from_} to {to}")
|
||||||
|
|
||||||
And on the command line:
|
And on the command line:
|
||||||
|
|
||||||
|
@ -121,7 +121,8 @@ the ``nargs`` parameter. The values are then stored as a tuple.
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--pos', nargs=2, type=float)
|
@click.option('--pos', nargs=2, type=float)
|
||||||
def findme(pos):
|
def findme(pos):
|
||||||
click.echo('%s / %s' % pos)
|
a, b = pos
|
||||||
|
click.echo(f"{a} / {b}")
|
||||||
|
|
||||||
And on the command line:
|
And on the command line:
|
||||||
|
|
||||||
|
@ -146,7 +147,8 @@ the tuple. For this you can directly specify a tuple as type:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--item', type=(str, int))
|
@click.option('--item', type=(str, int))
|
||||||
def putitem(item):
|
def putitem(item):
|
||||||
click.echo('name=%s id=%d' % item)
|
name, id = item
|
||||||
|
click.echo(f"name={name} id={id}")
|
||||||
|
|
||||||
And on the command line:
|
And on the command line:
|
||||||
|
|
||||||
|
@ -163,7 +165,8 @@ used. The above example is thus equivalent to this:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--item', nargs=2, type=click.Tuple([str, int]))
|
@click.option('--item', nargs=2, type=click.Tuple([str, int]))
|
||||||
def putitem(item):
|
def putitem(item):
|
||||||
click.echo('name=%s id=%d' % item)
|
name, id = item
|
||||||
|
click.echo(f"name={name} id={id}")
|
||||||
|
|
||||||
.. _multiple-options:
|
.. _multiple-options:
|
||||||
|
|
||||||
|
@ -212,7 +215,7 @@ for instance:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('-v', '--verbose', count=True)
|
@click.option('-v', '--verbose', count=True)
|
||||||
def log(verbose):
|
def log(verbose):
|
||||||
click.echo('Verbosity: %s' % verbose)
|
click.echo(f"Verbosity: {verbose}")
|
||||||
|
|
||||||
And on the command line:
|
And on the command line:
|
||||||
|
|
||||||
|
@ -250,6 +253,7 @@ And on the command line:
|
||||||
|
|
||||||
invoke(info, args=['--shout'])
|
invoke(info, args=['--shout'])
|
||||||
invoke(info, args=['--no-shout'])
|
invoke(info, args=['--no-shout'])
|
||||||
|
invoke(info)
|
||||||
|
|
||||||
If you really don't want an off-switch, you can just define one and
|
If you really don't want an off-switch, you can just define one and
|
||||||
manually inform Click that something is a flag:
|
manually inform Click that something is a flag:
|
||||||
|
@ -271,6 +275,7 @@ And on the command line:
|
||||||
.. click:run::
|
.. click:run::
|
||||||
|
|
||||||
invoke(info, args=['--shout'])
|
invoke(info, args=['--shout'])
|
||||||
|
invoke(info)
|
||||||
|
|
||||||
Note that if a slash is contained in your option already (for instance, if
|
Note that if a slash is contained in your option already (for instance, if
|
||||||
you use Windows-style parameters where ``/`` is the prefix character), you
|
you use Windows-style parameters where ``/`` is the prefix character), you
|
||||||
|
@ -281,7 +286,7 @@ can alternatively split the parameters through ``;`` instead:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('/debug;/no-debug')
|
@click.option('/debug;/no-debug')
|
||||||
def log(debug):
|
def log(debug):
|
||||||
click.echo('debug=%s' % debug)
|
click.echo(f"debug={debug}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
log()
|
log()
|
||||||
|
@ -402,7 +407,7 @@ Example:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--name', prompt=True)
|
@click.option('--name', prompt=True)
|
||||||
def hello(name):
|
def hello(name):
|
||||||
click.echo('Hello %s!' % name)
|
click.echo(f"Hello {name}!")
|
||||||
|
|
||||||
And what it looks like:
|
And what it looks like:
|
||||||
|
|
||||||
|
@ -419,7 +424,7 @@ a different one:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--name', prompt='Your name please')
|
@click.option('--name', prompt='Your name please')
|
||||||
def hello(name):
|
def hello(name):
|
||||||
click.echo('Hello %s!' % name)
|
click.echo(f"Hello {name}!")
|
||||||
|
|
||||||
What it looks like:
|
What it looks like:
|
||||||
|
|
||||||
|
@ -430,6 +435,10 @@ What it looks like:
|
||||||
It is advised that prompt not be used in conjunction with the multiple
|
It is advised that prompt not be used in conjunction with the multiple
|
||||||
flag set to True. Instead, prompt in the function interactively.
|
flag set to True. Instead, prompt in the function interactively.
|
||||||
|
|
||||||
|
By default, the user will be prompted for an input if one was not passed
|
||||||
|
through the command line. To turn this behavior off, see
|
||||||
|
:ref:`optional-value`.
|
||||||
|
|
||||||
|
|
||||||
Password Prompts
|
Password Prompts
|
||||||
----------------
|
----------------
|
||||||
|
@ -439,27 +448,30 @@ useful for password input:
|
||||||
|
|
||||||
.. click:example::
|
.. click:example::
|
||||||
|
|
||||||
@click.command()
|
import codecs
|
||||||
@click.option('--password', prompt=True, hide_input=True,
|
|
||||||
confirmation_prompt=True)
|
|
||||||
def encrypt(password):
|
|
||||||
click.echo('Encrypting password to %s' % password.encode('rot13'))
|
|
||||||
|
|
||||||
What it looks like:
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"--password", prompt=True, hide_input=True,
|
||||||
|
confirmation_prompt=True
|
||||||
|
)
|
||||||
|
def encode(password):
|
||||||
|
click.echo(f"encoded: {codecs.encode(password, 'rot13')}")
|
||||||
|
|
||||||
.. click:run::
|
.. click:run::
|
||||||
|
|
||||||
invoke(encrypt, input=['secret', 'secret'])
|
invoke(encode, input=['secret', 'secret'])
|
||||||
|
|
||||||
Because this combination of parameters is quite common, this can also be
|
Because this combination of parameters is quite common, this can also be
|
||||||
replaced with the :func:`password_option` decorator:
|
replaced with the :func:`password_option` decorator:
|
||||||
|
|
||||||
.. click:example::
|
.. code-block:: python
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.password_option()
|
@click.password_option()
|
||||||
def encrypt(password):
|
def encrypt(password):
|
||||||
click.echo('Encrypting password to %s' % password.encode('rot13'))
|
click.echo(f"encoded: to {codecs.encode(password, 'rot13')}")
|
||||||
|
|
||||||
|
|
||||||
Dynamic Defaults for Prompts
|
Dynamic Defaults for Prompts
|
||||||
----------------------------
|
----------------------------
|
||||||
|
@ -474,28 +486,37 @@ prompted if the option isn't specified on the command line, you can do so
|
||||||
by supplying a callable as the default value. For example, to get a default
|
by supplying a callable as the default value. For example, to get a default
|
||||||
from the environment:
|
from the environment:
|
||||||
|
|
||||||
.. click:example::
|
.. code-block:: python
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--username', prompt=True,
|
@click.option(
|
||||||
default=lambda: os.environ.get('USER', ''))
|
"--username", prompt=True,
|
||||||
|
default=lambda: os.environ.get("USER", "")
|
||||||
|
)
|
||||||
def hello(username):
|
def hello(username):
|
||||||
print("Hello,", username)
|
click.echo(f"Hello, {username}!")
|
||||||
|
|
||||||
To describe what the default value will be, set it in ``show_default``.
|
To describe what the default value will be, set it in ``show_default``.
|
||||||
|
|
||||||
.. click:example::
|
.. click:example::
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--username', prompt=True,
|
@click.option(
|
||||||
default=lambda: os.environ.get('USER', ''),
|
"--username", prompt=True,
|
||||||
show_default='current user')
|
default=lambda: os.environ.get("USER", ""),
|
||||||
|
show_default="current user"
|
||||||
|
)
|
||||||
def hello(username):
|
def hello(username):
|
||||||
print("Hello,", username)
|
click.echo(f"Hello, {username}!")
|
||||||
|
|
||||||
.. click:run::
|
.. click:run::
|
||||||
|
|
||||||
invoke(hello, args=['--help'])
|
invoke(hello, args=["--help"])
|
||||||
|
|
||||||
|
|
||||||
Callbacks and Eager Options
|
Callbacks and Eager Options
|
||||||
---------------------------
|
---------------------------
|
||||||
|
@ -625,7 +646,7 @@ Example usage:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--username')
|
@click.option('--username')
|
||||||
def greet(username):
|
def greet(username):
|
||||||
click.echo('Hello %s!' % username)
|
click.echo(f'Hello {username}!')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
greet(auto_envvar_prefix='GREETER')
|
greet(auto_envvar_prefix='GREETER')
|
||||||
|
@ -650,12 +671,12 @@ Example:
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.option('--debug/--no-debug')
|
@click.option('--debug/--no-debug')
|
||||||
def cli(debug):
|
def cli(debug):
|
||||||
click.echo('Debug mode is %s' % ('on' if debug else 'off'))
|
click.echo(f"Debug mode is {'on' if debug else 'off'}")
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--username')
|
@click.option('--username')
|
||||||
def greet(username):
|
def greet(username):
|
||||||
click.echo('Hello %s!' % username)
|
click.echo(f"Hello {username}!")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
cli(auto_envvar_prefix='GREETER')
|
cli(auto_envvar_prefix='GREETER')
|
||||||
|
@ -677,7 +698,7 @@ Example usage:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--username', envvar='USERNAME')
|
@click.option('--username', envvar='USERNAME')
|
||||||
def greet(username):
|
def greet(username):
|
||||||
click.echo('Hello %s!' % username)
|
click.echo(f"Hello {username}!")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
greet()
|
greet()
|
||||||
|
@ -726,7 +747,7 @@ And from the command line:
|
||||||
.. click:run::
|
.. click:run::
|
||||||
|
|
||||||
import os
|
import os
|
||||||
invoke(perform, env={'PATHS': './foo/bar%s./test' % os.path.pathsep})
|
invoke(perform, env={"PATHS": f"./foo/bar{os.path.pathsep}./test"})
|
||||||
|
|
||||||
Other Prefix Characters
|
Other Prefix Characters
|
||||||
-----------------------
|
-----------------------
|
||||||
|
@ -742,7 +763,7 @@ POSIX semantics. However in certain situations this can be useful:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('+w/-w')
|
@click.option('+w/-w')
|
||||||
def chmod(w):
|
def chmod(w):
|
||||||
click.echo('writable=%s' % w)
|
click.echo(f"writable={w}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
chmod()
|
chmod()
|
||||||
|
@ -762,7 +783,7 @@ boolean flag you need to separate it with ``;`` instead of ``/``:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('/debug;/no-debug')
|
@click.option('/debug;/no-debug')
|
||||||
def log(debug):
|
def log(debug):
|
||||||
click.echo('debug=%s' % debug)
|
click.echo(f"debug={debug}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
log()
|
log()
|
||||||
|
@ -772,39 +793,34 @@ boolean flag you need to separate it with ``;`` instead of ``/``:
|
||||||
Range Options
|
Range Options
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
A special mention should go to the :class:`IntRange` type, which works very
|
The :class:`IntRange` type extends the :data:`INT` type to ensure the
|
||||||
similarly to the :data:`INT` type, but restricts the value to fall into a
|
value is contained in the given range. The :class:`FloatRange` type does
|
||||||
specific range (inclusive on both edges). It has two modes:
|
the same for :data:`FLOAT`.
|
||||||
|
|
||||||
- the default mode (non-clamping mode) where a value that falls outside
|
If ``min`` or ``max`` is omitted, that side is *unbounded*. Any value in
|
||||||
of the range will cause an error.
|
that direction is accepted. By default, both bounds are *closed*, which
|
||||||
- an optional clamping mode where a value that falls outside of the
|
means the boundary value is included in the accepted range. ``min_open``
|
||||||
range will be clamped. This means that a range of ``0-5`` would
|
and ``max_open`` can be used to exclude that boundary from the range.
|
||||||
return ``5`` for the value ``10`` or ``0`` for the value ``-1`` (for
|
|
||||||
example).
|
|
||||||
|
|
||||||
Example:
|
If ``clamp`` mode is enabled, a value that is outside the range is set
|
||||||
|
to the boundary instead of failing. For example, the range ``0, 5``
|
||||||
|
would return ``5`` for the value ``10``, or ``0`` for the value ``-1``.
|
||||||
|
When using :class:`FloatRange`, ``clamp`` can only be enabled if both
|
||||||
|
bounds are *closed* (the default).
|
||||||
|
|
||||||
.. click:example::
|
.. click:example::
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--count', type=click.IntRange(0, 20, clamp=True))
|
@click.option("--count", type=click.IntRange(0, 20, clamp=True))
|
||||||
@click.option('--digit', type=click.IntRange(0, 10))
|
@click.option("--digit", type=click.IntRange(0, 9))
|
||||||
def repeat(count, digit):
|
def repeat(count, digit):
|
||||||
click.echo(str(digit) * count)
|
click.echo(str(digit) * count)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
repeat()
|
|
||||||
|
|
||||||
And from the command line:
|
|
||||||
|
|
||||||
.. click:run::
|
.. click:run::
|
||||||
|
|
||||||
invoke(repeat, args=['--count=1000', '--digit=5'])
|
invoke(repeat, args=['--count=100', '--digit=5'])
|
||||||
invoke(repeat, args=['--count=1000', '--digit=12'])
|
invoke(repeat, args=['--count=6', '--digit=12'])
|
||||||
|
|
||||||
If you pass ``None`` for any of the edges, it means that the range is open
|
|
||||||
at that side.
|
|
||||||
|
|
||||||
Callbacks for Validation
|
Callbacks for Validation
|
||||||
------------------------
|
------------------------
|
||||||
|
@ -812,37 +828,87 @@ Callbacks for Validation
|
||||||
.. versionchanged:: 2.0
|
.. versionchanged:: 2.0
|
||||||
|
|
||||||
If you want to apply custom validation logic, you can do this in the
|
If you want to apply custom validation logic, you can do this in the
|
||||||
parameter callbacks. These callbacks can both modify values as well as
|
parameter callbacks. These callbacks can both modify values as well as
|
||||||
raise errors if the validation does not work.
|
raise errors if the validation does not work. The callback runs after
|
||||||
|
type conversion. It is called for all sources, including prompts.
|
||||||
|
|
||||||
In Click 1.0, you can only raise the :exc:`UsageError` but starting with
|
In Click 1.0, you can only raise the :exc:`UsageError` but starting with
|
||||||
Click 2.0, you can also raise the :exc:`BadParameter` error, which has the
|
Click 2.0, you can also raise the :exc:`BadParameter` error, which has the
|
||||||
added advantage that it will automatically format the error message to
|
added advantage that it will automatically format the error message to
|
||||||
also contain the parameter name.
|
also contain the parameter name.
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. click:example::
|
.. click:example::
|
||||||
|
|
||||||
def validate_rolls(ctx, param, value):
|
def validate_rolls(ctx, param, value):
|
||||||
|
if isinstance(value, tuple):
|
||||||
|
return value
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rolls, dice = map(int, value.split('d', 2))
|
rolls, _, dice = value.partition("d")
|
||||||
return (dice, rolls)
|
return int(dice), int(rolls)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise click.BadParameter('rolls need to be in format NdM')
|
raise click.BadParameter("format must be 'NdM'")
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--rolls', callback=validate_rolls, default='1d6')
|
@click.option(
|
||||||
|
"--rolls", type=click.UNPROCESSED, callback=validate_rolls,
|
||||||
|
default="1d6", prompt=True,
|
||||||
|
)
|
||||||
def roll(rolls):
|
def roll(rolls):
|
||||||
click.echo('Rolling a %d-sided dice %d time(s)' % rolls)
|
sides, times = rolls
|
||||||
|
click.echo(f"Rolling a {sides}-sided dice {times} time(s)")
|
||||||
if __name__ == '__main__':
|
|
||||||
roll()
|
|
||||||
|
|
||||||
And what it looks like:
|
|
||||||
|
|
||||||
.. click:run::
|
.. click:run::
|
||||||
|
|
||||||
invoke(roll, args=['--rolls=42'])
|
invoke(roll, args=["--rolls=42"])
|
||||||
println()
|
println()
|
||||||
invoke(roll, args=['--rolls=2d12'])
|
invoke(roll, args=["--rolls=2d12"])
|
||||||
|
println()
|
||||||
|
invoke(roll, input=["42", "2d12"])
|
||||||
|
|
||||||
|
|
||||||
|
.. _optional-value:
|
||||||
|
|
||||||
|
Optional Value
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Providing the value to an option can be made optional, in which case
|
||||||
|
providing only the option's flag without a value will either show a
|
||||||
|
prompt or use its ``flag_value``.
|
||||||
|
|
||||||
|
Setting ``is_flag=False, flag_value=value`` tells Click that the option
|
||||||
|
can still be passed a value, but if only the flag is given the
|
||||||
|
``flag_value`` is used.
|
||||||
|
|
||||||
|
.. click:example::
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option("--name", is_flag=False, flag_value="Flag", default="Default")
|
||||||
|
def hello(name):
|
||||||
|
click.echo(f"Hello, {name}!")
|
||||||
|
|
||||||
|
.. click:run::
|
||||||
|
|
||||||
|
invoke(hello, args=[])
|
||||||
|
invoke(hello, args=["--name", "Value"])
|
||||||
|
invoke(hello, args=["--name"])
|
||||||
|
|
||||||
|
If the option has ``prompt`` enabled, then setting
|
||||||
|
``prompt_required=False`` tells Click to only show the prompt if the
|
||||||
|
option's flag is given, instead of if the option is not provided at all.
|
||||||
|
|
||||||
|
.. click:example::
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option('--name', prompt=True, prompt_required=False, default="Default")
|
||||||
|
def hello(name):
|
||||||
|
click.echo(f"Hello {name}!")
|
||||||
|
|
||||||
|
.. click:run::
|
||||||
|
|
||||||
|
invoke(hello)
|
||||||
|
invoke(hello, args=["--name", "Value"])
|
||||||
|
invoke(hello, args=["--name"], input="Prompt")
|
||||||
|
|
||||||
|
If ``required=True``, then the option will still prompt if it is not
|
||||||
|
given, but it will also prompt if only the flag is given.
|
||||||
|
|
|
@ -47,10 +47,10 @@ different behavior and some are supported out of the box:
|
||||||
A parameter that only accepts floating point values.
|
A parameter that only accepts floating point values.
|
||||||
|
|
||||||
``bool`` / :data:`click.BOOL`:
|
``bool`` / :data:`click.BOOL`:
|
||||||
A parameter that accepts boolean values. This is automatically used
|
A parameter that accepts boolean values. This is automatically used
|
||||||
for boolean flags. If used with string values ``1``, ``yes``, ``y``, ``t``
|
for boolean flags. The string values "1", "true", "t", "yes", "y",
|
||||||
and ``true`` convert to `True` and ``0``, ``no``, ``n``, ``f`` and ``false``
|
and "on" convert to ``True``. "0", "false", "f", "no", "n", and
|
||||||
convert to `False`.
|
"off" convert to ``False``.
|
||||||
|
|
||||||
:data:`click.UUID`:
|
:data:`click.UUID`:
|
||||||
A parameter that accepts UUID values. This is not automatically
|
A parameter that accepts UUID values. This is not automatically
|
||||||
|
@ -118,19 +118,15 @@ integers.
|
||||||
name = "integer"
|
name = "integer"
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(self, value, param, ctx):
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if value[:2].lower() == "0x":
|
if value[:2].lower() == "0x":
|
||||||
return int(value[2:], 16)
|
return int(value[2:], 16)
|
||||||
elif value[:1] == "0":
|
elif value[:1] == "0":
|
||||||
return int(value, 8)
|
return int(value, 8)
|
||||||
return int(value, 10)
|
return int(value, 10)
|
||||||
except TypeError:
|
|
||||||
self.fail(
|
|
||||||
"expected string for int() conversion, got "
|
|
||||||
f"{value!r} of type {type(value).__name__}",
|
|
||||||
param,
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.fail(f"{value!r} is not a valid integer", param, ctx)
|
self.fail(f"{value!r} is not a valid integer", param, ctx)
|
||||||
|
|
||||||
|
@ -140,3 +136,8 @@ The :attr:`~ParamType.name` attribute is optional and is used for
|
||||||
documentation. Call :meth:`~ParamType.fail` if conversion fails. The
|
documentation. Call :meth:`~ParamType.fail` if conversion fails. The
|
||||||
``param`` and ``ctx`` arguments may be ``None`` in some cases such as
|
``param`` and ``ctx`` arguments may be ``None`` in some cases such as
|
||||||
prompts.
|
prompts.
|
||||||
|
|
||||||
|
Values from user input or the command line will be strings, but default
|
||||||
|
values and Python arguments may already be the correct type. The custom
|
||||||
|
type should check at the top if the value is already valid and pass it
|
||||||
|
through to support those cases.
|
||||||
|
|
191
docs/python3.rst
191
docs/python3.rst
|
@ -1,191 +0,0 @@
|
||||||
Python 3 Support
|
|
||||||
================
|
|
||||||
|
|
||||||
.. currentmodule:: click
|
|
||||||
|
|
||||||
Click supports Python 3, but like all other command line utility libraries,
|
|
||||||
it suffers from the Unicode text model in Python 3. All examples in the
|
|
||||||
documentation were written so that they could run on both Python 2.x and
|
|
||||||
Python 3.4 or higher.
|
|
||||||
|
|
||||||
.. _python3-limitations:
|
|
||||||
|
|
||||||
Python 3 Limitations
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
At the moment, Click suffers from a few problems with Python 3:
|
|
||||||
|
|
||||||
* The command line in Unix traditionally is in bytes, not Unicode. While
|
|
||||||
there are encoding hints for all of this, there are generally some
|
|
||||||
situations where this can break. The most common one is SSH
|
|
||||||
connections to machines with different locales.
|
|
||||||
|
|
||||||
Misconfigured environments can currently cause a wide range of Unicode
|
|
||||||
problems in Python 3 due to the lack of support for roundtripping
|
|
||||||
surrogate escapes. This will not be fixed in Click itself!
|
|
||||||
|
|
||||||
For more information see :ref:`python3-surrogates`.
|
|
||||||
|
|
||||||
* Standard input and output in Python 3 is opened in Unicode mode by
|
|
||||||
default. Click has to reopen the stream in binary mode in certain
|
|
||||||
situations. Because there is no standardized way to do this, this
|
|
||||||
might not always work. Primarily this can become a problem when
|
|
||||||
testing command-line applications.
|
|
||||||
|
|
||||||
This is not supported::
|
|
||||||
|
|
||||||
sys.stdin = io.StringIO('Input here')
|
|
||||||
sys.stdout = io.StringIO()
|
|
||||||
|
|
||||||
Instead you need to do this::
|
|
||||||
|
|
||||||
input = 'Input here'
|
|
||||||
in_stream = io.BytesIO(input.encode('utf-8'))
|
|
||||||
sys.stdin = io.TextIOWrapper(in_stream, encoding='utf-8')
|
|
||||||
out_stream = io.BytesIO()
|
|
||||||
sys.stdout = io.TextIOWrapper(out_stream, encoding='utf-8')
|
|
||||||
|
|
||||||
Remember that in that case, you need to use ``out_stream.getvalue()``
|
|
||||||
and not ``sys.stdout.getvalue()`` if you want to access the buffer
|
|
||||||
contents as the wrapper will not forward that method.
|
|
||||||
|
|
||||||
Python 2 and 3 Differences
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
Click attempts to minimize the differences between Python 2 and Python 3
|
|
||||||
by following best practices for both languages.
|
|
||||||
|
|
||||||
In Python 2, the following is true:
|
|
||||||
|
|
||||||
* ``sys.stdin``, ``sys.stdout``, and ``sys.stderr`` are opened in binary
|
|
||||||
mode, but under some circumstances they support Unicode output. Click
|
|
||||||
attempts to not subvert this but provides support for forcing streams
|
|
||||||
to be Unicode-based.
|
|
||||||
* ``sys.argv`` is always byte-based. Click will pass bytes to all
|
|
||||||
input types and convert as necessary. The :class:`STRING` type
|
|
||||||
automatically will decode properly the input value into a string by
|
|
||||||
trying the most appropriate encodings.
|
|
||||||
* When dealing with files, Click will never go through the Unicode APIs
|
|
||||||
and will instead use the operating system's byte APIs to open the
|
|
||||||
files.
|
|
||||||
|
|
||||||
In Python 3, the following is true:
|
|
||||||
|
|
||||||
* ``sys.stdin``, ``sys.stdout`` and ``sys.stderr`` are by default
|
|
||||||
text-based. When Click needs a binary stream, it attempts to discover
|
|
||||||
the underlying binary stream. See :ref:`python3-limitations` for how
|
|
||||||
this works.
|
|
||||||
* ``sys.argv`` is always Unicode-based. This also means that the native
|
|
||||||
type for input values to the types in Click is Unicode, and not bytes.
|
|
||||||
|
|
||||||
This causes problems if the terminal is incorrectly set and Python
|
|
||||||
does not figure out the encoding. In that case, the Unicode string
|
|
||||||
will contain error bytes encoded as surrogate escapes.
|
|
||||||
* When dealing with files, Click will always use the Unicode file system
|
|
||||||
API calls by using the operating system's reported or guessed
|
|
||||||
filesystem encoding. Surrogates are supported for filenames, so it
|
|
||||||
should be possible to open files through the :class:`File` type even
|
|
||||||
if the environment is misconfigured.
|
|
||||||
|
|
||||||
.. _python3-surrogates:
|
|
||||||
|
|
||||||
Python 3 Surrogate Handling
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
Click in Python 3 does all the Unicode handling in the standard library
|
|
||||||
and is subject to its behavior. In Python 2, Click does all the Unicode
|
|
||||||
handling itself, which means there are differences in error behavior.
|
|
||||||
|
|
||||||
The most glaring difference is that in Python 2, Unicode will "just work",
|
|
||||||
while in Python 3, it requires extra care. The reason for this is that in
|
|
||||||
Python 3, the encoding detection is done in the interpreter, and on Linux
|
|
||||||
and certain other operating systems, its encoding handling is problematic.
|
|
||||||
|
|
||||||
The biggest source of frustration is that Click scripts invoked by
|
|
||||||
init systems (sysvinit, upstart, systemd, etc.), deployment tools (salt,
|
|
||||||
puppet), or cron jobs (cron) will refuse to work unless a Unicode locale is
|
|
||||||
exported.
|
|
||||||
|
|
||||||
If Click encounters such an environment it will prevent further execution
|
|
||||||
to force you to set a locale. This is done because Click cannot know
|
|
||||||
about the state of the system once it's invoked and restore the values
|
|
||||||
before Python's Unicode handling kicked in.
|
|
||||||
|
|
||||||
If you see something like this error in Python 3::
|
|
||||||
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
RuntimeError: Click will abort further execution because Python 3 was
|
|
||||||
configured to use ASCII as encoding for the environment. Either switch
|
|
||||||
to Python 2 or consult the Python 3 section of the docs for
|
|
||||||
mitigation steps.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
In Python 3.7 and later you will no longer get a ``RuntimeError`` in
|
|
||||||
many cases thanks to :pep:`538` and :pep:`540`, which changed the
|
|
||||||
default assumption in unconfigured environments.
|
|
||||||
|
|
||||||
You are dealing with an environment where Python 3 thinks you are
|
|
||||||
restricted to ASCII data. The solution to these problems is different
|
|
||||||
depending on which locale your computer is running in.
|
|
||||||
|
|
||||||
For instance, if you have a German Linux machine, you can fix the problem
|
|
||||||
by exporting the locale to ``de_DE.utf-8``::
|
|
||||||
|
|
||||||
export LC_ALL=de_DE.utf-8
|
|
||||||
export LANG=de_DE.utf-8
|
|
||||||
|
|
||||||
If you are on a US machine, ``en_US.utf-8`` is the encoding of choice. On
|
|
||||||
some newer Linux systems, you could also try ``C.UTF-8`` as the locale::
|
|
||||||
|
|
||||||
export LC_ALL=C.UTF-8
|
|
||||||
export LANG=C.UTF-8
|
|
||||||
|
|
||||||
On some systems it was reported that `UTF-8` has to be written as `UTF8`
|
|
||||||
and vice versa. To see which locales are supported you can invoke
|
|
||||||
``locale -a``::
|
|
||||||
|
|
||||||
locale -a
|
|
||||||
|
|
||||||
You need to do this before you invoke your Python script. If you are
|
|
||||||
curious about the reasons for this, you can join the discussions in the
|
|
||||||
Python 3 bug tracker:
|
|
||||||
|
|
||||||
* `ASCII is a bad filesystem default encoding
|
|
||||||
<https://bugs.python.org/issue13643#msg149941>`_
|
|
||||||
* `Use surrogateescape as default error handler
|
|
||||||
<https://bugs.python.org/issue19977>`_
|
|
||||||
* `Python 3 raises Unicode errors in the C locale
|
|
||||||
<https://bugs.python.org/issue19846>`_
|
|
||||||
* `LC_CTYPE=C: pydoc leaves terminal in an unusable state
|
|
||||||
<https://bugs.python.org/issue21398>`_ (this is relevant to Click
|
|
||||||
because the pager support is provided by the stdlib pydoc module)
|
|
||||||
|
|
||||||
Note (Python 3.7 onwards): Even though your locale may not be properly
|
|
||||||
configured, Python 3.7 Click will not raise the above exception because Python
|
|
||||||
3.7 programs are better at choosing default locales. This doesn't change the
|
|
||||||
general issue that your locale may be misconfigured.
|
|
||||||
|
|
||||||
Unicode Literals
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Starting with Click 5.0 there will be a warning for the use of the
|
|
||||||
``unicode_literals`` future import in Python 2. This has been done due to
|
|
||||||
the negative consequences of this import with regards to unintentionally
|
|
||||||
causing bugs due to introducing Unicode data to APIs that are incapable of
|
|
||||||
handling them. For some examples of this issue, see the discussion on
|
|
||||||
this github issue: `python-future#22
|
|
||||||
<https://github.com/PythonCharmers/python-future/issues/22>`_.
|
|
||||||
|
|
||||||
If you use ``unicode_literals`` in any file that defines a Click command
|
|
||||||
or that invokes a click command you will be given a warning. You are
|
|
||||||
strongly encouraged to not use ``unicode_literals`` and instead use
|
|
||||||
explicit ``u`` prefixes for your Unicode strings.
|
|
||||||
|
|
||||||
If you do want to ignore the warning and continue using
|
|
||||||
``unicode_literals`` on your own peril, you can disable the warning as
|
|
||||||
follows::
|
|
||||||
|
|
||||||
import click
|
|
||||||
click.disable_unicode_literals_warning = True
|
|
|
@ -32,12 +32,7 @@ install separate copies of Python, but it does provide a clever way to
|
||||||
keep different project environments isolated. Let's see how virtualenv
|
keep different project environments isolated. Let's see how virtualenv
|
||||||
works.
|
works.
|
||||||
|
|
||||||
If you are on Mac OS X or Linux, chances are that one of the following two
|
If you are on Mac OS X or Linux::
|
||||||
commands will work for you::
|
|
||||||
|
|
||||||
$ sudo easy_install virtualenv
|
|
||||||
|
|
||||||
or even better::
|
|
||||||
|
|
||||||
$ pip install virtualenv --user
|
$ pip install virtualenv --user
|
||||||
|
|
||||||
|
@ -84,7 +79,7 @@ After doing this, the prompt of your shell should be as familiar as before.
|
||||||
Now, let's move on. Enter the following command to get Click activated in your
|
Now, let's move on. Enter the following command to get Click activated in your
|
||||||
virtualenv::
|
virtualenv::
|
||||||
|
|
||||||
$ pip install Click
|
$ pip install click
|
||||||
|
|
||||||
A few seconds later and you are good to go.
|
A few seconds later and you are good to go.
|
||||||
|
|
||||||
|
@ -102,23 +97,23 @@ Examples of Click applications can be found in the documentation as well
|
||||||
as in the GitHub repository together with readme files:
|
as in the GitHub repository together with readme files:
|
||||||
|
|
||||||
* ``inout``: `File input and output
|
* ``inout``: `File input and output
|
||||||
<https://github.com/pallets/click/tree/master/examples/inout>`_
|
<https://github.com/pallets/click/tree/main/examples/inout>`_
|
||||||
* ``naval``: `Port of docopt naval example
|
* ``naval``: `Port of docopt naval example
|
||||||
<https://github.com/pallets/click/tree/master/examples/naval>`_
|
<https://github.com/pallets/click/tree/main/examples/naval>`_
|
||||||
* ``aliases``: `Command alias example
|
* ``aliases``: `Command alias example
|
||||||
<https://github.com/pallets/click/tree/master/examples/aliases>`_
|
<https://github.com/pallets/click/tree/main/examples/aliases>`_
|
||||||
* ``repo``: `Git-/Mercurial-like command line interface
|
* ``repo``: `Git-/Mercurial-like command line interface
|
||||||
<https://github.com/pallets/click/tree/master/examples/repo>`_
|
<https://github.com/pallets/click/tree/main/examples/repo>`_
|
||||||
* ``complex``: `Complex example with plugin loading
|
* ``complex``: `Complex example with plugin loading
|
||||||
<https://github.com/pallets/click/tree/master/examples/complex>`_
|
<https://github.com/pallets/click/tree/main/examples/complex>`_
|
||||||
* ``validation``: `Custom parameter validation example
|
* ``validation``: `Custom parameter validation example
|
||||||
<https://github.com/pallets/click/tree/master/examples/validation>`_
|
<https://github.com/pallets/click/tree/main/examples/validation>`_
|
||||||
* ``colors``: `Colorama ANSI color support
|
* ``colors``: `Color support demo
|
||||||
<https://github.com/pallets/click/tree/master/examples/colors>`_
|
<https://github.com/pallets/click/tree/main/examples/colors>`_
|
||||||
* ``termui``: `Terminal UI functions demo
|
* ``termui``: `Terminal UI functions demo
|
||||||
<https://github.com/pallets/click/tree/master/examples/termui>`_
|
<https://github.com/pallets/click/tree/main/examples/termui>`_
|
||||||
* ``imagepipe``: `Multi command chaining demo
|
* ``imagepipe``: `Multi command chaining demo
|
||||||
<https://github.com/pallets/click/tree/master/examples/imagepipe>`_
|
<https://github.com/pallets/click/tree/main/examples/imagepipe>`_
|
||||||
|
|
||||||
Basic Concepts - Creating a Command
|
Basic Concepts - Creating a Command
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
@ -162,7 +157,7 @@ Echoing
|
||||||
|
|
||||||
Why does this example use :func:`echo` instead of the regular
|
Why does this example use :func:`echo` instead of the regular
|
||||||
:func:`print` function? The answer to this question is that Click
|
:func:`print` function? The answer to this question is that Click
|
||||||
attempts to support both Python 2 and Python 3 the same way and to be very
|
attempts to support different environments consistently and to be very
|
||||||
robust even when the environment is misconfigured. Click wants to be
|
robust even when the environment is misconfigured. Click wants to be
|
||||||
functional at least on a basic level even if everything is completely
|
functional at least on a basic level even if everything is completely
|
||||||
broken.
|
broken.
|
||||||
|
@ -171,12 +166,10 @@ What this means is that the :func:`echo` function applies some error
|
||||||
correction in case the terminal is misconfigured instead of dying with an
|
correction in case the terminal is misconfigured instead of dying with an
|
||||||
:exc:`UnicodeError`.
|
:exc:`UnicodeError`.
|
||||||
|
|
||||||
As an added benefit, starting with Click 2.0, the echo function also
|
The echo function also supports color and other styles in output. It
|
||||||
has good support for ANSI colors. It will automatically strip ANSI codes
|
will automatically remove styles if the output stream is a file. On
|
||||||
if the output stream is a file and if colorama is supported, ANSI colors
|
Windows, colorama is automatically installed and used. See
|
||||||
will also work on Windows. Note that in Python 2, the :func:`echo` function
|
:ref:`ansi-colors`.
|
||||||
does not parse color code information from bytearrays. See :ref:`ansi-colors`
|
|
||||||
for more information.
|
|
||||||
|
|
||||||
If you don't need this, you can also use the `print()` construct /
|
If you don't need this, you can also use the `print()` construct /
|
||||||
function.
|
function.
|
||||||
|
@ -233,6 +226,30 @@ other invocations::
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
cli()
|
cli()
|
||||||
|
|
||||||
|
|
||||||
|
Registering Commands Later
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Instead of using the ``@group.command()`` decorator, commands can be
|
||||||
|
decorated with the plain ``@click.command()`` decorator and registered
|
||||||
|
with a group later with ``group.add_command()``. This could be used to
|
||||||
|
split commands into multiple Python modules.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def greet():
|
||||||
|
click.echo("Hello, World!")
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
def group():
|
||||||
|
pass
|
||||||
|
|
||||||
|
group.add_command(greet)
|
||||||
|
|
||||||
|
|
||||||
Adding Parameters
|
Adding Parameters
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
@ -245,7 +262,7 @@ To add parameters, use the :func:`option` and :func:`argument` decorators:
|
||||||
@click.argument('name')
|
@click.argument('name')
|
||||||
def hello(count, name):
|
def hello(count, name):
|
||||||
for x in range(count):
|
for x in range(count):
|
||||||
click.echo('Hello %s!' % name)
|
click.echo(f"Hello {name}!")
|
||||||
|
|
||||||
What it looks like:
|
What it looks like:
|
||||||
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
Sphinx~=2.4.4
|
|
||||||
Pallets-Sphinx-Themes~=1.2.3
|
|
||||||
sphinxcontrib-log-cabinet~=1.0.1
|
|
||||||
sphinx-issues~=1.2.0
|
|
|
@ -43,7 +43,9 @@ Introduction
|
||||||
To bundle your script with setuptools, all you need is the script in a
|
To bundle your script with setuptools, all you need is the script in a
|
||||||
Python package and a ``setup.py`` file.
|
Python package and a ``setup.py`` file.
|
||||||
|
|
||||||
Imagine this directory structure::
|
Imagine this directory structure:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
yourscript.py
|
yourscript.py
|
||||||
setup.py
|
setup.py
|
||||||
|
@ -59,21 +61,24 @@ Contents of ``yourscript.py``:
|
||||||
"""Example script."""
|
"""Example script."""
|
||||||
click.echo('Hello World!')
|
click.echo('Hello World!')
|
||||||
|
|
||||||
Contents of ``setup.py``::
|
Contents of ``setup.py``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='yourscript',
|
name='yourscript',
|
||||||
version='0.1',
|
version='0.1.0',
|
||||||
py_modules=['yourscript'],
|
py_modules=['yourscript'],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'Click',
|
'Click',
|
||||||
],
|
],
|
||||||
entry_points='''
|
entry_points={
|
||||||
[console_scripts]
|
'console_scripts': [
|
||||||
yourscript=yourscript:cli
|
'yourscript = yourscript:cli',
|
||||||
''',
|
],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
The magic is in the ``entry_points`` parameter. Below
|
The magic is in the ``entry_points`` parameter. Below
|
||||||
|
@ -88,7 +93,9 @@ Testing The Script
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
To test the script, you can make a new virtualenv and then install your
|
To test the script, you can make a new virtualenv and then install your
|
||||||
package::
|
package:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
$ virtualenv venv
|
$ virtualenv venv
|
||||||
$ . venv/bin/activate
|
$ . venv/bin/activate
|
||||||
|
@ -105,35 +112,42 @@ Scripts in Packages
|
||||||
|
|
||||||
If your script is growing and you want to switch over to your script being
|
If your script is growing and you want to switch over to your script being
|
||||||
contained in a Python package the changes necessary are minimal. Let's
|
contained in a Python package the changes necessary are minimal. Let's
|
||||||
assume your directory structure changed to this::
|
assume your directory structure changed to this:
|
||||||
|
|
||||||
yourpackage/
|
.. code-block:: text
|
||||||
__init__.py
|
|
||||||
main.py
|
project/
|
||||||
utils.py
|
yourpackage/
|
||||||
scripts/
|
|
||||||
__init__.py
|
__init__.py
|
||||||
yourscript.py
|
main.py
|
||||||
|
utils.py
|
||||||
|
scripts/
|
||||||
|
__init__.py
|
||||||
|
yourscript.py
|
||||||
|
setup.py
|
||||||
|
|
||||||
In this case instead of using ``py_modules`` in your ``setup.py`` file you
|
In this case instead of using ``py_modules`` in your ``setup.py`` file you
|
||||||
can use ``packages`` and the automatic package finding support of
|
can use ``packages`` and the automatic package finding support of
|
||||||
setuptools. In addition to that it's also recommended to include other
|
setuptools. In addition to that it's also recommended to include other
|
||||||
package data.
|
package data.
|
||||||
|
|
||||||
These would be the modified contents of ``setup.py``::
|
These would be the modified contents of ``setup.py``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='yourpackage',
|
name='yourpackage',
|
||||||
version='0.1',
|
version='0.1.0',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'Click',
|
'Click',
|
||||||
],
|
],
|
||||||
entry_points='''
|
entry_points={
|
||||||
[console_scripts]
|
'console_scripts': [
|
||||||
yourscript=yourpackage.scripts.yourscript:cli
|
'yourscript = yourpackage.scripts.yourscript:cli',
|
||||||
''',
|
],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
285
docs/shell-completion.rst
Normal file
285
docs/shell-completion.rst
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
.. currentmodule:: click.shell_completion
|
||||||
|
|
||||||
|
Shell Completion
|
||||||
|
================
|
||||||
|
|
||||||
|
Click provides tab completion support for Bash (version 4.4 and up),
|
||||||
|
Zsh, and Fish. It is possible to add support for other shells too, and
|
||||||
|
suggestions can be customized at multiple levels.
|
||||||
|
|
||||||
|
Shell completion suggests command names, option names, and values for
|
||||||
|
choice, file, and path parameter types. Options are only listed if at
|
||||||
|
least a dash has been entered. Hidden commands and options are not
|
||||||
|
shown.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ repo <TAB><TAB>
|
||||||
|
clone commit copy delete setuser
|
||||||
|
$ repo clone -<TAB><TAB>
|
||||||
|
--deep --help --rev --shallow -r
|
||||||
|
|
||||||
|
|
||||||
|
Enabling Completion
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Completion is only available if a script is installed and invoked
|
||||||
|
through an entry point, not through the ``python`` command. See
|
||||||
|
:doc:`/setuptools`. Once the executable is installed, calling it with
|
||||||
|
a special environment variable will put Click in completion mode.
|
||||||
|
|
||||||
|
In order for completion to be used, the user needs to register a special
|
||||||
|
function with their shell. The script is different for every shell, and
|
||||||
|
Click will output it when called with ``_{PROG_NAME}_COMPLETE`` set to
|
||||||
|
``{shell}_source``. ``{PROG_NAME}`` is the executable name in uppercase
|
||||||
|
with dashes replaced by underscores. The built-in shells are ``bash``,
|
||||||
|
``zsh``, and ``fish``.
|
||||||
|
|
||||||
|
Provide your users with the following instructions customized to your
|
||||||
|
program name. This uses ``foo-bar`` as an example.
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. group-tab:: Bash
|
||||||
|
|
||||||
|
Add this to ``~/.bashrc``:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
eval "$(_FOO_BAR_COMPLETE=bash_source foo-bar)"
|
||||||
|
|
||||||
|
.. group-tab:: Zsh
|
||||||
|
|
||||||
|
Add this to ``~/.zshrc``:
|
||||||
|
|
||||||
|
.. code-block:: zsh
|
||||||
|
|
||||||
|
eval "$(_FOO_BAR_COMPLETE=zsh_source foo-bar)"
|
||||||
|
|
||||||
|
.. group-tab:: Fish
|
||||||
|
|
||||||
|
Add this to ``~/.config/fish/completions/foo-bar.fish``:
|
||||||
|
|
||||||
|
.. code-block:: fish
|
||||||
|
|
||||||
|
eval (env _FOO_BAR_COMPLETE=fish_source foo-bar)
|
||||||
|
|
||||||
|
This is the same file used for the activation script method
|
||||||
|
below. For Fish it's probably always easier to use that method.
|
||||||
|
|
||||||
|
Using ``eval`` means that the command is invoked and evaluated every
|
||||||
|
time a shell is started, which can delay shell responsiveness. To speed
|
||||||
|
it up, write the generated script to a file, then source that. You can
|
||||||
|
generate the files ahead of time and distribute them with your program
|
||||||
|
to save your users a step.
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. group-tab:: Bash
|
||||||
|
|
||||||
|
Save the script somewhere.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
_FOO_BAR_COMPLETE=bash_source foo-bar > ~/.foo-bar-complete.bash
|
||||||
|
|
||||||
|
Source the file in ``~/.bashrc``.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
. ~/.foo-bar-complete.bash
|
||||||
|
|
||||||
|
.. group-tab:: Zsh
|
||||||
|
|
||||||
|
Save the script somewhere.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
_FOO_BAR_COMPLETE=zsh_source foo-bar > ~/.foo-bar-complete.zsh
|
||||||
|
|
||||||
|
Source the file in ``~/.zshrc``.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
. ~/.foo-bar-complete.zsh
|
||||||
|
|
||||||
|
.. group-tab:: Fish
|
||||||
|
|
||||||
|
Save the script to ``~/.config/fish/completions/foo-bar.fish``:
|
||||||
|
|
||||||
|
.. code-block:: fish
|
||||||
|
|
||||||
|
_FOO_BAR_COMPLETE=fish_source foo-bar > ~/.config/fish/completions/foo-bar.fish
|
||||||
|
|
||||||
|
After modifying the shell config, you need to start a new shell in order
|
||||||
|
for the changes to be loaded.
|
||||||
|
|
||||||
|
|
||||||
|
Custom Type Completion
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
When creating a custom :class:`~click.ParamType`, override its
|
||||||
|
:meth:`~click.ParamType.shell_complete` method to provide shell
|
||||||
|
completion for parameters with the type. The method must return a list
|
||||||
|
of :class:`~CompletionItem` objects. Besides the value, these objects
|
||||||
|
hold metadata that shell support might use. The built-in implementations
|
||||||
|
use ``type`` to indicate special handling for paths, and ``help`` for
|
||||||
|
shells that support showing a help string next to a suggestion.
|
||||||
|
|
||||||
|
In this example, the type will suggest environment variables that start
|
||||||
|
with the incomplete value.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class EnvVarType(ParamType):
|
||||||
|
def shell_complete(self, ctx, param, incomplete):
|
||||||
|
return [
|
||||||
|
CompletionItem(name)
|
||||||
|
for name in os.environ if name.startswith(incomplete)
|
||||||
|
]
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option("--ev", type=EnvVarType())
|
||||||
|
def cli(ev):
|
||||||
|
click.echo(os.environ[ev])
|
||||||
|
|
||||||
|
|
||||||
|
Overriding Value Completion
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Value completions for a parameter can be customized without a custom
|
||||||
|
type by providing a ``shell_complete`` function. The function is used
|
||||||
|
instead of any completion provided by the type. It is passed 3 keyword
|
||||||
|
arguments:
|
||||||
|
|
||||||
|
- ``ctx`` - The current command context.
|
||||||
|
- ``param`` - The current parameter requesting completion.
|
||||||
|
- ``incomplete`` - The partial word that is being completed. May
|
||||||
|
be an empty string if no characters have been entered yet.
|
||||||
|
|
||||||
|
It must return a list of :class:`CompletionItem` objects, or as a
|
||||||
|
shortcut it can return a list of strings.
|
||||||
|
|
||||||
|
In this example, the command will suggest environment variables that
|
||||||
|
start with the incomplete value.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def complete_env_vars(ctx, param, incomplete):
|
||||||
|
return [k for k in os.environ if k.startswith(incomplete)]
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument("name", shell_complete=complete_env_vars)
|
||||||
|
def cli(name):
|
||||||
|
click.echo(f"Name: {name}")
|
||||||
|
click.echo(f"Value: {os.environ[name]}")
|
||||||
|
|
||||||
|
|
||||||
|
Adding Support for a Shell
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Support can be added for shells that do not come built in. Be sure to
|
||||||
|
check PyPI to see if there's already a package that adds support for
|
||||||
|
your shell. This topic is very technical, you'll want to look at Click's
|
||||||
|
source to study the built-in implementations.
|
||||||
|
|
||||||
|
Shell support is provided by subclasses of :class:`ShellComplete`
|
||||||
|
registered with :func:`add_completion_class`. When Click is invoked in
|
||||||
|
completion mode, it calls :meth:`~ShellComplete.source` to output the
|
||||||
|
completion script, or :meth:`~ShellComplete.complete` to output
|
||||||
|
completions. The base class provides default implementations that
|
||||||
|
require implementing some smaller parts.
|
||||||
|
|
||||||
|
First, you'll need to figure out how your shell's completion system
|
||||||
|
works and write a script to integrate it with Click. It must invoke your
|
||||||
|
program with the environment variable ``_{PROG_NAME}_COMPLETE`` set to
|
||||||
|
``{shell}_complete`` and pass the complete args and incomplete value.
|
||||||
|
How it passes those values, and the format of the completion response
|
||||||
|
from Click is up to you.
|
||||||
|
|
||||||
|
In your subclass, set :attr:`~ShellComplete.source_template` to the
|
||||||
|
completion script. The default implementation will perform ``%``
|
||||||
|
formatting with the following variables:
|
||||||
|
|
||||||
|
- ``complete_func`` - A safe name for the completion function defined
|
||||||
|
in the script.
|
||||||
|
- ``complete_var`` - The environment variable name for passing the
|
||||||
|
``{shell}_complete`` instruction.
|
||||||
|
- ``prog_name`` - The name of the executable being completed.
|
||||||
|
|
||||||
|
The example code is for a made up shell "My Shell" or "mysh" for short.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from click.shell_completion import add_completion_class
|
||||||
|
from click.shell_completion import ShellComplete
|
||||||
|
|
||||||
|
_mysh_source = """\
|
||||||
|
%(complete_func)s {
|
||||||
|
response=$(%(complete_var)s=mysh_complete %(prog_name)s)
|
||||||
|
# parse response and set completions somehow
|
||||||
|
}
|
||||||
|
call-on-complete %(prog_name)s %(complete_func)s
|
||||||
|
"""
|
||||||
|
|
||||||
|
@add_completion_class
|
||||||
|
class MyshComplete(ShellComplete):
|
||||||
|
name = "mysh"
|
||||||
|
source_template = _mysh_source
|
||||||
|
|
||||||
|
Next, implement :meth:`~ShellComplete.get_completion_args`. This must
|
||||||
|
get, parse, and return the complete args and incomplete value from the
|
||||||
|
completion script. For example, for the Bash implementation the
|
||||||
|
``COMP_WORDS`` env var contains the command line args as a string, and
|
||||||
|
the ``COMP_CWORD`` env var contains the index of the incomplete arg. The
|
||||||
|
method must return a ``(args, incomplete)`` tuple.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import os
|
||||||
|
from click.parser import split_arg_string
|
||||||
|
|
||||||
|
class MyshComplete(ShellComplete):
|
||||||
|
...
|
||||||
|
|
||||||
|
def get_completion_args(self):
|
||||||
|
args = split_arg_string(os.environ["COMP_WORDS"])
|
||||||
|
|
||||||
|
if os.environ["COMP_PARTIAL"] == "1":
|
||||||
|
incomplete = args.pop()
|
||||||
|
return args, incomplete
|
||||||
|
|
||||||
|
return args, ""
|
||||||
|
|
||||||
|
Finally, implement :meth:`~ShellComplete.format_completion`. This is
|
||||||
|
called to format each :class:`CompletionItem` into a string. For
|
||||||
|
example, the Bash implementation returns ``f"{item.type},{item.value}``
|
||||||
|
(it doesn't support help strings), and the Zsh implementation returns
|
||||||
|
each part separated by a newline, replacing empty help with a ``_``
|
||||||
|
placeholder. This format is entirely up to what you parse with your
|
||||||
|
completion script.
|
||||||
|
|
||||||
|
The ``type`` value is usually ``plain``, but it can be another value
|
||||||
|
that the completion script can switch on. For example, ``file`` or
|
||||||
|
``dir`` can tell the shell to handle path completion, since the shell is
|
||||||
|
better at that than Click.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class MyshComplete(ShellComplete):
|
||||||
|
...
|
||||||
|
|
||||||
|
def format_completion(self, item):
|
||||||
|
return f"{item.type}\t{item.value}"
|
||||||
|
|
||||||
|
With those three things implemented, the new shell support is ready. In
|
||||||
|
case those weren't sufficient, there are more parts that can be
|
||||||
|
overridden, but that probably isn't necessary.
|
||||||
|
|
||||||
|
The activation instructions will again depend on how your shell works.
|
||||||
|
Use the following to generate the completion script, then load it into
|
||||||
|
the shell somehow.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
_FOO_BAR_COMPLETE=mysh_source foo-bar
|
|
@ -30,7 +30,7 @@ data, exit code, and optional exception attached:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument('name')
|
@click.argument('name')
|
||||||
def hello(name):
|
def hello(name):
|
||||||
click.echo('Hello %s!' % name)
|
click.echo(f'Hello {name}!')
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:caption: test_hello.py
|
:caption: test_hello.py
|
||||||
|
@ -54,7 +54,7 @@ For subcommand testing, a subcommand name must be specified in the `args` parame
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.option('--debug/--no-debug', default=False)
|
@click.option('--debug/--no-debug', default=False)
|
||||||
def cli(debug):
|
def cli(debug):
|
||||||
click.echo('Debug mode is %s' % ('on' if debug else 'off'))
|
click.echo(f"Debug mode is {'on' if debug else 'off'}")
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def sync():
|
def sync():
|
||||||
|
@ -112,6 +112,19 @@ current working directory to a new, empty folder.
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert result.output == 'Hello World!\n'
|
assert result.output == 'Hello World!\n'
|
||||||
|
|
||||||
|
Pass ``temp_dir`` to control where the temporary directory is created.
|
||||||
|
The directory will not be removed by Click in this case. This is useful
|
||||||
|
to integrate with a framework like Pytest that manages temporary files.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def test_keep_dir(tmp_path):
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
with runner.isolated_filesystem(temp_dir=tmp_path) as td:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
Input Streams
|
Input Streams
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
@ -126,7 +139,7 @@ stream (stdin). This is very useful for testing prompts, for instance:
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--foo', prompt=True)
|
@click.option('--foo', prompt=True)
|
||||||
def prompt(foo):
|
def prompt(foo):
|
||||||
click.echo('foo=%s' % foo)
|
click.echo(f"foo={foo}")
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:caption: test_prompt.py
|
:caption: test_prompt.py
|
||||||
|
|
112
docs/unicode-support.rst
Normal file
112
docs/unicode-support.rst
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
Unicode Support
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. currentmodule:: click
|
||||||
|
|
||||||
|
Click has to take extra care to support Unicode text in different
|
||||||
|
environments.
|
||||||
|
|
||||||
|
* The command line in Unix is traditionally bytes, not Unicode. While
|
||||||
|
there are encoding hints, there are some situations where this can
|
||||||
|
break. The most common one is SSH connections to machines with
|
||||||
|
different locales.
|
||||||
|
|
||||||
|
Misconfigured environments can cause a wide range of Unicode
|
||||||
|
problems due to the lack of support for roundtripping surrogate
|
||||||
|
escapes. This will not be fixed in Click itself!
|
||||||
|
|
||||||
|
* Standard input and output is opened in text mode by default. Click
|
||||||
|
has to reopen the stream in binary mode in certain situations.
|
||||||
|
Because there is no standard way to do this, it might not always
|
||||||
|
work. Primarily this can become a problem when testing command-line
|
||||||
|
applications.
|
||||||
|
|
||||||
|
This is not supported::
|
||||||
|
|
||||||
|
sys.stdin = io.StringIO('Input here')
|
||||||
|
sys.stdout = io.StringIO()
|
||||||
|
|
||||||
|
Instead you need to do this::
|
||||||
|
|
||||||
|
input = 'Input here'
|
||||||
|
in_stream = io.BytesIO(input.encode('utf-8'))
|
||||||
|
sys.stdin = io.TextIOWrapper(in_stream, encoding='utf-8')
|
||||||
|
out_stream = io.BytesIO()
|
||||||
|
sys.stdout = io.TextIOWrapper(out_stream, encoding='utf-8')
|
||||||
|
|
||||||
|
Remember in that case, you need to use ``out_stream.getvalue()``
|
||||||
|
and not ``sys.stdout.getvalue()`` if you want to access the buffer
|
||||||
|
contents as the wrapper will not forward that method.
|
||||||
|
|
||||||
|
* ``sys.stdin``, ``sys.stdout`` and ``sys.stderr`` are by default
|
||||||
|
text-based. When Click needs a binary stream, it attempts to
|
||||||
|
discover the underlying binary stream.
|
||||||
|
|
||||||
|
* ``sys.argv`` is always text. This means that the native type for
|
||||||
|
input values to the types in Click is Unicode, not bytes.
|
||||||
|
|
||||||
|
This causes problems if the terminal is incorrectly set and Python
|
||||||
|
does not figure out the encoding. In that case, the Unicode string
|
||||||
|
will contain error bytes encoded as surrogate escapes.
|
||||||
|
|
||||||
|
* When dealing with files, Click will always use the Unicode file
|
||||||
|
system API by using the operating system's reported or guessed
|
||||||
|
filesystem encoding. Surrogates are supported for filenames, so it
|
||||||
|
should be possible to open files through the :class:`File` type even
|
||||||
|
if the environment is misconfigured.
|
||||||
|
|
||||||
|
|
||||||
|
Surrogate Handling
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Click does all the Unicode handling in the standard library and is
|
||||||
|
subject to its behavior. Unicode requires extra care. The reason for
|
||||||
|
this is that the encoding detection is done in the interpreter, and on
|
||||||
|
Linux and certain other operating systems, its encoding handling is
|
||||||
|
problematic.
|
||||||
|
|
||||||
|
The biggest source of frustration is that Click scripts invoked by init
|
||||||
|
systems, deployment tools, or cron jobs will refuse to work unless a
|
||||||
|
Unicode locale is exported.
|
||||||
|
|
||||||
|
If Click encounters such an environment it will prevent further
|
||||||
|
execution to force you to set a locale. This is done because Click
|
||||||
|
cannot know about the state of the system once it's invoked and restore
|
||||||
|
the values before Python's Unicode handling kicked in.
|
||||||
|
|
||||||
|
If you see something like this error::
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
RuntimeError: Click will abort further execution because Python was
|
||||||
|
configured to use ASCII as encoding for the environment. Consult
|
||||||
|
https://click.palletsprojects.com/unicode-support/ for mitigation
|
||||||
|
steps.
|
||||||
|
|
||||||
|
You are dealing with an environment where Python thinks you are
|
||||||
|
restricted to ASCII data. The solution to these problems is different
|
||||||
|
depending on which locale your computer is running in.
|
||||||
|
|
||||||
|
For instance, if you have a German Linux machine, you can fix the
|
||||||
|
problem by exporting the locale to ``de_DE.utf-8``::
|
||||||
|
|
||||||
|
export LC_ALL=de_DE.utf-8
|
||||||
|
export LANG=de_DE.utf-8
|
||||||
|
|
||||||
|
If you are on a US machine, ``en_US.utf-8`` is the encoding of choice.
|
||||||
|
On some newer Linux systems, you could also try ``C.UTF-8`` as the
|
||||||
|
locale::
|
||||||
|
|
||||||
|
export LC_ALL=C.UTF-8
|
||||||
|
export LANG=C.UTF-8
|
||||||
|
|
||||||
|
On some systems it was reported that ``UTF-8`` has to be written as
|
||||||
|
``UTF8`` and vice versa. To see which locales are supported you can
|
||||||
|
invoke ``locale -a``.
|
||||||
|
|
||||||
|
You need to export the values before you invoke your Python script.
|
||||||
|
|
||||||
|
In Python 3.7 and later you will no longer get a ``RuntimeError`` in
|
||||||
|
many cases thanks to :pep:`538` and :pep:`540`, which changed the
|
||||||
|
default assumption in unconfigured environments. This doesn't change the
|
||||||
|
general issue that your locale may be misconfigured.
|
|
@ -90,7 +90,7 @@ restored.
|
||||||
If you do require the know which exact commands will be invoked there are
|
If you do require the know which exact commands will be invoked there are
|
||||||
different ways to cope with this. The first one is to let the subcommands
|
different ways to cope with this. The first one is to let the subcommands
|
||||||
all return functions and then to invoke the functions in a
|
all return functions and then to invoke the functions in a
|
||||||
:meth:`Context.resultcallback`.
|
:meth:`Context.result_callback`.
|
||||||
|
|
||||||
|
|
||||||
.. _upgrade-to-2.0:
|
.. _upgrade-to-2.0:
|
||||||
|
|
|
@ -13,9 +13,7 @@ Printing to Stdout
|
||||||
|
|
||||||
The most obvious helper is the :func:`echo` function, which in many ways
|
The most obvious helper is the :func:`echo` function, which in many ways
|
||||||
works like the Python ``print`` statement or function. The main difference is
|
works like the Python ``print`` statement or function. The main difference is
|
||||||
that it works the same in Python 2 and 3, it intelligently detects
|
that it works the same in many different terminal environments.
|
||||||
misconfigured output streams, and it will never fail (except in Python 3; for
|
|
||||||
more information see :ref:`python3-limitations`).
|
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
|
@ -23,10 +21,8 @@ Example::
|
||||||
|
|
||||||
click.echo('Hello World!')
|
click.echo('Hello World!')
|
||||||
|
|
||||||
Most importantly, it can print both Unicode and binary data, unlike the
|
It can output both text and binary data. It will emit a trailing newline
|
||||||
built-in ``print`` function in Python 3, which cannot output any bytes. It
|
by default, which needs to be suppressed by passing ``nl=False``::
|
||||||
will, however, emit a trailing newline by default, which needs to be
|
|
||||||
suppressed by passing ``nl=False``::
|
|
||||||
|
|
||||||
click.echo(b'\xe2\x98\x83', nl=False)
|
click.echo(b'\xe2\x98\x83', nl=False)
|
||||||
|
|
||||||
|
@ -34,19 +30,17 @@ Last but not least :func:`echo` uses click's intelligent internal output
|
||||||
streams to stdout and stderr which support unicode output on the Windows
|
streams to stdout and stderr which support unicode output on the Windows
|
||||||
console. This means for as long as you are using `click.echo` you can
|
console. This means for as long as you are using `click.echo` you can
|
||||||
output unicode characters (there are some limitations on the default font
|
output unicode characters (there are some limitations on the default font
|
||||||
with regards to which characters can be displayed). This functionality is
|
with regards to which characters can be displayed).
|
||||||
new in Click 6.0.
|
|
||||||
|
|
||||||
.. versionadded:: 6.0
|
.. versionadded:: 6.0
|
||||||
|
|
||||||
Click now emulates output streams on Windows to support unicode to the
|
Click emulates output streams on Windows to support unicode to the
|
||||||
Windows console through separate APIs. For more information see
|
Windows console through separate APIs. For more information see
|
||||||
:doc:`wincmd`.
|
:doc:`wincmd`.
|
||||||
|
|
||||||
.. versionadded:: 3.0
|
.. versionadded:: 3.0
|
||||||
|
|
||||||
Starting with Click 3.0 you can also easily print to standard error by
|
You can also easily print to standard error by passing ``err=True``::
|
||||||
passing ``err=True``::
|
|
||||||
|
|
||||||
click.echo('Hello World!', err=True)
|
click.echo('Hello World!', err=True)
|
||||||
|
|
||||||
|
@ -58,11 +52,8 @@ ANSI Colors
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
Starting with Click 2.0, the :func:`echo` function gained extra
|
The :func:`echo` function supports ANSI colors and styles. On Windows
|
||||||
functionality to deal with ANSI colors and styles. Note that on Windows,
|
this uses `colorama`_.
|
||||||
this functionality is only available if `colorama`_ is installed. If it
|
|
||||||
is installed, then ANSI codes are intelligently handled. Note that in Python
|
|
||||||
2, the echo function doesn't parse color code information from bytearrays.
|
|
||||||
|
|
||||||
Primarily this means that:
|
Primarily this means that:
|
||||||
|
|
||||||
|
@ -73,12 +64,8 @@ Primarily this means that:
|
||||||
that colors will work on Windows the same way they do on other
|
that colors will work on Windows the same way they do on other
|
||||||
operating systems.
|
operating systems.
|
||||||
|
|
||||||
Note for `colorama` support: Click will automatically detect when `colorama`
|
On Windows, Click uses colorama without calling ``colorama.init()``. You
|
||||||
is available and use it. Do *not* call ``colorama.init()``!
|
can still call that in your code, but it's not required for Click.
|
||||||
|
|
||||||
To install `colorama`, run this command::
|
|
||||||
|
|
||||||
$ pip install colorama
|
|
||||||
|
|
||||||
For styling a string, the :func:`style` function can be used::
|
For styling a string, the :func:`style` function can be used::
|
||||||
|
|
||||||
|
@ -112,15 +99,14 @@ Example:
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
def less():
|
def less():
|
||||||
click.echo_via_pager('\n'.join('Line %d' % idx
|
click.echo_via_pager("\n".join(f"Line {idx}" for idx in range(200)))
|
||||||
for idx in range(200)))
|
|
||||||
|
|
||||||
If you want to use the pager for a lot of text, especially if generating everything in advance would take a lot of time, you can pass a generator (or generator function) instead of a string:
|
If you want to use the pager for a lot of text, especially if generating everything in advance would take a lot of time, you can pass a generator (or generator function) instead of a string:
|
||||||
|
|
||||||
.. click:example::
|
.. click:example::
|
||||||
def _generate_output():
|
def _generate_output():
|
||||||
for idx in range(50000):
|
for idx in range(50000):
|
||||||
yield "Line %d\n" % idx
|
yield f"Line {idx}\n"
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
def less():
|
def less():
|
||||||
|
@ -253,9 +239,7 @@ Printing Filenames
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
Because filenames might not be Unicode, formatting them can be a bit
|
Because filenames might not be Unicode, formatting them can be a bit
|
||||||
tricky. Generally, this is easier in Python 2 than on 3, as you can just
|
tricky.
|
||||||
write the bytes to stdout with the ``print`` function, but in Python 3, you
|
|
||||||
will always need to operate in Unicode.
|
|
||||||
|
|
||||||
The way this works with click is through the :func:`format_filename`
|
The way this works with click is through the :func:`format_filename`
|
||||||
function. It does a best-effort conversion of the filename to Unicode and
|
function. It does a best-effort conversion of the filename to Unicode and
|
||||||
|
@ -264,7 +248,7 @@ context of a full Unicode string.
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
click.echo('Path: %s' % click.format_filename(b'foo.txt'))
|
click.echo(f"Path: {click.format_filename(b'foo.txt')}")
|
||||||
|
|
||||||
|
|
||||||
Standard Streams
|
Standard Streams
|
||||||
|
@ -281,8 +265,7 @@ Because of this, click provides the :func:`get_binary_stream` and
|
||||||
different Python versions and for a wide variety of terminal configurations.
|
different Python versions and for a wide variety of terminal configurations.
|
||||||
|
|
||||||
The end result is that these functions will always return a functional
|
The end result is that these functions will always return a functional
|
||||||
stream object (except in very odd cases in Python 3; see
|
stream object (except in very odd cases; see :doc:`/unicode-support`).
|
||||||
:ref:`python3-limitations`).
|
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
|
@ -349,20 +332,25 @@ Example usage::
|
||||||
rv = {}
|
rv = {}
|
||||||
for section in parser.sections():
|
for section in parser.sections():
|
||||||
for key, value in parser.items(section):
|
for key, value in parser.items(section):
|
||||||
rv['%s.%s' % (section, key)] = value
|
rv[f"{section}.{key}"] = value
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
Showing Progress Bars
|
Showing Progress Bars
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
|
|
||||||
Sometimes, you have command line scripts that need to process a lot of data,
|
Sometimes, you have command line scripts that need to process a lot of data,
|
||||||
but you want to quickly show the user some progress about how long that
|
but you want to quickly show the user some progress about how long that
|
||||||
will take. Click supports simple progress bar rendering for that through
|
will take. Click supports simple progress bar rendering for that through
|
||||||
the :func:`progressbar` function.
|
the :func:`progressbar` function.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you find that you have requirements beyond what Click's progress
|
||||||
|
bar supports, try using `tqdm`_.
|
||||||
|
|
||||||
|
.. _tqdm: https://tqdm.github.io/
|
||||||
|
|
||||||
The basic usage is very simple: the idea is that you have an iterable that
|
The basic usage is very simple: the idea is that you have an iterable that
|
||||||
you want to operate on. For each item in the iterable it might take some
|
you want to operate on. For each item in the iterable it might take some
|
||||||
time to do processing. So say you have a loop like this::
|
time to do processing. So say you have a loop like this::
|
||||||
|
@ -396,7 +384,7 @@ loop. So code like this will render correctly::
|
||||||
|
|
||||||
with click.progressbar([1, 2, 3]) as bar:
|
with click.progressbar([1, 2, 3]) as bar:
|
||||||
for x in bar:
|
for x in bar:
|
||||||
print('sleep({})...'.format(x))
|
print(f"sleep({x})...")
|
||||||
time.sleep(x)
|
time.sleep(x)
|
||||||
|
|
||||||
Another useful feature is to associate a label with the progress bar which
|
Another useful feature is to associate a label with the progress bar which
|
||||||
|
|
25
docs/why.rst
25
docs/why.rst
|
@ -7,16 +7,15 @@ why does Click exist?
|
||||||
This question is easy to answer: because there is not a single command
|
This question is easy to answer: because there is not a single command
|
||||||
line utility for Python out there which ticks the following boxes:
|
line utility for Python out there which ticks the following boxes:
|
||||||
|
|
||||||
* is lazily composable without restrictions
|
* Is lazily composable without restrictions.
|
||||||
* supports implementation of Unix/POSIX command line conventions
|
* Supports implementation of Unix/POSIX command line conventions.
|
||||||
* supports loading values from environment variables out of the box
|
* Supports loading values from environment variables out of the box.
|
||||||
* support for prompting of custom values
|
* Support for prompting of custom values.
|
||||||
* is fully nestable and composable
|
* Is fully nestable and composable.
|
||||||
* works the same in Python 2 and 3
|
* Supports file handling out of the box.
|
||||||
* supports file handling out of the box
|
* Comes with useful common helpers (getting terminal dimensions,
|
||||||
* comes with useful common helpers (getting terminal dimensions,
|
|
||||||
ANSI colors, fetching direct keyboard input, screen clearing,
|
ANSI colors, fetching direct keyboard input, screen clearing,
|
||||||
finding config paths, launching apps and editors, etc.)
|
finding config paths, launching apps and editors, etc.).
|
||||||
|
|
||||||
There are many alternatives to Click; the obvious ones are ``optparse``
|
There are many alternatives to Click; the obvious ones are ``optparse``
|
||||||
and ``argparse`` from the standard library. Have a look to see if something
|
and ``argparse`` from the standard library. Have a look to see if something
|
||||||
|
@ -47,15 +46,15 @@ Why not Argparse?
|
||||||
|
|
||||||
Click is internally based on ``optparse`` instead of ``argparse``. This
|
Click is internally based on ``optparse`` instead of ``argparse``. This
|
||||||
is an implementation detail that a user does not have to be concerned
|
is an implementation detail that a user does not have to be concerned
|
||||||
with. Click is not based on argparse because it has some behaviors that
|
with. Click is not based on ``argparse`` because it has some behaviors that
|
||||||
make handling arbitrary command line interfaces hard:
|
make handling arbitrary command line interfaces hard:
|
||||||
|
|
||||||
* argparse has built-in behavior to guess if something is an
|
* ``argparse`` has built-in behavior to guess if something is an
|
||||||
argument or an option. This becomes a problem when dealing with
|
argument or an option. This becomes a problem when dealing with
|
||||||
incomplete command lines; the behaviour becomes unpredictable
|
incomplete command lines; the behaviour becomes unpredictable
|
||||||
without full knowledge of a command line. This goes against Click's
|
without full knowledge of a command line. This goes against Click's
|
||||||
ambitions of dispatching to subparsers.
|
ambitions of dispatching to subparsers.
|
||||||
* argparse does not support disabling interspersed arguments. Without
|
* ``argparse`` does not support disabling interspersed arguments. Without
|
||||||
this feature, it's not possible to safely implement Click's nested
|
this feature, it's not possible to safely implement Click's nested
|
||||||
parsing.
|
parsing.
|
||||||
|
|
||||||
|
@ -135,7 +134,7 @@ Why No Auto Correction?
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
The question came up why Click does not auto correct parameters given that
|
The question came up why Click does not auto correct parameters given that
|
||||||
even optparse and argparse support automatic expansion of long arguments.
|
even optparse and ``argparse`` support automatic expansion of long arguments.
|
||||||
The reason for this is that it's a liability for backwards compatibility.
|
The reason for this is that it's a liability for backwards compatibility.
|
||||||
If people start relying on automatically modified parameters and someone
|
If people start relying on automatically modified parameters and someone
|
||||||
adds a new parameter in the future, the script might stop working. These
|
adds a new parameter in the future, the script might stop working. These
|
||||||
|
|
|
@ -3,11 +3,7 @@ Windows Console Notes
|
||||||
|
|
||||||
.. versionadded:: 6.0
|
.. versionadded:: 6.0
|
||||||
|
|
||||||
Until Click 6.0 there are various bugs and limitations with using Click on
|
Click emulates output streams on Windows to support unicode to the
|
||||||
a Windows console. Most notably the decoding of command line arguments
|
|
||||||
was performed with the wrong encoding on Python 2 and on all versions of
|
|
||||||
Python output of unicode characters was impossible. Starting with Click
|
|
||||||
6.0 we now emulate output streams on Windows to support unicode to the
|
|
||||||
Windows console through separate APIs and we perform different decoding of
|
Windows console through separate APIs and we perform different decoding of
|
||||||
parameters.
|
parameters.
|
||||||
|
|
||||||
|
@ -22,18 +18,10 @@ performed to the type expected value as late as possible. This has some
|
||||||
advantages as it allows us to accept the data in the most appropriate form
|
advantages as it allows us to accept the data in the most appropriate form
|
||||||
for the operating system and Python version.
|
for the operating system and Python version.
|
||||||
|
|
||||||
For instance paths are left as bytes on Python 2 unless you explicitly
|
|
||||||
tell it otherwise.
|
|
||||||
|
|
||||||
This caused some problems on Windows where initially the wrong encoding
|
This caused some problems on Windows where initially the wrong encoding
|
||||||
was used and garbage ended up in your input data. We not only fixed the
|
was used and garbage ended up in your input data. We not only fixed the
|
||||||
encoding part, but we also now extract unicode parameters from `sys.argv`.
|
encoding part, but we also now extract unicode parameters from `sys.argv`.
|
||||||
|
|
||||||
This means that on Python 2 under Windows, the arguments processed will
|
|
||||||
*most likely* be of unicode nature and not bytes. This was something that
|
|
||||||
previously did not really happen unless you explicitly passed in unicode
|
|
||||||
parameters so your custom types need to be aware of this.
|
|
||||||
|
|
||||||
There is also another limitation with this: if `sys.argv` was modified
|
There is also another limitation with this: if `sys.argv` was modified
|
||||||
prior to invoking a click handler, we have to fall back to the regular
|
prior to invoking a click handler, we have to fall back to the regular
|
||||||
byte input in which case not all unicode values are available but only a
|
byte input in which case not all unicode values are available but only a
|
||||||
|
@ -55,10 +43,6 @@ stream will also use ``utf-16-le`` as internal encoding. However there is
|
||||||
some hackery going on that the underlying raw IO buffer is still bypassing
|
some hackery going on that the underlying raw IO buffer is still bypassing
|
||||||
the unicode APIs and byte output through an indirection is still possible.
|
the unicode APIs and byte output through an indirection is still possible.
|
||||||
|
|
||||||
This hackery is used on both Python 2 and Python 3 as neither version of
|
|
||||||
Python has native support for cmd.exe with unicode characters. There are
|
|
||||||
some limitations you need to be aware of:
|
|
||||||
|
|
||||||
* This unicode support is limited to ``click.echo``, ``click.prompt`` as
|
* This unicode support is limited to ``click.echo``, ``click.prompt`` as
|
||||||
well as ``click.get_text_stream``.
|
well as ``click.get_text_stream``.
|
||||||
* Depending on if unicode values or byte strings are passed the control
|
* Depending on if unicode values or byte strings are passed the control
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
try:
|
|
||||||
import configparser
|
|
||||||
except ImportError:
|
|
||||||
import ConfigParser as configparser
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
class Config(object):
|
|
||||||
"""The config in this example only holds aliases."""
|
"""The config in this example only holds aliases."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -53,7 +49,7 @@ class AliasedGroup(click.Group):
|
||||||
# will create the config object is missing.
|
# will create the config object is missing.
|
||||||
cfg = ctx.ensure_object(Config)
|
cfg = ctx.ensure_object(Config)
|
||||||
|
|
||||||
# Step three: lookup an explicit command aliase in the config
|
# Step three: look up an explicit command alias in the config
|
||||||
if cmd_name in cfg.aliases:
|
if cmd_name in cfg.aliases:
|
||||||
actual_cmd = cfg.aliases[cmd_name]
|
actual_cmd = cfg.aliases[cmd_name]
|
||||||
return click.Group.get_command(self, ctx, actual_cmd)
|
return click.Group.get_command(self, ctx, actual_cmd)
|
||||||
|
@ -69,7 +65,12 @@ class AliasedGroup(click.Group):
|
||||||
return None
|
return None
|
||||||
elif len(matches) == 1:
|
elif len(matches) == 1:
|
||||||
return click.Group.get_command(self, ctx, matches[0])
|
return click.Group.get_command(self, ctx, matches[0])
|
||||||
ctx.fail("Too many matches: {}".format(", ".join(sorted(matches))))
|
ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")
|
||||||
|
|
||||||
|
def resolve_command(self, ctx, args):
|
||||||
|
# always return the command's name, not the alias
|
||||||
|
_, cmd, args = super().resolve_command(ctx, args)
|
||||||
|
return cmd.name, cmd, args
|
||||||
|
|
||||||
|
|
||||||
def read_config(ctx, param, value):
|
def read_config(ctx, param, value):
|
||||||
|
@ -125,7 +126,7 @@ def commit():
|
||||||
@pass_config
|
@pass_config
|
||||||
def status(config):
|
def status(config):
|
||||||
"""Shows the status."""
|
"""Shows the status."""
|
||||||
click.echo("Status for {}".format(config.path))
|
click.echo(f"Status for {config.path}")
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
@ -139,4 +140,4 @@ def alias(config, alias_, cmd, config_file):
|
||||||
"""Adds an alias to the specified configuration file."""
|
"""Adds an alias to the specified configuration file."""
|
||||||
config.add_alias(alias_, cmd)
|
config.add_alias(alias_, cmd)
|
||||||
config.write_config(config_file)
|
config.write_config(config_file)
|
||||||
click.echo("Added '{}' as alias for '{}'".format(alias_, cmd))
|
click.echo(f"Added '{alias_}' as alias for '{cmd}'")
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
$ bashcompletion
|
|
||||||
|
|
||||||
bashcompletion is a simple example of an application that
|
|
||||||
tries to autocomplete commands, arguments and options.
|
|
||||||
|
|
||||||
This example requires Click 2.0 or higher.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
$ pip install --editable .
|
|
||||||
$ eval "$(_BASHCOMPLETION_COMPLETE=source bashcompletion)"
|
|
||||||
$ bashcompletion --help
|
|
|
@ -1,45 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
import click
|
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
|
||||||
def cli():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_env_vars(ctx, args, incomplete):
|
|
||||||
# Completions returned as strings do not have a description displayed.
|
|
||||||
for key in os.environ.keys():
|
|
||||||
if incomplete in key:
|
|
||||||
yield key
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command(help="A command to print environment variables")
|
|
||||||
@click.argument("envvar", type=click.STRING, autocompletion=get_env_vars)
|
|
||||||
def cmd1(envvar):
|
|
||||||
click.echo("Environment variable: {}".format(envvar))
|
|
||||||
click.echo("Value: {}".format(os.environ[envvar]))
|
|
||||||
|
|
||||||
|
|
||||||
@click.group(help="A group that holds a subcommand")
|
|
||||||
def group():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def list_users(ctx, args, incomplete):
|
|
||||||
# You can generate completions with descriptions by returning
|
|
||||||
# tuples in the form (completion, description).
|
|
||||||
users = [("bob", "butcher"), ("alice", "baker"), ("jerry", "candlestick maker")]
|
|
||||||
# Ths will allow completion matches based on matches within the
|
|
||||||
# description string too!
|
|
||||||
return [user for user in users if incomplete in user[0] or incomplete in user[1]]
|
|
||||||
|
|
||||||
|
|
||||||
@group.command(help="Choose a user")
|
|
||||||
@click.argument("user", type=click.STRING, autocompletion=list_users)
|
|
||||||
def subcmd(user):
|
|
||||||
click.echo("Chosen user is {}".format(user))
|
|
||||||
|
|
||||||
|
|
||||||
cli.add_command(group)
|
|
|
@ -3,7 +3,7 @@ $ colors_
|
||||||
colors is a simple example that shows how you can
|
colors is a simple example that shows how you can
|
||||||
colorize text.
|
colorize text.
|
||||||
|
|
||||||
For this to work on Windows, colorama is required.
|
Uses colorama on Windows.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
|
|
|
@ -23,22 +23,17 @@ all_colors = (
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
def cli():
|
def cli():
|
||||||
"""This script prints some colors. If colorama is installed this will
|
"""This script prints some colors. It will also automatically remove
|
||||||
also work on Windows. It will also automatically remove all ANSI
|
all ANSI styles if data is piped into a file.
|
||||||
styles if data is piped into a file.
|
|
||||||
|
|
||||||
Give it a try!
|
Give it a try!
|
||||||
"""
|
"""
|
||||||
for color in all_colors:
|
for color in all_colors:
|
||||||
click.echo(click.style("I am colored {}".format(color), fg=color))
|
click.echo(click.style(f"I am colored {color}", fg=color))
|
||||||
for color in all_colors:
|
for color in all_colors:
|
||||||
click.echo(
|
click.echo(click.style(f"I am colored {color} and bold", fg=color, bold=True))
|
||||||
click.style("I am colored {} and bold".format(color), fg=color, bold=True)
|
|
||||||
)
|
|
||||||
for color in all_colors:
|
for color in all_colors:
|
||||||
click.echo(
|
click.echo(click.style(f"I am reverse colored {color}", fg=color, reverse=True))
|
||||||
click.style("I am reverse colored {}".format(color), fg=color, reverse=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
click.echo(click.style("I am blinking", blink=True))
|
click.echo(click.style("I am blinking", blink=True))
|
||||||
click.echo(click.style("I am underlined", underline=True))
|
click.echo(click.style("I am underlined", underline=True))
|
||||||
|
|
|
@ -5,11 +5,7 @@ setup(
|
||||||
version="1.0",
|
version="1.0",
|
||||||
py_modules=["colors"],
|
py_modules=["colors"],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=[
|
install_requires=["click"],
|
||||||
"click",
|
|
||||||
# Colorama is only required for Windows.
|
|
||||||
"colorama",
|
|
||||||
],
|
|
||||||
entry_points="""
|
entry_points="""
|
||||||
[console_scripts]
|
[console_scripts]
|
||||||
colors=colors:cli
|
colors=colors:cli
|
||||||
|
|
28
examples/completion/README
Normal file
28
examples/completion/README
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
$ completion
|
||||||
|
============
|
||||||
|
|
||||||
|
Demonstrates Click's shell completion support.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install --editable .
|
||||||
|
|
||||||
|
For Bash:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
eval "$(_COMPLETION_COMPLETE=bash_source completion)"
|
||||||
|
|
||||||
|
For Zsh:
|
||||||
|
|
||||||
|
.. code-block:: zsh
|
||||||
|
|
||||||
|
eval "$(_COMPLETION_COMPLETE=zsh_source completion)"
|
||||||
|
|
||||||
|
For Fish:
|
||||||
|
|
||||||
|
.. code-block:: fish
|
||||||
|
|
||||||
|
eval (env _COMPLETION_COMPLETE=fish_source completion)
|
||||||
|
|
||||||
|
Now press tab (maybe twice) after typing something to see completions.
|
56
examples/completion/completion.py
Normal file
56
examples/completion/completion.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
import click
|
||||||
|
from click.shell_completion import CompletionItem
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
def cli():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("--dir", type=click.Path(file_okay=False))
|
||||||
|
def ls(dir):
|
||||||
|
click.echo("\n".join(os.listdir(dir)))
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_vars(ctx, param, incomplete):
|
||||||
|
# Returning a list of values is a shortcut to returning a list of
|
||||||
|
# CompletionItem(value).
|
||||||
|
return [k for k in os.environ if incomplete in k]
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(help="A command to print environment variables")
|
||||||
|
@click.argument("envvar", shell_complete=get_env_vars)
|
||||||
|
def show_env(envvar):
|
||||||
|
click.echo(f"Environment variable: {envvar}")
|
||||||
|
click.echo(f"Value: {os.environ[envvar]}")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.group(help="A group that holds a subcommand")
|
||||||
|
def group():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def list_users(ctx, param, incomplete):
|
||||||
|
# You can generate completions with help strings by returning a list
|
||||||
|
# of CompletionItem. You can match on whatever you want, including
|
||||||
|
# the help.
|
||||||
|
items = [("bob", "butcher"), ("alice", "baker"), ("jerry", "candlestick maker")]
|
||||||
|
out = []
|
||||||
|
|
||||||
|
for value, help in items:
|
||||||
|
if incomplete in value or incomplete in help:
|
||||||
|
out.append(CompletionItem(value, help=help))
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@group.command(help="Choose a user")
|
||||||
|
@click.argument("user", shell_complete=list_users)
|
||||||
|
def select_user(user):
|
||||||
|
click.echo(f"Chosen user is {user}")
|
||||||
|
|
||||||
|
|
||||||
|
cli.add_command(group)
|
|
@ -1,13 +1,13 @@
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="click-example-bashcompletion",
|
name="click-example-completion",
|
||||||
version="1.0",
|
version="1.0",
|
||||||
py_modules=["bashcompletion"],
|
py_modules=["completion"],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=["click"],
|
install_requires=["click"],
|
||||||
entry_points="""
|
entry_points="""
|
||||||
[console_scripts]
|
[console_scripts]
|
||||||
bashcompletion=bashcompletion:cli
|
completion=completion:cli
|
||||||
""",
|
""",
|
||||||
)
|
)
|
|
@ -7,7 +7,7 @@ import click
|
||||||
CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX")
|
CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX")
|
||||||
|
|
||||||
|
|
||||||
class Environment(object):
|
class Environment:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.verbose = False
|
self.verbose = False
|
||||||
self.home = os.getcwd()
|
self.home = os.getcwd()
|
||||||
|
@ -39,11 +39,7 @@ class ComplexCLI(click.MultiCommand):
|
||||||
|
|
||||||
def get_command(self, ctx, name):
|
def get_command(self, ctx, name):
|
||||||
try:
|
try:
|
||||||
if sys.version_info[0] == 2:
|
mod = __import__(f"complex.commands.cmd_{name}", None, None, ["cli"])
|
||||||
name = name.encode("ascii", "replace")
|
|
||||||
mod = __import__(
|
|
||||||
"complex.commands.cmd_{}".format(name), None, None, ["cli"]
|
|
||||||
)
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return
|
return
|
||||||
return mod.cli
|
return mod.cli
|
||||||
|
|
|
@ -10,4 +10,4 @@ def cli(ctx, path):
|
||||||
"""Initializes a repository."""
|
"""Initializes a repository."""
|
||||||
if path is None:
|
if path is None:
|
||||||
path = ctx.home
|
path = ctx.home
|
||||||
ctx.log("Initialized the repository in %s", click.format_filename(path))
|
ctx.log(f"Initialized the repository in {click.format_filename(path)}")
|
||||||
|
|
|
@ -20,7 +20,7 @@ def cli():
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@cli.resultcallback()
|
@cli.result_callback()
|
||||||
def process_commands(processors):
|
def process_commands(processors):
|
||||||
"""This result callback is invoked with an iterable of all the chained
|
"""This result callback is invoked with an iterable of all the chained
|
||||||
subcommands. As in this example each subcommand returns a function
|
subcommands. As in this example each subcommand returns a function
|
||||||
|
@ -60,10 +60,8 @@ def generator(f):
|
||||||
|
|
||||||
@processor
|
@processor
|
||||||
def new_func(stream, *args, **kwargs):
|
def new_func(stream, *args, **kwargs):
|
||||||
for item in stream:
|
yield from stream
|
||||||
yield item
|
yield from f(*args, **kwargs)
|
||||||
for item in f(*args, **kwargs):
|
|
||||||
yield item
|
|
||||||
|
|
||||||
return update_wrapper(new_func, f)
|
return update_wrapper(new_func, f)
|
||||||
|
|
||||||
|
@ -89,7 +87,7 @@ def open_cmd(images):
|
||||||
"""
|
"""
|
||||||
for image in images:
|
for image in images:
|
||||||
try:
|
try:
|
||||||
click.echo("Opening '{}'".format(image))
|
click.echo(f"Opening '{image}'")
|
||||||
if image == "-":
|
if image == "-":
|
||||||
img = Image.open(click.get_binary_stdin())
|
img = Image.open(click.get_binary_stdin())
|
||||||
img.filename = "-"
|
img.filename = "-"
|
||||||
|
@ -97,7 +95,7 @@ def open_cmd(images):
|
||||||
img = Image.open(image)
|
img = Image.open(image)
|
||||||
yield img
|
yield img
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
click.echo("Could not open image '{}': {}".format(image, e), err=True)
|
click.echo(f"Could not open image '{image}': {e}", err=True)
|
||||||
|
|
||||||
|
|
||||||
@cli.command("save")
|
@cli.command("save")
|
||||||
|
@ -114,12 +112,10 @@ def save_cmd(images, filename):
|
||||||
for idx, image in enumerate(images):
|
for idx, image in enumerate(images):
|
||||||
try:
|
try:
|
||||||
fn = filename.format(idx + 1)
|
fn = filename.format(idx + 1)
|
||||||
click.echo("Saving '{}' as '{}'".format(image.filename, fn))
|
click.echo(f"Saving '{image.filename}' as '{fn}'")
|
||||||
yield image.save(fn)
|
yield image.save(fn)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
click.echo(
|
click.echo(f"Could not save image '{image.filename}': {e}", err=True)
|
||||||
"Could not save image '{}': {}".format(image.filename, e), err=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command("display")
|
@cli.command("display")
|
||||||
|
@ -127,7 +123,7 @@ def save_cmd(images, filename):
|
||||||
def display_cmd(images):
|
def display_cmd(images):
|
||||||
"""Opens all images in an image viewer."""
|
"""Opens all images in an image viewer."""
|
||||||
for image in images:
|
for image in images:
|
||||||
click.echo("Displaying '{}'".format(image.filename))
|
click.echo(f"Displaying '{image.filename}'")
|
||||||
image.show()
|
image.show()
|
||||||
yield image
|
yield image
|
||||||
|
|
||||||
|
@ -142,7 +138,7 @@ def resize_cmd(images, width, height):
|
||||||
"""
|
"""
|
||||||
for image in images:
|
for image in images:
|
||||||
w, h = (width or image.size[0], height or image.size[1])
|
w, h = (width or image.size[0], height or image.size[1])
|
||||||
click.echo("Resizing '{}' to {}x{}".format(image.filename, w, h))
|
click.echo(f"Resizing '{image.filename}' to {w}x{h}")
|
||||||
image.thumbnail((w, h))
|
image.thumbnail((w, h))
|
||||||
yield image
|
yield image
|
||||||
|
|
||||||
|
@ -160,7 +156,7 @@ def crop_cmd(images, border):
|
||||||
if border is not None:
|
if border is not None:
|
||||||
for idx, val in enumerate(box):
|
for idx, val in enumerate(box):
|
||||||
box[idx] = max(0, val - border)
|
box[idx] = max(0, val - border)
|
||||||
click.echo("Cropping '{}' by {}px".format(image.filename, border))
|
click.echo(f"Cropping '{image.filename}' by {border}px")
|
||||||
yield copy_filename(image.crop(box), image)
|
yield copy_filename(image.crop(box), image)
|
||||||
else:
|
else:
|
||||||
yield image
|
yield image
|
||||||
|
@ -176,7 +172,7 @@ def convert_rotation(ctx, param, value):
|
||||||
return (Image.ROTATE_180, 180)
|
return (Image.ROTATE_180, 180)
|
||||||
if value in ("-90", "270", "l", "left"):
|
if value in ("-90", "270", "l", "left"):
|
||||||
return (Image.ROTATE_270, 270)
|
return (Image.ROTATE_270, 270)
|
||||||
raise click.BadParameter("invalid rotation '{}'".format(value))
|
raise click.BadParameter(f"invalid rotation '{value}'")
|
||||||
|
|
||||||
|
|
||||||
def convert_flip(ctx, param, value):
|
def convert_flip(ctx, param, value):
|
||||||
|
@ -187,7 +183,7 @@ def convert_flip(ctx, param, value):
|
||||||
return (Image.FLIP_LEFT_RIGHT, "left to right")
|
return (Image.FLIP_LEFT_RIGHT, "left to right")
|
||||||
if value in ("tb", "topbottom", "upsidedown", "ud"):
|
if value in ("tb", "topbottom", "upsidedown", "ud"):
|
||||||
return (Image.FLIP_LEFT_RIGHT, "top to bottom")
|
return (Image.FLIP_LEFT_RIGHT, "top to bottom")
|
||||||
raise click.BadParameter("invalid flip '{}'".format(value))
|
raise click.BadParameter(f"invalid flip '{value}'")
|
||||||
|
|
||||||
|
|
||||||
@cli.command("transpose")
|
@cli.command("transpose")
|
||||||
|
@ -201,11 +197,11 @@ def transpose_cmd(images, rotate, flip):
|
||||||
for image in images:
|
for image in images:
|
||||||
if rotate is not None:
|
if rotate is not None:
|
||||||
mode, degrees = rotate
|
mode, degrees = rotate
|
||||||
click.echo("Rotate '{}' by {}deg".format(image.filename, degrees))
|
click.echo(f"Rotate '{image.filename}' by {degrees}deg")
|
||||||
image = copy_filename(image.transpose(mode), image)
|
image = copy_filename(image.transpose(mode), image)
|
||||||
if flip is not None:
|
if flip is not None:
|
||||||
mode, direction = flip
|
mode, direction = flip
|
||||||
click.echo("Flip '{}' {}".format(image.filename, direction))
|
click.echo(f"Flip '{image.filename}' {direction}")
|
||||||
image = copy_filename(image.transpose(mode), image)
|
image = copy_filename(image.transpose(mode), image)
|
||||||
yield image
|
yield image
|
||||||
|
|
||||||
|
@ -217,7 +213,7 @@ def blur_cmd(images, radius):
|
||||||
"""Applies gaussian blur."""
|
"""Applies gaussian blur."""
|
||||||
blur = ImageFilter.GaussianBlur(radius)
|
blur = ImageFilter.GaussianBlur(radius)
|
||||||
for image in images:
|
for image in images:
|
||||||
click.echo("Blurring '{}' by {}px".format(image.filename, radius))
|
click.echo(f"Blurring '{image.filename}' by {radius}px")
|
||||||
yield copy_filename(image.filter(blur), image)
|
yield copy_filename(image.filter(blur), image)
|
||||||
|
|
||||||
|
|
||||||
|
@ -234,9 +230,8 @@ def smoothen_cmd(images, iterations):
|
||||||
"""Applies a smoothening filter."""
|
"""Applies a smoothening filter."""
|
||||||
for image in images:
|
for image in images:
|
||||||
click.echo(
|
click.echo(
|
||||||
"Smoothening '{}' {} time{}".format(
|
f"Smoothening {image.filename!r} {iterations}"
|
||||||
image.filename, iterations, "s" if iterations != 1 else ""
|
f" time{'s' if iterations != 1 else ''}"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for _ in range(iterations):
|
for _ in range(iterations):
|
||||||
image = copy_filename(image.filter(ImageFilter.BLUR), image)
|
image = copy_filename(image.filter(ImageFilter.BLUR), image)
|
||||||
|
@ -248,7 +243,7 @@ def smoothen_cmd(images, iterations):
|
||||||
def emboss_cmd(images):
|
def emboss_cmd(images):
|
||||||
"""Embosses an image."""
|
"""Embosses an image."""
|
||||||
for image in images:
|
for image in images:
|
||||||
click.echo("Embossing '{}'".format(image.filename))
|
click.echo(f"Embossing '{image.filename}'")
|
||||||
yield copy_filename(image.filter(ImageFilter.EMBOSS), image)
|
yield copy_filename(image.filter(ImageFilter.EMBOSS), image)
|
||||||
|
|
||||||
|
|
||||||
|
@ -260,7 +255,7 @@ def emboss_cmd(images):
|
||||||
def sharpen_cmd(images, factor):
|
def sharpen_cmd(images, factor):
|
||||||
"""Sharpens an image."""
|
"""Sharpens an image."""
|
||||||
for image in images:
|
for image in images:
|
||||||
click.echo("Sharpen '{}' by {}".format(image.filename, factor))
|
click.echo(f"Sharpen '{image.filename}' by {factor}")
|
||||||
enhancer = ImageEnhance.Sharpness(image)
|
enhancer = ImageEnhance.Sharpness(image)
|
||||||
yield copy_filename(enhancer.enhance(max(1.0, factor)), image)
|
yield copy_filename(enhancer.enhance(max(1.0, factor)), image)
|
||||||
|
|
||||||
|
@ -282,13 +277,12 @@ def paste_cmd(images, left, right):
|
||||||
yield image
|
yield image
|
||||||
return
|
return
|
||||||
|
|
||||||
click.echo("Paste '{}' on '{}'".format(to_paste.filename, image.filename))
|
click.echo(f"Paste '{to_paste.filename}' on '{image.filename}'")
|
||||||
mask = None
|
mask = None
|
||||||
if to_paste.mode == "RGBA" or "transparency" in to_paste.info:
|
if to_paste.mode == "RGBA" or "transparency" in to_paste.info:
|
||||||
mask = to_paste
|
mask = to_paste
|
||||||
image.paste(to_paste, (left, right), mask)
|
image.paste(to_paste, (left, right), mask)
|
||||||
image.filename += "+{}".format(to_paste.filename)
|
image.filename += f"+{to_paste.filename}"
|
||||||
yield image
|
yield image
|
||||||
|
|
||||||
for image in imageiter:
|
yield from imageiter
|
||||||
yield image
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ def ship():
|
||||||
@click.argument("name")
|
@click.argument("name")
|
||||||
def ship_new(name):
|
def ship_new(name):
|
||||||
"""Creates a new ship."""
|
"""Creates a new ship."""
|
||||||
click.echo("Created ship {}".format(name))
|
click.echo(f"Created ship {name}")
|
||||||
|
|
||||||
|
|
||||||
@ship.command("move")
|
@ship.command("move")
|
||||||
|
@ -31,7 +31,7 @@ def ship_new(name):
|
||||||
@click.option("--speed", metavar="KN", default=10, help="Speed in knots.")
|
@click.option("--speed", metavar="KN", default=10, help="Speed in knots.")
|
||||||
def ship_move(ship, x, y, speed):
|
def ship_move(ship, x, y, speed):
|
||||||
"""Moves SHIP to the new location X,Y."""
|
"""Moves SHIP to the new location X,Y."""
|
||||||
click.echo("Moving ship {} to {},{} with speed {}".format(ship, x, y, speed))
|
click.echo(f"Moving ship {ship} to {x},{y} with speed {speed}")
|
||||||
|
|
||||||
|
|
||||||
@ship.command("shoot")
|
@ship.command("shoot")
|
||||||
|
@ -40,7 +40,7 @@ def ship_move(ship, x, y, speed):
|
||||||
@click.argument("y", type=float)
|
@click.argument("y", type=float)
|
||||||
def ship_shoot(ship, x, y):
|
def ship_shoot(ship, x, y):
|
||||||
"""Makes SHIP fire to X,Y."""
|
"""Makes SHIP fire to X,Y."""
|
||||||
click.echo("Ship {} fires to {},{}".format(ship, x, y))
|
click.echo(f"Ship {ship} fires to {x},{y}")
|
||||||
|
|
||||||
|
|
||||||
@cli.group("mine")
|
@cli.group("mine")
|
||||||
|
@ -61,7 +61,7 @@ def mine():
|
||||||
@click.option("ty", "--drifting", flag_value="drifting", help="Drifting mine.")
|
@click.option("ty", "--drifting", flag_value="drifting", help="Drifting mine.")
|
||||||
def mine_set(x, y, ty):
|
def mine_set(x, y, ty):
|
||||||
"""Sets a mine at a specific coordinate."""
|
"""Sets a mine at a specific coordinate."""
|
||||||
click.echo("Set {} mine at {},{}".format(ty, x, y))
|
click.echo(f"Set {ty} mine at {x},{y}")
|
||||||
|
|
||||||
|
|
||||||
@mine.command("remove")
|
@mine.command("remove")
|
||||||
|
@ -69,4 +69,4 @@ def mine_set(x, y, ty):
|
||||||
@click.argument("y", type=float)
|
@click.argument("y", type=float)
|
||||||
def mine_remove(x, y):
|
def mine_remove(x, y):
|
||||||
"""Removes a mine at a specific coordinate."""
|
"""Removes a mine at a specific coordinate."""
|
||||||
click.echo("Removed mine at {},{}".format(x, y))
|
click.echo(f"Removed mine at {x},{y}")
|
||||||
|
|
|
@ -5,7 +5,7 @@ import sys
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
class Repo(object):
|
class Repo:
|
||||||
def __init__(self, home):
|
def __init__(self, home):
|
||||||
self.home = home
|
self.home = home
|
||||||
self.config = {}
|
self.config = {}
|
||||||
|
@ -14,10 +14,10 @@ class Repo(object):
|
||||||
def set_config(self, key, value):
|
def set_config(self, key, value):
|
||||||
self.config[key] = value
|
self.config[key] = value
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
click.echo(" config[{}] = {}".format(key, value), file=sys.stderr)
|
click.echo(f" config[{key}] = {value}", file=sys.stderr)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Repo {}>".format(self.home)
|
return f"<Repo {self.home}>"
|
||||||
|
|
||||||
|
|
||||||
pass_repo = click.make_pass_decorator(Repo)
|
pass_repo = click.make_pass_decorator(Repo)
|
||||||
|
@ -78,11 +78,11 @@ def clone(repo, src, dest, shallow, rev):
|
||||||
"""
|
"""
|
||||||
if dest is None:
|
if dest is None:
|
||||||
dest = posixpath.split(src)[-1] or "."
|
dest = posixpath.split(src)[-1] or "."
|
||||||
click.echo("Cloning repo {} to {}".format(src, os.path.abspath(dest)))
|
click.echo(f"Cloning repo {src} to {os.path.basename(dest)}")
|
||||||
repo.home = dest
|
repo.home = dest
|
||||||
if shallow:
|
if shallow:
|
||||||
click.echo("Making shallow checkout")
|
click.echo("Making shallow checkout")
|
||||||
click.echo("Checking out revision {}".format(rev))
|
click.echo(f"Checking out revision {rev}")
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
@ -93,7 +93,7 @@ def delete(repo):
|
||||||
|
|
||||||
This will throw away the current repository.
|
This will throw away the current repository.
|
||||||
"""
|
"""
|
||||||
click.echo("Destroying repo {}".format(repo.home))
|
click.echo(f"Destroying repo {repo.home}")
|
||||||
click.echo("Deleted!")
|
click.echo("Deleted!")
|
||||||
|
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ def commit(repo, files, message):
|
||||||
marker = "# Files to be committed:"
|
marker = "# Files to be committed:"
|
||||||
hint = ["", "", marker, "#"]
|
hint = ["", "", marker, "#"]
|
||||||
for file in files:
|
for file in files:
|
||||||
hint.append("# U {}".format(file))
|
hint.append(f"# U {file}")
|
||||||
message = click.edit("\n".join(hint))
|
message = click.edit("\n".join(hint))
|
||||||
if message is None:
|
if message is None:
|
||||||
click.echo("Aborted!")
|
click.echo("Aborted!")
|
||||||
|
@ -147,8 +147,8 @@ def commit(repo, files, message):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
msg = "\n".join(message)
|
msg = "\n".join(message)
|
||||||
click.echo("Files to be committed: {}".format(files))
|
click.echo(f"Files to be committed: {files}")
|
||||||
click.echo("Commit message:\n{}".format(msg))
|
click.echo(f"Commit message:\n{msg}")
|
||||||
|
|
||||||
|
|
||||||
@cli.command(short_help="Copies files.")
|
@cli.command(short_help="Copies files.")
|
||||||
|
@ -163,4 +163,4 @@ def copy(repo, src, dst, force):
|
||||||
files from SRC to DST.
|
files from SRC to DST.
|
||||||
"""
|
"""
|
||||||
for fn in src:
|
for fn in src:
|
||||||
click.echo("Copy from {} -> {}".format(fn, dst))
|
click.echo(f"Copy from {fn} -> {dst}")
|
||||||
|
|
|
@ -5,11 +5,7 @@ setup(
|
||||||
version="1.0",
|
version="1.0",
|
||||||
py_modules=["termui"],
|
py_modules=["termui"],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=[
|
install_requires=["click"],
|
||||||
"click",
|
|
||||||
# Colorama is only required for Windows.
|
|
||||||
"colorama",
|
|
||||||
],
|
|
||||||
entry_points="""
|
entry_points="""
|
||||||
[console_scripts]
|
[console_scripts]
|
||||||
termui=termui:cli
|
termui=termui:cli
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# coding: utf-8
|
|
||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
@ -16,8 +15,8 @@ def cli():
|
||||||
def colordemo():
|
def colordemo():
|
||||||
"""Demonstrates ANSI color support."""
|
"""Demonstrates ANSI color support."""
|
||||||
for color in "red", "green", "blue":
|
for color in "red", "green", "blue":
|
||||||
click.echo(click.style("I am colored {}".format(color), fg=color))
|
click.echo(click.style(f"I am colored {color}", fg=color))
|
||||||
click.echo(click.style("I am background colored {}".format(color), bg=color))
|
click.echo(click.style(f"I am background colored {color}", bg=color))
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
@ -25,7 +24,7 @@ def pager():
|
||||||
"""Demonstrates using the pager."""
|
"""Demonstrates using the pager."""
|
||||||
lines = []
|
lines = []
|
||||||
for x in range(200):
|
for x in range(200):
|
||||||
lines.append("{}. Hello World!".format(click.style(str(x), fg="green")))
|
lines.append(f"{click.style(str(x), fg='green')}. Hello World!")
|
||||||
click.echo_via_pager("\n".join(lines))
|
click.echo_via_pager("\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,7 +55,7 @@ def progress(count):
|
||||||
|
|
||||||
def show_item(item):
|
def show_item(item):
|
||||||
if item is not None:
|
if item is not None:
|
||||||
return "Item #{}".format(item)
|
return f"Item #{item}"
|
||||||
|
|
||||||
with click.progressbar(
|
with click.progressbar(
|
||||||
filter(items),
|
filter(items),
|
||||||
|
@ -71,7 +70,7 @@ def progress(count):
|
||||||
length=count,
|
length=count,
|
||||||
label="Counting",
|
label="Counting",
|
||||||
bar_template="%(label)s %(bar)s | %(info)s",
|
bar_template="%(label)s %(bar)s | %(info)s",
|
||||||
fill_char=click.style(u"█", fg="cyan"),
|
fill_char=click.style("█", fg="cyan"),
|
||||||
empty_char=" ",
|
empty_char=" ",
|
||||||
) as bar:
|
) as bar:
|
||||||
for item in bar:
|
for item in bar:
|
||||||
|
@ -94,7 +93,7 @@ def progress(count):
|
||||||
length=count,
|
length=count,
|
||||||
show_percent=False,
|
show_percent=False,
|
||||||
label="Slowing progress bar",
|
label="Slowing progress bar",
|
||||||
fill_char=click.style(u"█", fg="green"),
|
fill_char=click.style("█", fg="green"),
|
||||||
) as bar:
|
) as bar:
|
||||||
for item in steps:
|
for item in steps:
|
||||||
time.sleep(item)
|
time.sleep(item)
|
||||||
|
@ -119,13 +118,13 @@ def locate(url):
|
||||||
def edit():
|
def edit():
|
||||||
"""Opens an editor with some text in it."""
|
"""Opens an editor with some text in it."""
|
||||||
MARKER = "# Everything below is ignored\n"
|
MARKER = "# Everything below is ignored\n"
|
||||||
message = click.edit("\n\n{}".format(MARKER))
|
message = click.edit(f"\n\n{MARKER}")
|
||||||
if message is not None:
|
if message is not None:
|
||||||
msg = message.split(MARKER, 1)[0].rstrip("\n")
|
msg = message.split(MARKER, 1)[0].rstrip("\n")
|
||||||
if not msg:
|
if not msg:
|
||||||
click.echo("Empty message!")
|
click.echo("Empty message!")
|
||||||
else:
|
else:
|
||||||
click.echo("Message:\n{}".format(msg))
|
click.echo(f"Message:\n{msg}")
|
||||||
else:
|
else:
|
||||||
click.echo("You did not enter anything!")
|
click.echo("You did not enter anything!")
|
||||||
|
|
||||||
|
@ -146,7 +145,7 @@ def pause():
|
||||||
def menu():
|
def menu():
|
||||||
"""Shows a simple menu."""
|
"""Shows a simple menu."""
|
||||||
menu = "main"
|
menu = "main"
|
||||||
while 1:
|
while True:
|
||||||
if menu == "main":
|
if menu == "main":
|
||||||
click.echo("Main menu:")
|
click.echo("Main menu:")
|
||||||
click.echo(" d: debug menu")
|
click.echo(" d: debug menu")
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import click
|
from urllib import parse as urlparse
|
||||||
|
|
||||||
try:
|
import click
|
||||||
from urllib import parse as urlparse
|
|
||||||
except ImportError:
|
|
||||||
import urlparse
|
|
||||||
|
|
||||||
|
|
||||||
def validate_count(ctx, param, value):
|
def validate_count(ctx, param, value):
|
||||||
|
@ -20,8 +17,7 @@ class URL(click.ParamType):
|
||||||
value = urlparse.urlparse(value)
|
value = urlparse.urlparse(value)
|
||||||
if value.scheme not in ("http", "https"):
|
if value.scheme not in ("http", "https"):
|
||||||
self.fail(
|
self.fail(
|
||||||
"invalid URL scheme ({}). Only HTTP URLs are"
|
f"invalid URL scheme ({value.scheme}). Only HTTP URLs are allowed",
|
||||||
" allowed".format(value.scheme),
|
|
||||||
param,
|
param,
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
|
@ -47,6 +43,6 @@ def cli(count, foo, url):
|
||||||
'If a value is provided it needs to be the value "wat".',
|
'If a value is provided it needs to be the value "wat".',
|
||||||
param_hint=["--foo"],
|
param_hint=["--foo"],
|
||||||
)
|
)
|
||||||
click.echo("count: {}".format(count))
|
click.echo(f"count: {count}")
|
||||||
click.echo("foo: {}".format(foo))
|
click.echo(f"foo: {foo}")
|
||||||
click.echo("url: {!r}".format(url))
|
click.echo(f"url: {url!r}")
|
||||||
|
|
6
requirements/dev.in
Normal file
6
requirements/dev.in
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
-r docs.in
|
||||||
|
-r tests.in
|
||||||
|
-r typing.in
|
||||||
|
pip-tools
|
||||||
|
pre-commit
|
||||||
|
tox
|
143
requirements/dev.txt
Normal file
143
requirements/dev.txt
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile
|
||||||
|
# To update, run:
|
||||||
|
#
|
||||||
|
# pip-compile requirements/dev.in
|
||||||
|
#
|
||||||
|
alabaster==0.7.12
|
||||||
|
# via sphinx
|
||||||
|
attrs==21.2.0
|
||||||
|
# via pytest
|
||||||
|
babel==2.9.1
|
||||||
|
# via sphinx
|
||||||
|
backports.entry-points-selectable==1.1.0
|
||||||
|
# via virtualenv
|
||||||
|
certifi==2021.5.30
|
||||||
|
# via requests
|
||||||
|
cfgv==3.3.1
|
||||||
|
# via pre-commit
|
||||||
|
charset-normalizer==2.0.6
|
||||||
|
# via requests
|
||||||
|
click==8.0.1
|
||||||
|
# via pip-tools
|
||||||
|
distlib==0.3.3
|
||||||
|
# via virtualenv
|
||||||
|
docutils==0.16
|
||||||
|
# via
|
||||||
|
# sphinx
|
||||||
|
# sphinx-tabs
|
||||||
|
filelock==3.3.0
|
||||||
|
# via
|
||||||
|
# tox
|
||||||
|
# virtualenv
|
||||||
|
identify==2.3.0
|
||||||
|
# via pre-commit
|
||||||
|
idna==3.2
|
||||||
|
# via requests
|
||||||
|
imagesize==1.2.0
|
||||||
|
# via sphinx
|
||||||
|
iniconfig==1.1.1
|
||||||
|
# via pytest
|
||||||
|
jinja2==3.0.2
|
||||||
|
# via sphinx
|
||||||
|
markupsafe==2.0.1
|
||||||
|
# via jinja2
|
||||||
|
mypy-extensions==0.4.3
|
||||||
|
# via mypy
|
||||||
|
mypy==0.910
|
||||||
|
# via -r requirements/typing.in
|
||||||
|
nodeenv==1.6.0
|
||||||
|
# via pre-commit
|
||||||
|
packaging==21.0
|
||||||
|
# via
|
||||||
|
# pallets-sphinx-themes
|
||||||
|
# pytest
|
||||||
|
# sphinx
|
||||||
|
# tox
|
||||||
|
pallets-sphinx-themes==2.0.1
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
pep517==0.11.0
|
||||||
|
# via pip-tools
|
||||||
|
pip-tools==6.3.0
|
||||||
|
# via -r requirements/dev.in
|
||||||
|
platformdirs==2.4.0
|
||||||
|
# via virtualenv
|
||||||
|
pluggy==1.0.0
|
||||||
|
# via
|
||||||
|
# pytest
|
||||||
|
# tox
|
||||||
|
pre-commit==2.15.0
|
||||||
|
# via -r requirements/dev.in
|
||||||
|
py==1.10.0
|
||||||
|
# via
|
||||||
|
# pytest
|
||||||
|
# tox
|
||||||
|
pygments==2.10.0
|
||||||
|
# via
|
||||||
|
# sphinx
|
||||||
|
# sphinx-tabs
|
||||||
|
pyparsing==2.4.7
|
||||||
|
# via packaging
|
||||||
|
pytest==6.2.5
|
||||||
|
# via -r requirements/tests.in
|
||||||
|
pytz==2021.3
|
||||||
|
# via babel
|
||||||
|
pyyaml==5.4.1
|
||||||
|
# via pre-commit
|
||||||
|
requests==2.26.0
|
||||||
|
# via sphinx
|
||||||
|
six==1.16.0
|
||||||
|
# via
|
||||||
|
# tox
|
||||||
|
# virtualenv
|
||||||
|
snowballstemmer==2.1.0
|
||||||
|
# via sphinx
|
||||||
|
sphinx-issues==1.2.0
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinx-tabs==3.2.0
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinx==4.2.0
|
||||||
|
# via
|
||||||
|
# -r requirements/docs.in
|
||||||
|
# pallets-sphinx-themes
|
||||||
|
# sphinx-issues
|
||||||
|
# sphinx-tabs
|
||||||
|
# sphinxcontrib-log-cabinet
|
||||||
|
sphinxcontrib-applehelp==1.0.2
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-devhelp==1.0.2
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-htmlhelp==2.0.0
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-jsmath==1.0.1
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-log-cabinet==1.0.1
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinxcontrib-qthelp==1.0.3
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-serializinghtml==1.1.5
|
||||||
|
# via sphinx
|
||||||
|
toml==0.10.2
|
||||||
|
# via
|
||||||
|
# mypy
|
||||||
|
# pre-commit
|
||||||
|
# pytest
|
||||||
|
# tox
|
||||||
|
tomli==1.2.1
|
||||||
|
# via pep517
|
||||||
|
tox==3.24.4
|
||||||
|
# via -r requirements/dev.in
|
||||||
|
typing-extensions==3.10.0.2
|
||||||
|
# via mypy
|
||||||
|
urllib3==1.26.7
|
||||||
|
# via requests
|
||||||
|
virtualenv==20.8.1
|
||||||
|
# via
|
||||||
|
# pre-commit
|
||||||
|
# tox
|
||||||
|
wheel==0.37.0
|
||||||
|
# via pip-tools
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
# pip
|
||||||
|
# setuptools
|
5
requirements/docs.in
Normal file
5
requirements/docs.in
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
Pallets-Sphinx-Themes
|
||||||
|
Sphinx
|
||||||
|
sphinx-issues
|
||||||
|
sphinxcontrib-log-cabinet
|
||||||
|
sphinx-tabs
|
74
requirements/docs.txt
Normal file
74
requirements/docs.txt
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile
|
||||||
|
# To update, run:
|
||||||
|
#
|
||||||
|
# pip-compile requirements/docs.in
|
||||||
|
#
|
||||||
|
alabaster==0.7.12
|
||||||
|
# via sphinx
|
||||||
|
babel==2.9.1
|
||||||
|
# via sphinx
|
||||||
|
certifi==2021.5.30
|
||||||
|
# via requests
|
||||||
|
charset-normalizer==2.0.6
|
||||||
|
# via requests
|
||||||
|
docutils==0.16
|
||||||
|
# via
|
||||||
|
# sphinx
|
||||||
|
# sphinx-tabs
|
||||||
|
idna==3.2
|
||||||
|
# via requests
|
||||||
|
imagesize==1.2.0
|
||||||
|
# via sphinx
|
||||||
|
jinja2==3.0.2
|
||||||
|
# via sphinx
|
||||||
|
markupsafe==2.0.1
|
||||||
|
# via jinja2
|
||||||
|
packaging==21.0
|
||||||
|
# via
|
||||||
|
# pallets-sphinx-themes
|
||||||
|
# sphinx
|
||||||
|
pallets-sphinx-themes==2.0.1
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
pygments==2.10.0
|
||||||
|
# via
|
||||||
|
# sphinx
|
||||||
|
# sphinx-tabs
|
||||||
|
pyparsing==2.4.7
|
||||||
|
# via packaging
|
||||||
|
pytz==2021.3
|
||||||
|
# via babel
|
||||||
|
requests==2.26.0
|
||||||
|
# via sphinx
|
||||||
|
snowballstemmer==2.1.0
|
||||||
|
# via sphinx
|
||||||
|
sphinx-issues==1.2.0
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinx-tabs==3.2.0
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinx==4.2.0
|
||||||
|
# via
|
||||||
|
# -r requirements/docs.in
|
||||||
|
# pallets-sphinx-themes
|
||||||
|
# sphinx-issues
|
||||||
|
# sphinx-tabs
|
||||||
|
# sphinxcontrib-log-cabinet
|
||||||
|
sphinxcontrib-applehelp==1.0.2
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-devhelp==1.0.2
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-htmlhelp==2.0.0
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-jsmath==1.0.1
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-log-cabinet==1.0.1
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinxcontrib-qthelp==1.0.3
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-serializinghtml==1.1.5
|
||||||
|
# via sphinx
|
||||||
|
urllib3==1.26.7
|
||||||
|
# via requests
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
# setuptools
|
1
requirements/tests.in
Normal file
1
requirements/tests.in
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pytest
|
22
requirements/tests.txt
Normal file
22
requirements/tests.txt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile
|
||||||
|
# To update, run:
|
||||||
|
#
|
||||||
|
# pip-compile requirements/tests.in
|
||||||
|
#
|
||||||
|
attrs==21.2.0
|
||||||
|
# via pytest
|
||||||
|
iniconfig==1.1.1
|
||||||
|
# via pytest
|
||||||
|
packaging==21.0
|
||||||
|
# via pytest
|
||||||
|
pluggy==1.0.0
|
||||||
|
# via pytest
|
||||||
|
py==1.10.0
|
||||||
|
# via pytest
|
||||||
|
pyparsing==2.4.7
|
||||||
|
# via packaging
|
||||||
|
pytest==6.2.5
|
||||||
|
# via -r requirements/tests.in
|
||||||
|
toml==0.10.2
|
||||||
|
# via pytest
|
1
requirements/typing.in
Normal file
1
requirements/typing.in
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mypy
|
14
requirements/typing.txt
Normal file
14
requirements/typing.txt
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile
|
||||||
|
# To update, run:
|
||||||
|
#
|
||||||
|
# pip-compile requirements/typing.in
|
||||||
|
#
|
||||||
|
mypy-extensions==0.4.3
|
||||||
|
# via mypy
|
||||||
|
mypy==0.910
|
||||||
|
# via -r requirements/typing.in
|
||||||
|
toml==0.10.2
|
||||||
|
# via mypy
|
||||||
|
typing-extensions==3.10.0.2
|
||||||
|
# via mypy
|
97
setup.cfg
97
setup.cfg
|
@ -1,37 +1,100 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
license_file = LICENSE.rst
|
name = click
|
||||||
|
version = attr: click.__version__
|
||||||
|
url = https://palletsprojects.com/p/click/
|
||||||
|
project_urls =
|
||||||
|
Donate = https://palletsprojects.com/donate
|
||||||
|
Documentation = https://click.palletsprojects.com/
|
||||||
|
Changes = https://click.palletsprojects.com/changes/
|
||||||
|
Source Code = https://github.com/pallets/click/
|
||||||
|
Issue Tracker = https://github.com/pallets/click/issues/
|
||||||
|
Twitter = https://twitter.com/PalletsTeam
|
||||||
|
Chat = https://discord.gg/pallets
|
||||||
|
license = BSD-3-Clause
|
||||||
|
license_files = LICENSE.rst
|
||||||
|
author = Armin Ronacher
|
||||||
|
author_email = armin.ronacher@active-4.com
|
||||||
|
maintainer = Pallets
|
||||||
|
maintainer_email = contact@palletsprojects.com
|
||||||
|
description = Composable command line interface toolkit
|
||||||
|
long_description = file: README.rst
|
||||||
|
long_description_content_type = text/x-rst
|
||||||
|
classifiers =
|
||||||
|
Development Status :: 5 - Production/Stable
|
||||||
|
Intended Audience :: Developers
|
||||||
|
License :: OSI Approved :: BSD License
|
||||||
|
Operating System :: OS Independent
|
||||||
|
Programming Language :: Python
|
||||||
|
|
||||||
[bdist_wheel]
|
[options]
|
||||||
universal = 1
|
packages = find:
|
||||||
|
package_dir = = src
|
||||||
|
include_package_data = true
|
||||||
|
python_requires = >= 3.6
|
||||||
|
# Dependencies are in setup.py for GitHub's dependency graph.
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
where = src
|
||||||
|
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
testpaths = tests
|
testpaths = tests
|
||||||
filterwarnings =
|
filterwarnings =
|
||||||
error
|
error
|
||||||
|
|
||||||
[coverage:run]
|
[coverage:run]
|
||||||
branch = True
|
branch = true
|
||||||
source =
|
source =
|
||||||
src
|
click
|
||||||
tests
|
tests
|
||||||
|
|
||||||
[coverage:paths]
|
[coverage:paths]
|
||||||
source =
|
source =
|
||||||
click
|
click
|
||||||
*/site-packages
|
*/site-packages
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
|
# B = bugbear
|
||||||
|
# E = pycodestyle errors
|
||||||
|
# F = flake8 pyflakes
|
||||||
|
# W = pycodestyle warnings
|
||||||
|
# B9 = bugbear opinions,
|
||||||
|
# ISC = implicit str concat
|
||||||
select = B, E, F, W, B9, ISC
|
select = B, E, F, W, B9, ISC
|
||||||
ignore =
|
ignore =
|
||||||
E203
|
# slice notation whitespace, invalid
|
||||||
E501
|
E203
|
||||||
E722
|
# line length, handled by bugbear B950
|
||||||
W503
|
E501
|
||||||
|
# bare except, handled by bugbear B001
|
||||||
|
E722
|
||||||
|
# bin op line break, invalid
|
||||||
|
W503
|
||||||
|
# up to 88 allowed by bugbear B950
|
||||||
max-line-length = 80
|
max-line-length = 80
|
||||||
per-file-ignores =
|
per-file-ignores =
|
||||||
src/click/__init__.py: F401
|
# __init__ module exports names
|
||||||
|
src/click/__init__.py: F401
|
||||||
|
|
||||||
[egg_info]
|
[mypy]
|
||||||
tag_build =
|
files = src/click
|
||||||
tag_date = 0
|
python_version = 3.6
|
||||||
|
disallow_subclassing_any = True
|
||||||
|
disallow_untyped_calls = True
|
||||||
|
disallow_untyped_defs = True
|
||||||
|
disallow_incomplete_defs = True
|
||||||
|
check_untyped_defs = True
|
||||||
|
no_implicit_optional = True
|
||||||
|
local_partial_types = True
|
||||||
|
no_implicit_reexport = True
|
||||||
|
strict_equality = True
|
||||||
|
warn_redundant_casts = True
|
||||||
|
warn_unused_configs = True
|
||||||
|
warn_unused_ignores = True
|
||||||
|
warn_return_any = True
|
||||||
|
warn_unreachable = True
|
||||||
|
|
||||||
|
[mypy-colorama.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-importlib_metadata.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
37
setup.py
37
setup.py
|
@ -1,40 +1,9 @@
|
||||||
import io
|
|
||||||
import re
|
|
||||||
|
|
||||||
from setuptools import find_packages
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
with io.open("README.rst", "rt", encoding="utf8") as f:
|
|
||||||
readme = f.read()
|
|
||||||
|
|
||||||
with io.open("src/click/__init__.py", "rt", encoding="utf8") as f:
|
|
||||||
version = re.search(r'__version__ = "(.*?)"', f.read()).group(1)
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="click",
|
name="click",
|
||||||
version=version,
|
install_requires=[
|
||||||
url="https://palletsprojects.com/p/click/",
|
"colorama; platform_system == 'Windows'",
|
||||||
project_urls={
|
"importlib-metadata; python_version < '3.8'",
|
||||||
"Documentation": "https://click.palletsprojects.com/",
|
|
||||||
"Code": "https://github.com/pallets/click",
|
|
||||||
"Issue tracker": "https://github.com/pallets/click/issues",
|
|
||||||
},
|
|
||||||
license="BSD-3-Clause",
|
|
||||||
maintainer="Pallets",
|
|
||||||
maintainer_email="contact@palletsprojects.com",
|
|
||||||
description="Composable command line interface toolkit",
|
|
||||||
long_description=readme,
|
|
||||||
packages=find_packages("src"),
|
|
||||||
package_dir={"": "src"},
|
|
||||||
include_package_data=True,
|
|
||||||
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
|
|
||||||
classifiers=[
|
|
||||||
"Development Status :: 5 - Production/Stable",
|
|
||||||
"Intended Audience :: Developers",
|
|
||||||
"License :: OSI Approved :: BSD License",
|
|
||||||
"Operating System :: OS Independent",
|
|
||||||
"Programming Language :: Python",
|
|
||||||
"Programming Language :: Python :: 2",
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
Metadata-Version: 1.2
|
|
||||||
Name: click
|
|
||||||
Version: 7.1.2
|
|
||||||
Summary: Composable command line interface toolkit
|
|
||||||
Home-page: https://palletsprojects.com/p/click/
|
|
||||||
Maintainer: Pallets
|
|
||||||
Maintainer-email: contact@palletsprojects.com
|
|
||||||
License: BSD-3-Clause
|
|
||||||
Project-URL: Documentation, https://click.palletsprojects.com/
|
|
||||||
Project-URL: Code, https://github.com/pallets/click
|
|
||||||
Project-URL: Issue tracker, https://github.com/pallets/click/issues
|
|
||||||
Description: \$ click\_
|
|
||||||
==========
|
|
||||||
|
|
||||||
Click is a Python package for creating beautiful command line interfaces
|
|
||||||
in a composable way with as little code as necessary. It's the "Command
|
|
||||||
Line Interface Creation Kit". It's highly configurable but comes with
|
|
||||||
sensible defaults out of the box.
|
|
||||||
|
|
||||||
It aims to make the process of writing command line tools quick and fun
|
|
||||||
while also preventing any frustration caused by the inability to
|
|
||||||
implement an intended CLI API.
|
|
||||||
|
|
||||||
Click in three points:
|
|
||||||
|
|
||||||
- Arbitrary nesting of commands
|
|
||||||
- Automatic help page generation
|
|
||||||
- Supports lazy loading of subcommands at runtime
|
|
||||||
|
|
||||||
|
|
||||||
Installing
|
|
||||||
----------
|
|
||||||
|
|
||||||
Install and update using `pip`_:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
$ pip install -U click
|
|
||||||
|
|
||||||
.. _pip: https://pip.pypa.io/en/stable/quickstart/
|
|
||||||
|
|
||||||
|
|
||||||
A Simple Example
|
|
||||||
----------------
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import click
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option("--count", default=1, help="Number of greetings.")
|
|
||||||
@click.option("--name", prompt="Your name", help="The person to greet.")
|
|
||||||
def hello(count, name):
|
|
||||||
"""Simple program that greets NAME for a total of COUNT times."""
|
|
||||||
for _ in range(count):
|
|
||||||
click.echo(f"Hello, {name}!")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
hello()
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
$ python hello.py --count=3
|
|
||||||
Your name: Click
|
|
||||||
Hello, Click!
|
|
||||||
Hello, Click!
|
|
||||||
Hello, Click!
|
|
||||||
|
|
||||||
|
|
||||||
Donate
|
|
||||||
------
|
|
||||||
|
|
||||||
The Pallets organization develops and supports Click and other popular
|
|
||||||
packages. In order to grow the community of contributors and users, and
|
|
||||||
allow the maintainers to devote more time to the projects, `please
|
|
||||||
donate today`_.
|
|
||||||
|
|
||||||
.. _please donate today: https://palletsprojects.com/donate
|
|
||||||
|
|
||||||
|
|
||||||
Links
|
|
||||||
-----
|
|
||||||
|
|
||||||
- Website: https://palletsprojects.com/p/click/
|
|
||||||
- Documentation: https://click.palletsprojects.com/
|
|
||||||
- Releases: https://pypi.org/project/click/
|
|
||||||
- Code: https://github.com/pallets/click
|
|
||||||
- Issue tracker: https://github.com/pallets/click/issues
|
|
||||||
- Test status: https://dev.azure.com/pallets/click/_build
|
|
||||||
- Official chat: https://discord.gg/t6rrQZH
|
|
||||||
|
|
||||||
Platform: UNKNOWN
|
|
||||||
Classifier: Development Status :: 5 - Production/Stable
|
|
||||||
Classifier: Intended Audience :: Developers
|
|
||||||
Classifier: License :: OSI Approved :: BSD License
|
|
||||||
Classifier: Operating System :: OS Independent
|
|
||||||
Classifier: Programming Language :: Python
|
|
||||||
Classifier: Programming Language :: Python :: 2
|
|
||||||
Classifier: Programming Language :: Python :: 3
|
|
||||||
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
|
|
|
@ -1,114 +0,0 @@
|
||||||
CHANGES.rst
|
|
||||||
LICENSE.rst
|
|
||||||
MANIFEST.in
|
|
||||||
README.rst
|
|
||||||
setup.cfg
|
|
||||||
setup.py
|
|
||||||
tox.ini
|
|
||||||
artwork/logo.svg
|
|
||||||
docs/Makefile
|
|
||||||
docs/advanced.rst
|
|
||||||
docs/api.rst
|
|
||||||
docs/arguments.rst
|
|
||||||
docs/bashcomplete.rst
|
|
||||||
docs/changelog.rst
|
|
||||||
docs/commands.rst
|
|
||||||
docs/complex.rst
|
|
||||||
docs/conf.py
|
|
||||||
docs/contrib.rst
|
|
||||||
docs/documentation.rst
|
|
||||||
docs/exceptions.rst
|
|
||||||
docs/index.rst
|
|
||||||
docs/license.rst
|
|
||||||
docs/make.bat
|
|
||||||
docs/options.rst
|
|
||||||
docs/parameters.rst
|
|
||||||
docs/prompts.rst
|
|
||||||
docs/python3.rst
|
|
||||||
docs/quickstart.rst
|
|
||||||
docs/requirements.txt
|
|
||||||
docs/setuptools.rst
|
|
||||||
docs/testing.rst
|
|
||||||
docs/upgrading.rst
|
|
||||||
docs/utils.rst
|
|
||||||
docs/why.rst
|
|
||||||
docs/wincmd.rst
|
|
||||||
docs/_static/click-icon.png
|
|
||||||
docs/_static/click-logo-sidebar.png
|
|
||||||
docs/_static/click-logo.png
|
|
||||||
examples/README
|
|
||||||
examples/aliases/README
|
|
||||||
examples/aliases/aliases.ini
|
|
||||||
examples/aliases/aliases.py
|
|
||||||
examples/aliases/setup.py
|
|
||||||
examples/bashcompletion/README
|
|
||||||
examples/bashcompletion/bashcompletion.py
|
|
||||||
examples/bashcompletion/setup.py
|
|
||||||
examples/colors/README
|
|
||||||
examples/colors/colors.py
|
|
||||||
examples/colors/setup.py
|
|
||||||
examples/complex/README
|
|
||||||
examples/complex/setup.py
|
|
||||||
examples/complex/complex/__init__.py
|
|
||||||
examples/complex/complex/cli.py
|
|
||||||
examples/complex/complex/commands/__init__.py
|
|
||||||
examples/complex/complex/commands/cmd_init.py
|
|
||||||
examples/complex/complex/commands/cmd_status.py
|
|
||||||
examples/imagepipe/.gitignore
|
|
||||||
examples/imagepipe/README
|
|
||||||
examples/imagepipe/example01.jpg
|
|
||||||
examples/imagepipe/example02.jpg
|
|
||||||
examples/imagepipe/imagepipe.py
|
|
||||||
examples/imagepipe/setup.py
|
|
||||||
examples/inout/README
|
|
||||||
examples/inout/inout.py
|
|
||||||
examples/inout/setup.py
|
|
||||||
examples/naval/README
|
|
||||||
examples/naval/naval.py
|
|
||||||
examples/naval/setup.py
|
|
||||||
examples/repo/README
|
|
||||||
examples/repo/repo.py
|
|
||||||
examples/repo/setup.py
|
|
||||||
examples/termui/README
|
|
||||||
examples/termui/setup.py
|
|
||||||
examples/termui/termui.py
|
|
||||||
examples/validation/README
|
|
||||||
examples/validation/setup.py
|
|
||||||
examples/validation/validation.py
|
|
||||||
src/click/__init__.py
|
|
||||||
src/click/_bashcomplete.py
|
|
||||||
src/click/_compat.py
|
|
||||||
src/click/_termui_impl.py
|
|
||||||
src/click/_textwrap.py
|
|
||||||
src/click/_unicodefun.py
|
|
||||||
src/click/_winconsole.py
|
|
||||||
src/click/core.py
|
|
||||||
src/click/decorators.py
|
|
||||||
src/click/exceptions.py
|
|
||||||
src/click/formatting.py
|
|
||||||
src/click/globals.py
|
|
||||||
src/click/parser.py
|
|
||||||
src/click/termui.py
|
|
||||||
src/click/testing.py
|
|
||||||
src/click/types.py
|
|
||||||
src/click/utils.py
|
|
||||||
src/click.egg-info/PKG-INFO
|
|
||||||
src/click.egg-info/SOURCES.txt
|
|
||||||
src/click.egg-info/dependency_links.txt
|
|
||||||
src/click.egg-info/top_level.txt
|
|
||||||
tests/conftest.py
|
|
||||||
tests/test_arguments.py
|
|
||||||
tests/test_bashcomplete.py
|
|
||||||
tests/test_basic.py
|
|
||||||
tests/test_chain.py
|
|
||||||
tests/test_commands.py
|
|
||||||
tests/test_compat.py
|
|
||||||
tests/test_context.py
|
|
||||||
tests/test_defaults.py
|
|
||||||
tests/test_formatting.py
|
|
||||||
tests/test_imports.py
|
|
||||||
tests/test_normalization.py
|
|
||||||
tests/test_options.py
|
|
||||||
tests/test_termui.py
|
|
||||||
tests/test_testing.py
|
|
||||||
tests/test_utils.py
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
click
|
|
|
@ -4,76 +4,72 @@ writing command line scripts fun. Unlike other modules, it's based
|
||||||
around a simple API that does not come with too much magic and is
|
around a simple API that does not come with too much magic and is
|
||||||
composable.
|
composable.
|
||||||
"""
|
"""
|
||||||
from .core import Argument
|
from .core import Argument as Argument
|
||||||
from .core import BaseCommand
|
from .core import BaseCommand as BaseCommand
|
||||||
from .core import Command
|
from .core import Command as Command
|
||||||
from .core import CommandCollection
|
from .core import CommandCollection as CommandCollection
|
||||||
from .core import Context
|
from .core import Context as Context
|
||||||
from .core import Group
|
from .core import Group as Group
|
||||||
from .core import MultiCommand
|
from .core import MultiCommand as MultiCommand
|
||||||
from .core import Option
|
from .core import Option as Option
|
||||||
from .core import Parameter
|
from .core import Parameter as Parameter
|
||||||
from .decorators import argument
|
from .decorators import argument as argument
|
||||||
from .decorators import command
|
from .decorators import command as command
|
||||||
from .decorators import confirmation_option
|
from .decorators import confirmation_option as confirmation_option
|
||||||
from .decorators import group
|
from .decorators import group as group
|
||||||
from .decorators import help_option
|
from .decorators import help_option as help_option
|
||||||
from .decorators import make_pass_decorator
|
from .decorators import make_pass_decorator as make_pass_decorator
|
||||||
from .decorators import option
|
from .decorators import option as option
|
||||||
from .decorators import pass_context
|
from .decorators import pass_context as pass_context
|
||||||
from .decorators import pass_obj
|
from .decorators import pass_obj as pass_obj
|
||||||
from .decorators import password_option
|
from .decorators import password_option as password_option
|
||||||
from .decorators import version_option
|
from .decorators import version_option as version_option
|
||||||
from .exceptions import Abort
|
from .exceptions import Abort as Abort
|
||||||
from .exceptions import BadArgumentUsage
|
from .exceptions import BadArgumentUsage as BadArgumentUsage
|
||||||
from .exceptions import BadOptionUsage
|
from .exceptions import BadOptionUsage as BadOptionUsage
|
||||||
from .exceptions import BadParameter
|
from .exceptions import BadParameter as BadParameter
|
||||||
from .exceptions import ClickException
|
from .exceptions import ClickException as ClickException
|
||||||
from .exceptions import FileError
|
from .exceptions import FileError as FileError
|
||||||
from .exceptions import MissingParameter
|
from .exceptions import MissingParameter as MissingParameter
|
||||||
from .exceptions import NoSuchOption
|
from .exceptions import NoSuchOption as NoSuchOption
|
||||||
from .exceptions import UsageError
|
from .exceptions import UsageError as UsageError
|
||||||
from .formatting import HelpFormatter
|
from .formatting import HelpFormatter as HelpFormatter
|
||||||
from .formatting import wrap_text
|
from .formatting import wrap_text as wrap_text
|
||||||
from .globals import get_current_context
|
from .globals import get_current_context as get_current_context
|
||||||
from .parser import OptionParser
|
from .parser import OptionParser as OptionParser
|
||||||
from .termui import clear
|
from .termui import clear as clear
|
||||||
from .termui import confirm
|
from .termui import confirm as confirm
|
||||||
from .termui import echo_via_pager
|
from .termui import echo_via_pager as echo_via_pager
|
||||||
from .termui import edit
|
from .termui import edit as edit
|
||||||
from .termui import get_terminal_size
|
from .termui import get_terminal_size as get_terminal_size
|
||||||
from .termui import getchar
|
from .termui import getchar as getchar
|
||||||
from .termui import launch
|
from .termui import launch as launch
|
||||||
from .termui import pause
|
from .termui import pause as pause
|
||||||
from .termui import progressbar
|
from .termui import progressbar as progressbar
|
||||||
from .termui import prompt
|
from .termui import prompt as prompt
|
||||||
from .termui import secho
|
from .termui import secho as secho
|
||||||
from .termui import style
|
from .termui import style as style
|
||||||
from .termui import unstyle
|
from .termui import unstyle as unstyle
|
||||||
from .types import BOOL
|
from .types import BOOL as BOOL
|
||||||
from .types import Choice
|
from .types import Choice as Choice
|
||||||
from .types import DateTime
|
from .types import DateTime as DateTime
|
||||||
from .types import File
|
from .types import File as File
|
||||||
from .types import FLOAT
|
from .types import FLOAT as FLOAT
|
||||||
from .types import FloatRange
|
from .types import FloatRange as FloatRange
|
||||||
from .types import INT
|
from .types import INT as INT
|
||||||
from .types import IntRange
|
from .types import IntRange as IntRange
|
||||||
from .types import ParamType
|
from .types import ParamType as ParamType
|
||||||
from .types import Path
|
from .types import Path as Path
|
||||||
from .types import STRING
|
from .types import STRING as STRING
|
||||||
from .types import Tuple
|
from .types import Tuple as Tuple
|
||||||
from .types import UNPROCESSED
|
from .types import UNPROCESSED as UNPROCESSED
|
||||||
from .types import UUID
|
from .types import UUID as UUID
|
||||||
from .utils import echo
|
from .utils import echo as echo
|
||||||
from .utils import format_filename
|
from .utils import format_filename as format_filename
|
||||||
from .utils import get_app_dir
|
from .utils import get_app_dir as get_app_dir
|
||||||
from .utils import get_binary_stream
|
from .utils import get_binary_stream as get_binary_stream
|
||||||
from .utils import get_os_args
|
from .utils import get_os_args as get_os_args
|
||||||
from .utils import get_text_stream
|
from .utils import get_text_stream as get_text_stream
|
||||||
from .utils import open_file
|
from .utils import open_file as open_file
|
||||||
|
|
||||||
# Controls if click should emit the warning about the use of unicode
|
__version__ = "8.0.2"
|
||||||
# literals.
|
|
||||||
disable_unicode_literals_warning = False
|
|
||||||
|
|
||||||
__version__ = "7.1.2"
|
|
||||||
|
|
|
@ -1,375 +0,0 @@
|
||||||
import copy
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .core import Argument
|
|
||||||
from .core import MultiCommand
|
|
||||||
from .core import Option
|
|
||||||
from .parser import split_arg_string
|
|
||||||
from .types import Choice
|
|
||||||
from .utils import echo
|
|
||||||
|
|
||||||
try:
|
|
||||||
from collections import abc
|
|
||||||
except ImportError:
|
|
||||||
import collections as abc
|
|
||||||
|
|
||||||
WORDBREAK = "="
|
|
||||||
|
|
||||||
# Note, only BASH version 4.4 and later have the nosort option.
|
|
||||||
COMPLETION_SCRIPT_BASH = """
|
|
||||||
%(complete_func)s() {
|
|
||||||
local IFS=$'\n'
|
|
||||||
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\
|
|
||||||
COMP_CWORD=$COMP_CWORD \\
|
|
||||||
%(autocomplete_var)s=complete $1 ) )
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
%(complete_func)setup() {
|
|
||||||
local COMPLETION_OPTIONS=""
|
|
||||||
local BASH_VERSION_ARR=(${BASH_VERSION//./ })
|
|
||||||
# Only BASH version 4.4 and later have the nosort option.
|
|
||||||
if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] \
|
|
||||||
&& [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then
|
|
||||||
COMPLETION_OPTIONS="-o nosort"
|
|
||||||
fi
|
|
||||||
|
|
||||||
complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s
|
|
||||||
}
|
|
||||||
|
|
||||||
%(complete_func)setup
|
|
||||||
"""
|
|
||||||
|
|
||||||
COMPLETION_SCRIPT_ZSH = """
|
|
||||||
#compdef %(script_names)s
|
|
||||||
|
|
||||||
%(complete_func)s() {
|
|
||||||
local -a completions
|
|
||||||
local -a completions_with_descriptions
|
|
||||||
local -a response
|
|
||||||
(( ! $+commands[%(script_names)s] )) && return 1
|
|
||||||
|
|
||||||
response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\
|
|
||||||
COMP_CWORD=$((CURRENT-1)) \\
|
|
||||||
%(autocomplete_var)s=\"complete_zsh\" \\
|
|
||||||
%(script_names)s )}")
|
|
||||||
|
|
||||||
for key descr in ${(kv)response}; do
|
|
||||||
if [[ "$descr" == "_" ]]; then
|
|
||||||
completions+=("$key")
|
|
||||||
else
|
|
||||||
completions_with_descriptions+=("$key":"$descr")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -n "$completions_with_descriptions" ]; then
|
|
||||||
_describe -V unsorted completions_with_descriptions -U
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$completions" ]; then
|
|
||||||
compadd -U -V unsorted -a completions
|
|
||||||
fi
|
|
||||||
compstate[insert]="automenu"
|
|
||||||
}
|
|
||||||
|
|
||||||
compdef %(complete_func)s %(script_names)s
|
|
||||||
"""
|
|
||||||
|
|
||||||
COMPLETION_SCRIPT_FISH = (
|
|
||||||
"complete --no-files --command %(script_names)s --arguments"
|
|
||||||
' "(env %(autocomplete_var)s=complete_fish'
|
|
||||||
" COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t)"
|
|
||||||
' %(script_names)s)"'
|
|
||||||
)
|
|
||||||
|
|
||||||
_completion_scripts = {
|
|
||||||
"bash": COMPLETION_SCRIPT_BASH,
|
|
||||||
"zsh": COMPLETION_SCRIPT_ZSH,
|
|
||||||
"fish": COMPLETION_SCRIPT_FISH,
|
|
||||||
}
|
|
||||||
|
|
||||||
_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]")
|
|
||||||
|
|
||||||
|
|
||||||
def get_completion_script(prog_name, complete_var, shell):
|
|
||||||
cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_"))
|
|
||||||
script = _completion_scripts.get(shell, COMPLETION_SCRIPT_BASH)
|
|
||||||
return (
|
|
||||||
script
|
|
||||||
% {
|
|
||||||
"complete_func": "_{}_completion".format(cf_name),
|
|
||||||
"script_names": prog_name,
|
|
||||||
"autocomplete_var": complete_var,
|
|
||||||
}
|
|
||||||
).strip() + ";"
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_ctx(cli, prog_name, args):
|
|
||||||
"""Parse into a hierarchy of contexts. Contexts are connected
|
|
||||||
through the parent variable.
|
|
||||||
|
|
||||||
:param cli: command definition
|
|
||||||
:param prog_name: the program that is running
|
|
||||||
:param args: full list of args
|
|
||||||
:return: the final context/command parsed
|
|
||||||
"""
|
|
||||||
ctx = cli.make_context(prog_name, args, resilient_parsing=True)
|
|
||||||
args = ctx.protected_args + ctx.args
|
|
||||||
while args:
|
|
||||||
if isinstance(ctx.command, MultiCommand):
|
|
||||||
if not ctx.command.chain:
|
|
||||||
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
|
|
||||||
if cmd is None:
|
|
||||||
return ctx
|
|
||||||
ctx = cmd.make_context(
|
|
||||||
cmd_name, args, parent=ctx, resilient_parsing=True
|
|
||||||
)
|
|
||||||
args = ctx.protected_args + ctx.args
|
|
||||||
else:
|
|
||||||
# Walk chained subcommand contexts saving the last one.
|
|
||||||
while args:
|
|
||||||
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
|
|
||||||
if cmd is None:
|
|
||||||
return ctx
|
|
||||||
sub_ctx = cmd.make_context(
|
|
||||||
cmd_name,
|
|
||||||
args,
|
|
||||||
parent=ctx,
|
|
||||||
allow_extra_args=True,
|
|
||||||
allow_interspersed_args=False,
|
|
||||||
resilient_parsing=True,
|
|
||||||
)
|
|
||||||
args = sub_ctx.args
|
|
||||||
ctx = sub_ctx
|
|
||||||
args = sub_ctx.protected_args + sub_ctx.args
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
def start_of_option(param_str):
|
|
||||||
"""
|
|
||||||
:param param_str: param_str to check
|
|
||||||
:return: whether or not this is the start of an option declaration
|
|
||||||
(i.e. starts "-" or "--")
|
|
||||||
"""
|
|
||||||
return param_str and param_str[:1] == "-"
|
|
||||||
|
|
||||||
|
|
||||||
def is_incomplete_option(all_args, cmd_param):
|
|
||||||
"""
|
|
||||||
:param all_args: the full original list of args supplied
|
|
||||||
:param cmd_param: the current command paramter
|
|
||||||
:return: whether or not the last option declaration (i.e. starts
|
|
||||||
"-" or "--") is incomplete and corresponds to this cmd_param. In
|
|
||||||
other words whether this cmd_param option can still accept
|
|
||||||
values
|
|
||||||
"""
|
|
||||||
if not isinstance(cmd_param, Option):
|
|
||||||
return False
|
|
||||||
if cmd_param.is_flag:
|
|
||||||
return False
|
|
||||||
last_option = None
|
|
||||||
for index, arg_str in enumerate(
|
|
||||||
reversed([arg for arg in all_args if arg != WORDBREAK])
|
|
||||||
):
|
|
||||||
if index + 1 > cmd_param.nargs:
|
|
||||||
break
|
|
||||||
if start_of_option(arg_str):
|
|
||||||
last_option = arg_str
|
|
||||||
|
|
||||||
return True if last_option and last_option in cmd_param.opts else False
|
|
||||||
|
|
||||||
|
|
||||||
def is_incomplete_argument(current_params, cmd_param):
|
|
||||||
"""
|
|
||||||
:param current_params: the current params and values for this
|
|
||||||
argument as already entered
|
|
||||||
:param cmd_param: the current command parameter
|
|
||||||
:return: whether or not the last argument is incomplete and
|
|
||||||
corresponds to this cmd_param. In other words whether or not the
|
|
||||||
this cmd_param argument can still accept values
|
|
||||||
"""
|
|
||||||
if not isinstance(cmd_param, Argument):
|
|
||||||
return False
|
|
||||||
current_param_values = current_params[cmd_param.name]
|
|
||||||
if current_param_values is None:
|
|
||||||
return True
|
|
||||||
if cmd_param.nargs == -1:
|
|
||||||
return True
|
|
||||||
if (
|
|
||||||
isinstance(current_param_values, abc.Iterable)
|
|
||||||
and cmd_param.nargs > 1
|
|
||||||
and len(current_param_values) < cmd_param.nargs
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_autocompletions(ctx, args, incomplete, cmd_param):
|
|
||||||
"""
|
|
||||||
:param ctx: context associated with the parsed command
|
|
||||||
:param args: full list of args
|
|
||||||
:param incomplete: the incomplete text to autocomplete
|
|
||||||
:param cmd_param: command definition
|
|
||||||
:return: all the possible user-specified completions for the param
|
|
||||||
"""
|
|
||||||
results = []
|
|
||||||
if isinstance(cmd_param.type, Choice):
|
|
||||||
# Choices don't support descriptions.
|
|
||||||
results = [
|
|
||||||
(c, None) for c in cmd_param.type.choices if str(c).startswith(incomplete)
|
|
||||||
]
|
|
||||||
elif cmd_param.autocompletion is not None:
|
|
||||||
dynamic_completions = cmd_param.autocompletion(
|
|
||||||
ctx=ctx, args=args, incomplete=incomplete
|
|
||||||
)
|
|
||||||
results = [
|
|
||||||
c if isinstance(c, tuple) else (c, None) for c in dynamic_completions
|
|
||||||
]
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def get_visible_commands_starting_with(ctx, starts_with):
|
|
||||||
"""
|
|
||||||
:param ctx: context associated with the parsed command
|
|
||||||
:starts_with: string that visible commands must start with.
|
|
||||||
:return: all visible (not hidden) commands that start with starts_with.
|
|
||||||
"""
|
|
||||||
for c in ctx.command.list_commands(ctx):
|
|
||||||
if c.startswith(starts_with):
|
|
||||||
command = ctx.command.get_command(ctx, c)
|
|
||||||
if not command.hidden:
|
|
||||||
yield command
|
|
||||||
|
|
||||||
|
|
||||||
def add_subcommand_completions(ctx, incomplete, completions_out):
|
|
||||||
# Add subcommand completions.
|
|
||||||
if isinstance(ctx.command, MultiCommand):
|
|
||||||
completions_out.extend(
|
|
||||||
[
|
|
||||||
(c.name, c.get_short_help_str())
|
|
||||||
for c in get_visible_commands_starting_with(ctx, incomplete)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Walk up the context list and add any other completion
|
|
||||||
# possibilities from chained commands
|
|
||||||
while ctx.parent is not None:
|
|
||||||
ctx = ctx.parent
|
|
||||||
if isinstance(ctx.command, MultiCommand) and ctx.command.chain:
|
|
||||||
remaining_commands = [
|
|
||||||
c
|
|
||||||
for c in get_visible_commands_starting_with(ctx, incomplete)
|
|
||||||
if c.name not in ctx.protected_args
|
|
||||||
]
|
|
||||||
completions_out.extend(
|
|
||||||
[(c.name, c.get_short_help_str()) for c in remaining_commands]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_choices(cli, prog_name, args, incomplete):
|
|
||||||
"""
|
|
||||||
:param cli: command definition
|
|
||||||
:param prog_name: the program that is running
|
|
||||||
:param args: full list of args
|
|
||||||
:param incomplete: the incomplete text to autocomplete
|
|
||||||
:return: all the possible completions for the incomplete
|
|
||||||
"""
|
|
||||||
all_args = copy.deepcopy(args)
|
|
||||||
|
|
||||||
ctx = resolve_ctx(cli, prog_name, args)
|
|
||||||
if ctx is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
has_double_dash = "--" in all_args
|
|
||||||
|
|
||||||
# In newer versions of bash long opts with '='s are partitioned, but
|
|
||||||
# it's easier to parse without the '='
|
|
||||||
if start_of_option(incomplete) and WORDBREAK in incomplete:
|
|
||||||
partition_incomplete = incomplete.partition(WORDBREAK)
|
|
||||||
all_args.append(partition_incomplete[0])
|
|
||||||
incomplete = partition_incomplete[2]
|
|
||||||
elif incomplete == WORDBREAK:
|
|
||||||
incomplete = ""
|
|
||||||
|
|
||||||
completions = []
|
|
||||||
if not has_double_dash and start_of_option(incomplete):
|
|
||||||
# completions for partial options
|
|
||||||
for param in ctx.command.params:
|
|
||||||
if isinstance(param, Option) and not param.hidden:
|
|
||||||
param_opts = [
|
|
||||||
param_opt
|
|
||||||
for param_opt in param.opts + param.secondary_opts
|
|
||||||
if param_opt not in all_args or param.multiple
|
|
||||||
]
|
|
||||||
completions.extend(
|
|
||||||
[(o, param.help) for o in param_opts if o.startswith(incomplete)]
|
|
||||||
)
|
|
||||||
return completions
|
|
||||||
# completion for option values from user supplied values
|
|
||||||
for param in ctx.command.params:
|
|
||||||
if is_incomplete_option(all_args, param):
|
|
||||||
return get_user_autocompletions(ctx, all_args, incomplete, param)
|
|
||||||
# completion for argument values from user supplied values
|
|
||||||
for param in ctx.command.params:
|
|
||||||
if is_incomplete_argument(ctx.params, param):
|
|
||||||
return get_user_autocompletions(ctx, all_args, incomplete, param)
|
|
||||||
|
|
||||||
add_subcommand_completions(ctx, incomplete, completions)
|
|
||||||
# Sort before returning so that proper ordering can be enforced in custom types.
|
|
||||||
return sorted(completions)
|
|
||||||
|
|
||||||
|
|
||||||
def do_complete(cli, prog_name, include_descriptions):
|
|
||||||
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
|
||||||
cword = int(os.environ["COMP_CWORD"])
|
|
||||||
args = cwords[1:cword]
|
|
||||||
try:
|
|
||||||
incomplete = cwords[cword]
|
|
||||||
except IndexError:
|
|
||||||
incomplete = ""
|
|
||||||
|
|
||||||
for item in get_choices(cli, prog_name, args, incomplete):
|
|
||||||
echo(item[0])
|
|
||||||
if include_descriptions:
|
|
||||||
# ZSH has trouble dealing with empty array parameters when
|
|
||||||
# returned from commands, use '_' to indicate no description
|
|
||||||
# is present.
|
|
||||||
echo(item[1] if item[1] else "_")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def do_complete_fish(cli, prog_name):
|
|
||||||
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
|
||||||
incomplete = os.environ["COMP_CWORD"]
|
|
||||||
args = cwords[1:]
|
|
||||||
|
|
||||||
for item in get_choices(cli, prog_name, args, incomplete):
|
|
||||||
if item[1]:
|
|
||||||
echo("{arg}\t{desc}".format(arg=item[0], desc=item[1]))
|
|
||||||
else:
|
|
||||||
echo(item[0])
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def bashcomplete(cli, prog_name, complete_var, complete_instr):
|
|
||||||
if "_" in complete_instr:
|
|
||||||
command, shell = complete_instr.split("_", 1)
|
|
||||||
else:
|
|
||||||
command = complete_instr
|
|
||||||
shell = "bash"
|
|
||||||
|
|
||||||
if command == "source":
|
|
||||||
echo(get_completion_script(prog_name, complete_var, shell))
|
|
||||||
return True
|
|
||||||
elif command == "complete":
|
|
||||||
if shell == "fish":
|
|
||||||
return do_complete_fish(cli, prog_name)
|
|
||||||
elif shell in {"bash", "zsh"}:
|
|
||||||
return do_complete(cli, prog_name, shell == "zsh")
|
|
||||||
|
|
||||||
return False
|
|
|
@ -1,12 +1,11 @@
|
||||||
# flake8: noqa
|
|
||||||
import codecs
|
import codecs
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import typing as t
|
||||||
from weakref import WeakKeyDictionary
|
from weakref import WeakKeyDictionary
|
||||||
|
|
||||||
PY2 = sys.version_info[0] == 2
|
|
||||||
CYGWIN = sys.platform.startswith("cygwin")
|
CYGWIN = sys.platform.startswith("cygwin")
|
||||||
MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version)
|
MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version)
|
||||||
# Determine local App Engine environment, per Google's own suggestion
|
# Determine local App Engine environment, per Google's own suggestion
|
||||||
|
@ -14,19 +13,21 @@ APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ.
|
||||||
"SERVER_SOFTWARE", ""
|
"SERVER_SOFTWARE", ""
|
||||||
)
|
)
|
||||||
WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2
|
WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2
|
||||||
DEFAULT_COLUMNS = 80
|
auto_wrap_for_ansi: t.Optional[t.Callable[[t.TextIO], t.TextIO]] = None
|
||||||
|
|
||||||
|
|
||||||
_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
|
_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
|
||||||
|
|
||||||
|
|
||||||
def get_filesystem_encoding():
|
def get_filesystem_encoding() -> str:
|
||||||
return sys.getfilesystemencoding() or sys.getdefaultencoding()
|
return sys.getfilesystemencoding() or sys.getdefaultencoding()
|
||||||
|
|
||||||
|
|
||||||
def _make_text_stream(
|
def _make_text_stream(
|
||||||
stream, encoding, errors, force_readable=False, force_writable=False
|
stream: t.BinaryIO,
|
||||||
):
|
encoding: t.Optional[str],
|
||||||
|
errors: t.Optional[str],
|
||||||
|
force_readable: bool = False,
|
||||||
|
force_writable: bool = False,
|
||||||
|
) -> t.TextIO:
|
||||||
if encoding is None:
|
if encoding is None:
|
||||||
encoding = get_best_encoding(stream)
|
encoding = get_best_encoding(stream)
|
||||||
if errors is None:
|
if errors is None:
|
||||||
|
@ -41,7 +42,7 @@ def _make_text_stream(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_ascii_encoding(encoding):
|
def is_ascii_encoding(encoding: str) -> bool:
|
||||||
"""Checks if a given encoding is ascii."""
|
"""Checks if a given encoding is ascii."""
|
||||||
try:
|
try:
|
||||||
return codecs.lookup(encoding).name == "ascii"
|
return codecs.lookup(encoding).name == "ascii"
|
||||||
|
@ -49,7 +50,7 @@ def is_ascii_encoding(encoding):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_best_encoding(stream):
|
def get_best_encoding(stream: t.IO) -> str:
|
||||||
"""Returns the default stream encoding if not found."""
|
"""Returns the default stream encoding if not found."""
|
||||||
rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
|
rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
|
||||||
if is_ascii_encoding(rv):
|
if is_ascii_encoding(rv):
|
||||||
|
@ -60,46 +61,30 @@ def get_best_encoding(stream):
|
||||||
class _NonClosingTextIOWrapper(io.TextIOWrapper):
|
class _NonClosingTextIOWrapper(io.TextIOWrapper):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
stream,
|
stream: t.BinaryIO,
|
||||||
encoding,
|
encoding: t.Optional[str],
|
||||||
errors,
|
errors: t.Optional[str],
|
||||||
force_readable=False,
|
force_readable: bool = False,
|
||||||
force_writable=False,
|
force_writable: bool = False,
|
||||||
**extra
|
**extra: t.Any,
|
||||||
):
|
) -> None:
|
||||||
self._stream = stream = _FixupStream(stream, force_readable, force_writable)
|
self._stream = stream = t.cast(
|
||||||
io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra)
|
t.BinaryIO, _FixupStream(stream, force_readable, force_writable)
|
||||||
|
)
|
||||||
|
super().__init__(stream, encoding, errors, **extra)
|
||||||
|
|
||||||
# The io module is a place where the Python 3 text behavior
|
def __del__(self) -> None:
|
||||||
# was forced upon Python 2, so we need to unbreak
|
|
||||||
# it to look like Python 2.
|
|
||||||
if PY2:
|
|
||||||
|
|
||||||
def write(self, x):
|
|
||||||
if isinstance(x, str) or is_bytes(x):
|
|
||||||
try:
|
|
||||||
self.flush()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return self.buffer.write(str(x))
|
|
||||||
return io.TextIOWrapper.write(self, x)
|
|
||||||
|
|
||||||
def writelines(self, lines):
|
|
||||||
for line in lines:
|
|
||||||
self.write(line)
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
try:
|
try:
|
||||||
self.detach()
|
self.detach()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def isatty(self):
|
def isatty(self) -> bool:
|
||||||
# https://bitbucket.org/pypy/pypy/issue/1803
|
# https://bitbucket.org/pypy/pypy/issue/1803
|
||||||
return self._stream.isatty()
|
return self._stream.isatty()
|
||||||
|
|
||||||
|
|
||||||
class _FixupStream(object):
|
class _FixupStream:
|
||||||
"""The new io interface needs more from streams than streams
|
"""The new io interface needs more from streams than streams
|
||||||
traditionally implement. As such, this fix-up code is necessary in
|
traditionally implement. As such, this fix-up code is necessary in
|
||||||
some circumstances.
|
some circumstances.
|
||||||
|
@ -109,45 +94,47 @@ class _FixupStream(object):
|
||||||
of jupyter notebook).
|
of jupyter notebook).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, stream, force_readable=False, force_writable=False):
|
def __init__(
|
||||||
|
self,
|
||||||
|
stream: t.BinaryIO,
|
||||||
|
force_readable: bool = False,
|
||||||
|
force_writable: bool = False,
|
||||||
|
):
|
||||||
self._stream = stream
|
self._stream = stream
|
||||||
self._force_readable = force_readable
|
self._force_readable = force_readable
|
||||||
self._force_writable = force_writable
|
self._force_writable = force_writable
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
return getattr(self._stream, name)
|
return getattr(self._stream, name)
|
||||||
|
|
||||||
def read1(self, size):
|
def read1(self, size: int) -> bytes:
|
||||||
f = getattr(self._stream, "read1", None)
|
f = getattr(self._stream, "read1", None)
|
||||||
|
|
||||||
if f is not None:
|
if f is not None:
|
||||||
return f(size)
|
return t.cast(bytes, f(size))
|
||||||
# We only dispatch to readline instead of read in Python 2 as we
|
|
||||||
# do not want cause problems with the different implementation
|
|
||||||
# of line buffering.
|
|
||||||
if PY2:
|
|
||||||
return self._stream.readline(size)
|
|
||||||
return self._stream.read(size)
|
return self._stream.read(size)
|
||||||
|
|
||||||
def readable(self):
|
def readable(self) -> bool:
|
||||||
if self._force_readable:
|
if self._force_readable:
|
||||||
return True
|
return True
|
||||||
x = getattr(self._stream, "readable", None)
|
x = getattr(self._stream, "readable", None)
|
||||||
if x is not None:
|
if x is not None:
|
||||||
return x()
|
return t.cast(bool, x())
|
||||||
try:
|
try:
|
||||||
self._stream.read(0)
|
self._stream.read(0)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def writable(self):
|
def writable(self) -> bool:
|
||||||
if self._force_writable:
|
if self._force_writable:
|
||||||
return True
|
return True
|
||||||
x = getattr(self._stream, "writable", None)
|
x = getattr(self._stream, "writable", None)
|
||||||
if x is not None:
|
if x is not None:
|
||||||
return x()
|
return t.cast(bool, x())
|
||||||
try:
|
try:
|
||||||
self._stream.write("")
|
self._stream.write("") # type: ignore
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
self._stream.write(b"")
|
self._stream.write(b"")
|
||||||
|
@ -155,10 +142,10 @@ class _FixupStream(object):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def seekable(self):
|
def seekable(self) -> bool:
|
||||||
x = getattr(self._stream, "seekable", None)
|
x = getattr(self._stream, "seekable", None)
|
||||||
if x is not None:
|
if x is not None:
|
||||||
return x()
|
return t.cast(bool, x())
|
||||||
try:
|
try:
|
||||||
self._stream.seek(self._stream.tell())
|
self._stream.seek(self._stream.tell())
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -166,351 +153,239 @@ class _FixupStream(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
if PY2:
|
def _is_binary_reader(stream: t.IO, default: bool = False) -> bool:
|
||||||
text_type = unicode
|
|
||||||
raw_input = raw_input
|
|
||||||
string_types = (str, unicode)
|
|
||||||
int_types = (int, long)
|
|
||||||
iteritems = lambda x: x.iteritems()
|
|
||||||
range_type = xrange
|
|
||||||
|
|
||||||
def is_bytes(x):
|
|
||||||
return isinstance(x, (buffer, bytearray))
|
|
||||||
|
|
||||||
_identifier_re = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
|
||||||
|
|
||||||
# For Windows, we need to force stdout/stdin/stderr to binary if it's
|
|
||||||
# fetched for that. This obviously is not the most correct way to do
|
|
||||||
# it as it changes global state. Unfortunately, there does not seem to
|
|
||||||
# be a clear better way to do it as just reopening the file in binary
|
|
||||||
# mode does not change anything.
|
|
||||||
#
|
|
||||||
# An option would be to do what Python 3 does and to open the file as
|
|
||||||
# binary only, patch it back to the system, and then use a wrapper
|
|
||||||
# stream that converts newlines. It's not quite clear what's the
|
|
||||||
# correct option here.
|
|
||||||
#
|
|
||||||
# This code also lives in _winconsole for the fallback to the console
|
|
||||||
# emulation stream.
|
|
||||||
#
|
|
||||||
# There are also Windows environments where the `msvcrt` module is not
|
|
||||||
# available (which is why we use try-catch instead of the WIN variable
|
|
||||||
# here), such as the Google App Engine development server on Windows. In
|
|
||||||
# those cases there is just nothing we can do.
|
|
||||||
def set_binary_mode(f):
|
|
||||||
return f
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import msvcrt
|
return isinstance(stream.read(0), bytes)
|
||||||
except ImportError:
|
except Exception:
|
||||||
pass
|
return default
|
||||||
else:
|
# This happens in some cases where the stream was already
|
||||||
|
# closed. In this case, we assume the default.
|
||||||
|
|
||||||
def set_binary_mode(f):
|
|
||||||
try:
|
|
||||||
fileno = f.fileno()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
msvcrt.setmode(fileno, os.O_BINARY)
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
def _is_binary_writer(stream: t.IO, default: bool = False) -> bool:
|
||||||
try:
|
try:
|
||||||
import fcntl
|
stream.write(b"")
|
||||||
except ImportError:
|
except Exception:
|
||||||
pass
|
try:
|
||||||
|
stream.write("")
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return default
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _find_binary_reader(stream: t.IO) -> t.Optional[t.BinaryIO]:
|
||||||
|
# We need to figure out if the given stream is already binary.
|
||||||
|
# This can happen because the official docs recommend detaching
|
||||||
|
# the streams to get binary streams. Some code might do this, so
|
||||||
|
# we need to deal with this case explicitly.
|
||||||
|
if _is_binary_reader(stream, False):
|
||||||
|
return t.cast(t.BinaryIO, stream)
|
||||||
|
|
||||||
|
buf = getattr(stream, "buffer", None)
|
||||||
|
|
||||||
|
# Same situation here; this time we assume that the buffer is
|
||||||
|
# actually binary in case it's closed.
|
||||||
|
if buf is not None and _is_binary_reader(buf, True):
|
||||||
|
return t.cast(t.BinaryIO, buf)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _find_binary_writer(stream: t.IO) -> t.Optional[t.BinaryIO]:
|
||||||
|
# We need to figure out if the given stream is already binary.
|
||||||
|
# This can happen because the official docs recommend detaching
|
||||||
|
# the streams to get binary streams. Some code might do this, so
|
||||||
|
# we need to deal with this case explicitly.
|
||||||
|
if _is_binary_writer(stream, False):
|
||||||
|
return t.cast(t.BinaryIO, stream)
|
||||||
|
|
||||||
|
buf = getattr(stream, "buffer", None)
|
||||||
|
|
||||||
|
# Same situation here; this time we assume that the buffer is
|
||||||
|
# actually binary in case it's closed.
|
||||||
|
if buf is not None and _is_binary_writer(buf, True):
|
||||||
|
return t.cast(t.BinaryIO, buf)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _stream_is_misconfigured(stream: t.TextIO) -> bool:
|
||||||
|
"""A stream is misconfigured if its encoding is ASCII."""
|
||||||
|
# If the stream does not have an encoding set, we assume it's set
|
||||||
|
# to ASCII. This appears to happen in certain unittest
|
||||||
|
# environments. It's not quite clear what the correct behavior is
|
||||||
|
# but this at least will force Click to recover somehow.
|
||||||
|
return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: t.Optional[str]) -> bool:
|
||||||
|
"""A stream attribute is compatible if it is equal to the
|
||||||
|
desired value or the desired value is unset and the attribute
|
||||||
|
has a value.
|
||||||
|
"""
|
||||||
|
stream_value = getattr(stream, attr, None)
|
||||||
|
return stream_value == value or (value is None and stream_value is not None)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_compatible_text_stream(
|
||||||
|
stream: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str]
|
||||||
|
) -> bool:
|
||||||
|
"""Check if a stream's encoding and errors attributes are
|
||||||
|
compatible with the desired values.
|
||||||
|
"""
|
||||||
|
return _is_compat_stream_attr(
|
||||||
|
stream, "encoding", encoding
|
||||||
|
) and _is_compat_stream_attr(stream, "errors", errors)
|
||||||
|
|
||||||
|
|
||||||
|
def _force_correct_text_stream(
|
||||||
|
text_stream: t.IO,
|
||||||
|
encoding: t.Optional[str],
|
||||||
|
errors: t.Optional[str],
|
||||||
|
is_binary: t.Callable[[t.IO, bool], bool],
|
||||||
|
find_binary: t.Callable[[t.IO], t.Optional[t.BinaryIO]],
|
||||||
|
force_readable: bool = False,
|
||||||
|
force_writable: bool = False,
|
||||||
|
) -> t.TextIO:
|
||||||
|
if is_binary(text_stream, False):
|
||||||
|
binary_reader = t.cast(t.BinaryIO, text_stream)
|
||||||
else:
|
else:
|
||||||
|
text_stream = t.cast(t.TextIO, text_stream)
|
||||||
|
# If the stream looks compatible, and won't default to a
|
||||||
|
# misconfigured ascii encoding, return it as-is.
|
||||||
|
if _is_compatible_text_stream(text_stream, encoding, errors) and not (
|
||||||
|
encoding is None and _stream_is_misconfigured(text_stream)
|
||||||
|
):
|
||||||
|
return text_stream
|
||||||
|
|
||||||
def set_binary_mode(f):
|
# Otherwise, get the underlying binary reader.
|
||||||
try:
|
possible_binary_reader = find_binary(text_stream)
|
||||||
fileno = f.fileno()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
flags = fcntl.fcntl(fileno, fcntl.F_GETFL)
|
|
||||||
fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
|
|
||||||
return f
|
|
||||||
|
|
||||||
def isidentifier(x):
|
# If that's not possible, silently use the original reader
|
||||||
return _identifier_re.search(x) is not None
|
# and get mojibake instead of exceptions.
|
||||||
|
if possible_binary_reader is None:
|
||||||
|
return text_stream
|
||||||
|
|
||||||
def get_binary_stdin():
|
binary_reader = possible_binary_reader
|
||||||
return set_binary_mode(sys.stdin)
|
|
||||||
|
|
||||||
def get_binary_stdout():
|
# Default errors to replace instead of strict in order to get
|
||||||
_wrap_std_stream("stdout")
|
# something that works.
|
||||||
return set_binary_mode(sys.stdout)
|
if errors is None:
|
||||||
|
errors = "replace"
|
||||||
|
|
||||||
def get_binary_stderr():
|
# Wrap the binary stream in a text stream with the correct
|
||||||
_wrap_std_stream("stderr")
|
# encoding parameters.
|
||||||
return set_binary_mode(sys.stderr)
|
return _make_text_stream(
|
||||||
|
binary_reader,
|
||||||
def get_text_stdin(encoding=None, errors=None):
|
|
||||||
rv = _get_windows_console_stream(sys.stdin, encoding, errors)
|
|
||||||
if rv is not None:
|
|
||||||
return rv
|
|
||||||
return _make_text_stream(sys.stdin, encoding, errors, force_readable=True)
|
|
||||||
|
|
||||||
def get_text_stdout(encoding=None, errors=None):
|
|
||||||
_wrap_std_stream("stdout")
|
|
||||||
rv = _get_windows_console_stream(sys.stdout, encoding, errors)
|
|
||||||
if rv is not None:
|
|
||||||
return rv
|
|
||||||
return _make_text_stream(sys.stdout, encoding, errors, force_writable=True)
|
|
||||||
|
|
||||||
def get_text_stderr(encoding=None, errors=None):
|
|
||||||
_wrap_std_stream("stderr")
|
|
||||||
rv = _get_windows_console_stream(sys.stderr, encoding, errors)
|
|
||||||
if rv is not None:
|
|
||||||
return rv
|
|
||||||
return _make_text_stream(sys.stderr, encoding, errors, force_writable=True)
|
|
||||||
|
|
||||||
def filename_to_ui(value):
|
|
||||||
if isinstance(value, bytes):
|
|
||||||
value = value.decode(get_filesystem_encoding(), "replace")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
|
||||||
import io
|
|
||||||
|
|
||||||
text_type = str
|
|
||||||
raw_input = input
|
|
||||||
string_types = (str,)
|
|
||||||
int_types = (int,)
|
|
||||||
range_type = range
|
|
||||||
isidentifier = lambda x: x.isidentifier()
|
|
||||||
iteritems = lambda x: iter(x.items())
|
|
||||||
|
|
||||||
def is_bytes(x):
|
|
||||||
return isinstance(x, (bytes, memoryview, bytearray))
|
|
||||||
|
|
||||||
def _is_binary_reader(stream, default=False):
|
|
||||||
try:
|
|
||||||
return isinstance(stream.read(0), bytes)
|
|
||||||
except Exception:
|
|
||||||
return default
|
|
||||||
# This happens in some cases where the stream was already
|
|
||||||
# closed. In this case, we assume the default.
|
|
||||||
|
|
||||||
def _is_binary_writer(stream, default=False):
|
|
||||||
try:
|
|
||||||
stream.write(b"")
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
stream.write("")
|
|
||||||
return False
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return default
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _find_binary_reader(stream):
|
|
||||||
# We need to figure out if the given stream is already binary.
|
|
||||||
# This can happen because the official docs recommend detaching
|
|
||||||
# the streams to get binary streams. Some code might do this, so
|
|
||||||
# we need to deal with this case explicitly.
|
|
||||||
if _is_binary_reader(stream, False):
|
|
||||||
return stream
|
|
||||||
|
|
||||||
buf = getattr(stream, "buffer", None)
|
|
||||||
|
|
||||||
# Same situation here; this time we assume that the buffer is
|
|
||||||
# actually binary in case it's closed.
|
|
||||||
if buf is not None and _is_binary_reader(buf, True):
|
|
||||||
return buf
|
|
||||||
|
|
||||||
def _find_binary_writer(stream):
|
|
||||||
# We need to figure out if the given stream is already binary.
|
|
||||||
# This can happen because the official docs recommend detatching
|
|
||||||
# the streams to get binary streams. Some code might do this, so
|
|
||||||
# we need to deal with this case explicitly.
|
|
||||||
if _is_binary_writer(stream, False):
|
|
||||||
return stream
|
|
||||||
|
|
||||||
buf = getattr(stream, "buffer", None)
|
|
||||||
|
|
||||||
# Same situation here; this time we assume that the buffer is
|
|
||||||
# actually binary in case it's closed.
|
|
||||||
if buf is not None and _is_binary_writer(buf, True):
|
|
||||||
return buf
|
|
||||||
|
|
||||||
def _stream_is_misconfigured(stream):
|
|
||||||
"""A stream is misconfigured if its encoding is ASCII."""
|
|
||||||
# If the stream does not have an encoding set, we assume it's set
|
|
||||||
# to ASCII. This appears to happen in certain unittest
|
|
||||||
# environments. It's not quite clear what the correct behavior is
|
|
||||||
# but this at least will force Click to recover somehow.
|
|
||||||
return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
|
|
||||||
|
|
||||||
def _is_compat_stream_attr(stream, attr, value):
|
|
||||||
"""A stream attribute is compatible if it is equal to the
|
|
||||||
desired value or the desired value is unset and the attribute
|
|
||||||
has a value.
|
|
||||||
"""
|
|
||||||
stream_value = getattr(stream, attr, None)
|
|
||||||
return stream_value == value or (value is None and stream_value is not None)
|
|
||||||
|
|
||||||
def _is_compatible_text_stream(stream, encoding, errors):
|
|
||||||
"""Check if a stream's encoding and errors attributes are
|
|
||||||
compatible with the desired values.
|
|
||||||
"""
|
|
||||||
return _is_compat_stream_attr(
|
|
||||||
stream, "encoding", encoding
|
|
||||||
) and _is_compat_stream_attr(stream, "errors", errors)
|
|
||||||
|
|
||||||
def _force_correct_text_stream(
|
|
||||||
text_stream,
|
|
||||||
encoding,
|
encoding,
|
||||||
errors,
|
errors,
|
||||||
is_binary,
|
force_readable=force_readable,
|
||||||
find_binary,
|
force_writable=force_writable,
|
||||||
force_readable=False,
|
)
|
||||||
force_writable=False,
|
|
||||||
):
|
|
||||||
if is_binary(text_stream, False):
|
|
||||||
binary_reader = text_stream
|
|
||||||
else:
|
|
||||||
# If the stream looks compatible, and won't default to a
|
|
||||||
# misconfigured ascii encoding, return it as-is.
|
|
||||||
if _is_compatible_text_stream(text_stream, encoding, errors) and not (
|
|
||||||
encoding is None and _stream_is_misconfigured(text_stream)
|
|
||||||
):
|
|
||||||
return text_stream
|
|
||||||
|
|
||||||
# Otherwise, get the underlying binary reader.
|
|
||||||
binary_reader = find_binary(text_stream)
|
|
||||||
|
|
||||||
# If that's not possible, silently use the original reader
|
|
||||||
# and get mojibake instead of exceptions.
|
|
||||||
if binary_reader is None:
|
|
||||||
return text_stream
|
|
||||||
|
|
||||||
# Default errors to replace instead of strict in order to get
|
|
||||||
# something that works.
|
|
||||||
if errors is None:
|
|
||||||
errors = "replace"
|
|
||||||
|
|
||||||
# Wrap the binary stream in a text stream with the correct
|
|
||||||
# encoding parameters.
|
|
||||||
return _make_text_stream(
|
|
||||||
binary_reader,
|
|
||||||
encoding,
|
|
||||||
errors,
|
|
||||||
force_readable=force_readable,
|
|
||||||
force_writable=force_writable,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _force_correct_text_reader(text_reader, encoding, errors, force_readable=False):
|
|
||||||
return _force_correct_text_stream(
|
|
||||||
text_reader,
|
|
||||||
encoding,
|
|
||||||
errors,
|
|
||||||
_is_binary_reader,
|
|
||||||
_find_binary_reader,
|
|
||||||
force_readable=force_readable,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _force_correct_text_writer(text_writer, encoding, errors, force_writable=False):
|
|
||||||
return _force_correct_text_stream(
|
|
||||||
text_writer,
|
|
||||||
encoding,
|
|
||||||
errors,
|
|
||||||
_is_binary_writer,
|
|
||||||
_find_binary_writer,
|
|
||||||
force_writable=force_writable,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_binary_stdin():
|
|
||||||
reader = _find_binary_reader(sys.stdin)
|
|
||||||
if reader is None:
|
|
||||||
raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
|
|
||||||
return reader
|
|
||||||
|
|
||||||
def get_binary_stdout():
|
|
||||||
writer = _find_binary_writer(sys.stdout)
|
|
||||||
if writer is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Was not able to determine binary stream for sys.stdout."
|
|
||||||
)
|
|
||||||
return writer
|
|
||||||
|
|
||||||
def get_binary_stderr():
|
|
||||||
writer = _find_binary_writer(sys.stderr)
|
|
||||||
if writer is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Was not able to determine binary stream for sys.stderr."
|
|
||||||
)
|
|
||||||
return writer
|
|
||||||
|
|
||||||
def get_text_stdin(encoding=None, errors=None):
|
|
||||||
rv = _get_windows_console_stream(sys.stdin, encoding, errors)
|
|
||||||
if rv is not None:
|
|
||||||
return rv
|
|
||||||
return _force_correct_text_reader(
|
|
||||||
sys.stdin, encoding, errors, force_readable=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_text_stdout(encoding=None, errors=None):
|
|
||||||
rv = _get_windows_console_stream(sys.stdout, encoding, errors)
|
|
||||||
if rv is not None:
|
|
||||||
return rv
|
|
||||||
return _force_correct_text_writer(
|
|
||||||
sys.stdout, encoding, errors, force_writable=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_text_stderr(encoding=None, errors=None):
|
|
||||||
rv = _get_windows_console_stream(sys.stderr, encoding, errors)
|
|
||||||
if rv is not None:
|
|
||||||
return rv
|
|
||||||
return _force_correct_text_writer(
|
|
||||||
sys.stderr, encoding, errors, force_writable=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def filename_to_ui(value):
|
|
||||||
if isinstance(value, bytes):
|
|
||||||
value = value.decode(get_filesystem_encoding(), "replace")
|
|
||||||
else:
|
|
||||||
value = value.encode("utf-8", "surrogateescape").decode("utf-8", "replace")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def get_streerror(e, default=None):
|
def _force_correct_text_reader(
|
||||||
if hasattr(e, "strerror"):
|
text_reader: t.IO,
|
||||||
msg = e.strerror
|
encoding: t.Optional[str],
|
||||||
else:
|
errors: t.Optional[str],
|
||||||
if default is not None:
|
force_readable: bool = False,
|
||||||
msg = default
|
) -> t.TextIO:
|
||||||
else:
|
return _force_correct_text_stream(
|
||||||
msg = str(e)
|
text_reader,
|
||||||
if isinstance(msg, bytes):
|
encoding,
|
||||||
msg = msg.decode("utf-8", "replace")
|
errors,
|
||||||
return msg
|
_is_binary_reader,
|
||||||
|
_find_binary_reader,
|
||||||
|
force_readable=force_readable,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _wrap_io_open(file, mode, encoding, errors):
|
def _force_correct_text_writer(
|
||||||
"""On Python 2, :func:`io.open` returns a text file wrapper that
|
text_writer: t.IO,
|
||||||
requires passing ``unicode`` to ``write``. Need to open the file in
|
encoding: t.Optional[str],
|
||||||
binary mode then wrap it in a subclass that can write ``str`` and
|
errors: t.Optional[str],
|
||||||
``unicode``.
|
force_writable: bool = False,
|
||||||
|
) -> t.TextIO:
|
||||||
Also handles not passing ``encoding`` and ``errors`` in binary mode.
|
return _force_correct_text_stream(
|
||||||
"""
|
text_writer,
|
||||||
binary = "b" in mode
|
encoding,
|
||||||
|
errors,
|
||||||
if binary:
|
_is_binary_writer,
|
||||||
kwargs = {}
|
_find_binary_writer,
|
||||||
else:
|
force_writable=force_writable,
|
||||||
kwargs = {"encoding": encoding, "errors": errors}
|
)
|
||||||
|
|
||||||
if not PY2 or binary:
|
|
||||||
return io.open(file, mode, **kwargs)
|
|
||||||
|
|
||||||
f = io.open(file, "{}b".format(mode.replace("t", "")))
|
|
||||||
return _make_text_stream(f, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False):
|
def get_binary_stdin() -> t.BinaryIO:
|
||||||
|
reader = _find_binary_reader(sys.stdin)
|
||||||
|
if reader is None:
|
||||||
|
raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
|
||||||
|
return reader
|
||||||
|
|
||||||
|
|
||||||
|
def get_binary_stdout() -> t.BinaryIO:
|
||||||
|
writer = _find_binary_writer(sys.stdout)
|
||||||
|
if writer is None:
|
||||||
|
raise RuntimeError("Was not able to determine binary stream for sys.stdout.")
|
||||||
|
return writer
|
||||||
|
|
||||||
|
|
||||||
|
def get_binary_stderr() -> t.BinaryIO:
|
||||||
|
writer = _find_binary_writer(sys.stderr)
|
||||||
|
if writer is None:
|
||||||
|
raise RuntimeError("Was not able to determine binary stream for sys.stderr.")
|
||||||
|
return writer
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_stdin(
|
||||||
|
encoding: t.Optional[str] = None, errors: t.Optional[str] = None
|
||||||
|
) -> t.TextIO:
|
||||||
|
rv = _get_windows_console_stream(sys.stdin, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_stdout(
|
||||||
|
encoding: t.Optional[str] = None, errors: t.Optional[str] = None
|
||||||
|
) -> t.TextIO:
|
||||||
|
rv = _get_windows_console_stream(sys.stdout, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_stderr(
|
||||||
|
encoding: t.Optional[str] = None, errors: t.Optional[str] = None
|
||||||
|
) -> t.TextIO:
|
||||||
|
rv = _get_windows_console_stream(sys.stderr, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_io_open(
|
||||||
|
file: t.Union[str, os.PathLike, int],
|
||||||
|
mode: str,
|
||||||
|
encoding: t.Optional[str],
|
||||||
|
errors: t.Optional[str],
|
||||||
|
) -> t.IO:
|
||||||
|
"""Handles not passing ``encoding`` and ``errors`` in binary mode."""
|
||||||
|
if "b" in mode:
|
||||||
|
return open(file, mode)
|
||||||
|
|
||||||
|
return open(file, mode, encoding=encoding, errors=errors)
|
||||||
|
|
||||||
|
|
||||||
|
def open_stream(
|
||||||
|
filename: str,
|
||||||
|
mode: str = "r",
|
||||||
|
encoding: t.Optional[str] = None,
|
||||||
|
errors: t.Optional[str] = "strict",
|
||||||
|
atomic: bool = False,
|
||||||
|
) -> t.Tuple[t.IO, bool]:
|
||||||
binary = "b" in mode
|
binary = "b" in mode
|
||||||
|
|
||||||
# Standard streams first. These are simple because they don't need
|
# Standard streams first. These are simple because they don't need
|
||||||
|
@ -549,7 +424,7 @@ def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False
|
||||||
import random
|
import random
|
||||||
|
|
||||||
try:
|
try:
|
||||||
perm = os.stat(filename).st_mode
|
perm: t.Optional[int] = os.stat(filename).st_mode
|
||||||
except OSError:
|
except OSError:
|
||||||
perm = None
|
perm = None
|
||||||
|
|
||||||
|
@ -561,7 +436,7 @@ def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False
|
||||||
while True:
|
while True:
|
||||||
tmp_filename = os.path.join(
|
tmp_filename = os.path.join(
|
||||||
os.path.dirname(filename),
|
os.path.dirname(filename),
|
||||||
".__atomic-write{:08x}".format(random.randrange(1 << 32)),
|
f".__atomic-write{random.randrange(1 << 32):08x}",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
|
fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
|
||||||
|
@ -580,76 +455,55 @@ def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False
|
||||||
os.chmod(tmp_filename, perm) # in case perm includes bits in umask
|
os.chmod(tmp_filename, perm) # in case perm includes bits in umask
|
||||||
|
|
||||||
f = _wrap_io_open(fd, mode, encoding, errors)
|
f = _wrap_io_open(fd, mode, encoding, errors)
|
||||||
return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True
|
af = _AtomicFile(f, tmp_filename, os.path.realpath(filename))
|
||||||
|
return t.cast(t.IO, af), True
|
||||||
|
|
||||||
|
|
||||||
# Used in a destructor call, needs extra protection from interpreter cleanup.
|
class _AtomicFile:
|
||||||
if hasattr(os, "replace"):
|
def __init__(self, f: t.IO, tmp_filename: str, real_filename: str) -> None:
|
||||||
_replace = os.replace
|
|
||||||
_can_replace = True
|
|
||||||
else:
|
|
||||||
_replace = os.rename
|
|
||||||
_can_replace = not WIN
|
|
||||||
|
|
||||||
|
|
||||||
class _AtomicFile(object):
|
|
||||||
def __init__(self, f, tmp_filename, real_filename):
|
|
||||||
self._f = f
|
self._f = f
|
||||||
self._tmp_filename = tmp_filename
|
self._tmp_filename = tmp_filename
|
||||||
self._real_filename = real_filename
|
self._real_filename = real_filename
|
||||||
self.closed = False
|
self.closed = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
return self._real_filename
|
return self._real_filename
|
||||||
|
|
||||||
def close(self, delete=False):
|
def close(self, delete: bool = False) -> None:
|
||||||
if self.closed:
|
if self.closed:
|
||||||
return
|
return
|
||||||
self._f.close()
|
self._f.close()
|
||||||
if not _can_replace:
|
os.replace(self._tmp_filename, self._real_filename)
|
||||||
try:
|
|
||||||
os.remove(self._real_filename)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
_replace(self._tmp_filename, self._real_filename)
|
|
||||||
self.closed = True
|
self.closed = True
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
return getattr(self._f, name)
|
return getattr(self._f, name)
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> "_AtomicFile":
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb):
|
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||||
self.close(delete=exc_type is not None)
|
self.close(delete=exc_type is not None)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return repr(self._f)
|
return repr(self._f)
|
||||||
|
|
||||||
|
|
||||||
auto_wrap_for_ansi = None
|
def strip_ansi(value: str) -> str:
|
||||||
colorama = None
|
|
||||||
get_winterm_size = None
|
|
||||||
|
|
||||||
|
|
||||||
def strip_ansi(value):
|
|
||||||
return _ansi_re.sub("", value)
|
return _ansi_re.sub("", value)
|
||||||
|
|
||||||
|
|
||||||
def _is_jupyter_kernel_output(stream):
|
def _is_jupyter_kernel_output(stream: t.IO) -> bool:
|
||||||
if WIN:
|
|
||||||
# TODO: Couldn't test on Windows, should't try to support until
|
|
||||||
# someone tests the details wrt colorama.
|
|
||||||
return
|
|
||||||
|
|
||||||
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
|
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
|
||||||
stream = stream._stream
|
stream = stream._stream
|
||||||
|
|
||||||
return stream.__class__.__module__.startswith("ipykernel.")
|
return stream.__class__.__module__.startswith("ipykernel.")
|
||||||
|
|
||||||
|
|
||||||
def should_strip_ansi(stream=None, color=None):
|
def should_strip_ansi(
|
||||||
|
stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None
|
||||||
|
) -> bool:
|
||||||
if color is None:
|
if color is None:
|
||||||
if stream is None:
|
if stream is None:
|
||||||
stream = sys.stdin
|
stream = sys.stdin
|
||||||
|
@ -657,99 +511,85 @@ def should_strip_ansi(stream=None, color=None):
|
||||||
return not color
|
return not color
|
||||||
|
|
||||||
|
|
||||||
# If we're on Windows, we provide transparent integration through
|
# On Windows, wrap the output streams with colorama to support ANSI
|
||||||
# colorama. This will make ANSI colors through the echo function
|
# color codes.
|
||||||
# work automatically.
|
# NOTE: double check is needed so mypy does not analyze this on Linux
|
||||||
if WIN:
|
if sys.platform.startswith("win") and WIN:
|
||||||
# Windows has a smaller terminal
|
from ._winconsole import _get_windows_console_stream
|
||||||
DEFAULT_COLUMNS = 79
|
|
||||||
|
|
||||||
from ._winconsole import _get_windows_console_stream, _wrap_std_stream
|
def _get_argv_encoding() -> str:
|
||||||
|
|
||||||
def _get_argv_encoding():
|
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
return locale.getpreferredencoding()
|
return locale.getpreferredencoding()
|
||||||
|
|
||||||
if PY2:
|
_ansi_stream_wrappers: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
|
||||||
|
|
||||||
def raw_input(prompt=""):
|
def auto_wrap_for_ansi(
|
||||||
sys.stderr.flush()
|
stream: t.TextIO, color: t.Optional[bool] = None
|
||||||
if prompt:
|
) -> t.TextIO:
|
||||||
stdout = _default_text_stdout()
|
"""Support ANSI color and style codes on Windows by wrapping a
|
||||||
stdout.write(prompt)
|
stream with colorama.
|
||||||
stdin = _default_text_stdin()
|
"""
|
||||||
return stdin.readline().rstrip("\r\n")
|
try:
|
||||||
|
cached = _ansi_stream_wrappers.get(stream)
|
||||||
|
except Exception:
|
||||||
|
cached = None
|
||||||
|
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
try:
|
|
||||||
import colorama
|
import colorama
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
_ansi_stream_wrappers = WeakKeyDictionary()
|
|
||||||
|
|
||||||
def auto_wrap_for_ansi(stream, color=None):
|
strip = should_strip_ansi(stream, color)
|
||||||
"""This function wraps a stream so that calls through colorama
|
ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
|
||||||
are issued to the win32 console API to recolor on demand. It
|
rv = t.cast(t.TextIO, ansi_wrapper.stream)
|
||||||
also ensures to reset the colors if a write call is interrupted
|
_write = rv.write
|
||||||
to not destroy the console afterwards.
|
|
||||||
"""
|
def _safe_write(s):
|
||||||
try:
|
try:
|
||||||
cached = _ansi_stream_wrappers.get(stream)
|
return _write(s)
|
||||||
except Exception:
|
except BaseException:
|
||||||
cached = None
|
ansi_wrapper.reset_all()
|
||||||
if cached is not None:
|
raise
|
||||||
return cached
|
|
||||||
strip = should_strip_ansi(stream, color)
|
|
||||||
ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
|
|
||||||
rv = ansi_wrapper.stream
|
|
||||||
_write = rv.write
|
|
||||||
|
|
||||||
def _safe_write(s):
|
rv.write = _safe_write
|
||||||
try:
|
|
||||||
return _write(s)
|
|
||||||
except:
|
|
||||||
ansi_wrapper.reset_all()
|
|
||||||
raise
|
|
||||||
|
|
||||||
rv.write = _safe_write
|
try:
|
||||||
try:
|
_ansi_stream_wrappers[stream] = rv
|
||||||
_ansi_stream_wrappers[stream] = rv
|
except Exception:
|
||||||
except Exception:
|
pass
|
||||||
pass
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def get_winterm_size():
|
return rv
|
||||||
win = colorama.win32.GetConsoleScreenBufferInfo(
|
|
||||||
colorama.win32.STDOUT
|
|
||||||
).srWindow
|
|
||||||
return win.Right - win.Left, win.Bottom - win.Top
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def _get_argv_encoding():
|
def _get_argv_encoding() -> str:
|
||||||
return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding()
|
return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding()
|
||||||
|
|
||||||
_get_windows_console_stream = lambda *x: None
|
def _get_windows_console_stream(
|
||||||
_wrap_std_stream = lambda *x: None
|
f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str]
|
||||||
|
) -> t.Optional[t.TextIO]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def term_len(x):
|
def term_len(x: str) -> int:
|
||||||
return len(strip_ansi(x))
|
return len(strip_ansi(x))
|
||||||
|
|
||||||
|
|
||||||
def isatty(stream):
|
def isatty(stream: t.IO) -> bool:
|
||||||
try:
|
try:
|
||||||
return stream.isatty()
|
return stream.isatty()
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _make_cached_stream_func(src_func, wrapper_func):
|
def _make_cached_stream_func(
|
||||||
cache = WeakKeyDictionary()
|
src_func: t.Callable[[], t.TextIO], wrapper_func: t.Callable[[], t.TextIO]
|
||||||
|
) -> t.Callable[[], t.TextIO]:
|
||||||
|
cache: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
|
||||||
|
|
||||||
def func():
|
def func() -> t.TextIO:
|
||||||
stream = src_func()
|
stream = src_func()
|
||||||
try:
|
try:
|
||||||
rv = cache.get(stream)
|
rv = cache.get(stream)
|
||||||
|
@ -759,7 +599,6 @@ def _make_cached_stream_func(src_func, wrapper_func):
|
||||||
return rv
|
return rv
|
||||||
rv = wrapper_func()
|
rv = wrapper_func()
|
||||||
try:
|
try:
|
||||||
stream = src_func() # In case wrapper_func() modified the stream
|
|
||||||
cache[stream] = rv
|
cache[stream] = rv
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
@ -773,13 +612,15 @@ _default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_std
|
||||||
_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
|
_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
|
||||||
|
|
||||||
|
|
||||||
binary_streams = {
|
binary_streams: t.Mapping[str, t.Callable[[], t.BinaryIO]] = {
|
||||||
"stdin": get_binary_stdin,
|
"stdin": get_binary_stdin,
|
||||||
"stdout": get_binary_stdout,
|
"stdout": get_binary_stdout,
|
||||||
"stderr": get_binary_stderr,
|
"stderr": get_binary_stderr,
|
||||||
}
|
}
|
||||||
|
|
||||||
text_streams = {
|
text_streams: t.Mapping[
|
||||||
|
str, t.Callable[[t.Optional[str], t.Optional[str]], t.TextIO]
|
||||||
|
] = {
|
||||||
"stdin": get_text_stdin,
|
"stdin": get_text_stdin,
|
||||||
"stdout": get_text_stdout,
|
"stdout": get_text_stdout,
|
||||||
"stderr": get_text_stderr,
|
"stderr": get_text_stderr,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
"""
|
||||||
This module contains implementations for the termui module. To keep the
|
This module contains implementations for the termui module. To keep the
|
||||||
import time of Click down, some infrequently used functionality is
|
import time of Click down, some infrequently used functionality is
|
||||||
|
@ -9,20 +8,22 @@ import math
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import typing as t
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
from ._compat import _default_text_stdout
|
from ._compat import _default_text_stdout
|
||||||
from ._compat import CYGWIN
|
from ._compat import CYGWIN
|
||||||
from ._compat import get_best_encoding
|
from ._compat import get_best_encoding
|
||||||
from ._compat import int_types
|
|
||||||
from ._compat import isatty
|
from ._compat import isatty
|
||||||
from ._compat import open_stream
|
from ._compat import open_stream
|
||||||
from ._compat import range_type
|
|
||||||
from ._compat import strip_ansi
|
from ._compat import strip_ansi
|
||||||
from ._compat import term_len
|
from ._compat import term_len
|
||||||
from ._compat import WIN
|
from ._compat import WIN
|
||||||
from .exceptions import ClickException
|
from .exceptions import ClickException
|
||||||
from .utils import echo
|
from .utils import echo
|
||||||
|
|
||||||
|
V = t.TypeVar("V")
|
||||||
|
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
BEFORE_BAR = "\r"
|
BEFORE_BAR = "\r"
|
||||||
AFTER_BAR = "\n"
|
AFTER_BAR = "\n"
|
||||||
|
@ -31,42 +32,25 @@ else:
|
||||||
AFTER_BAR = "\033[?25h\n"
|
AFTER_BAR = "\033[?25h\n"
|
||||||
|
|
||||||
|
|
||||||
def _length_hint(obj):
|
class ProgressBar(t.Generic[V]):
|
||||||
"""Returns the length hint of an object."""
|
|
||||||
try:
|
|
||||||
return len(obj)
|
|
||||||
except (AttributeError, TypeError):
|
|
||||||
try:
|
|
||||||
get_hint = type(obj).__length_hint__
|
|
||||||
except AttributeError:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
hint = get_hint(obj)
|
|
||||||
except TypeError:
|
|
||||||
return None
|
|
||||||
if hint is NotImplemented or not isinstance(hint, int_types) or hint < 0:
|
|
||||||
return None
|
|
||||||
return hint
|
|
||||||
|
|
||||||
|
|
||||||
class ProgressBar(object):
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
iterable,
|
iterable: t.Optional[t.Iterable[V]],
|
||||||
length=None,
|
length: t.Optional[int] = None,
|
||||||
fill_char="#",
|
fill_char: str = "#",
|
||||||
empty_char=" ",
|
empty_char: str = " ",
|
||||||
bar_template="%(bar)s",
|
bar_template: str = "%(bar)s",
|
||||||
info_sep=" ",
|
info_sep: str = " ",
|
||||||
show_eta=True,
|
show_eta: bool = True,
|
||||||
show_percent=None,
|
show_percent: t.Optional[bool] = None,
|
||||||
show_pos=False,
|
show_pos: bool = False,
|
||||||
item_show_func=None,
|
item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None,
|
||||||
label=None,
|
label: t.Optional[str] = None,
|
||||||
file=None,
|
file: t.Optional[t.TextIO] = None,
|
||||||
color=None,
|
color: t.Optional[bool] = None,
|
||||||
width=30,
|
update_min_steps: int = 1,
|
||||||
):
|
width: int = 30,
|
||||||
|
) -> None:
|
||||||
self.fill_char = fill_char
|
self.fill_char = fill_char
|
||||||
self.empty_char = empty_char
|
self.empty_char = empty_char
|
||||||
self.bar_template = bar_template
|
self.bar_template = bar_template
|
||||||
|
@ -80,45 +64,50 @@ class ProgressBar(object):
|
||||||
file = _default_text_stdout()
|
file = _default_text_stdout()
|
||||||
self.file = file
|
self.file = file
|
||||||
self.color = color
|
self.color = color
|
||||||
|
self.update_min_steps = update_min_steps
|
||||||
|
self._completed_intervals = 0
|
||||||
self.width = width
|
self.width = width
|
||||||
self.autowidth = width == 0
|
self.autowidth = width == 0
|
||||||
|
|
||||||
if length is None:
|
if length is None:
|
||||||
length = _length_hint(iterable)
|
from operator import length_hint
|
||||||
|
|
||||||
|
length = length_hint(iterable, -1)
|
||||||
|
|
||||||
|
if length == -1:
|
||||||
|
length = None
|
||||||
if iterable is None:
|
if iterable is None:
|
||||||
if length is None:
|
if length is None:
|
||||||
raise TypeError("iterable or length is required")
|
raise TypeError("iterable or length is required")
|
||||||
iterable = range_type(length)
|
iterable = t.cast(t.Iterable[V], range(length))
|
||||||
self.iter = iter(iterable)
|
self.iter = iter(iterable)
|
||||||
self.length = length
|
self.length = length
|
||||||
self.length_known = length is not None
|
|
||||||
self.pos = 0
|
self.pos = 0
|
||||||
self.avg = []
|
self.avg: t.List[float] = []
|
||||||
self.start = self.last_eta = time.time()
|
self.start = self.last_eta = time.time()
|
||||||
self.eta_known = False
|
self.eta_known = False
|
||||||
self.finished = False
|
self.finished = False
|
||||||
self.max_width = None
|
self.max_width: t.Optional[int] = None
|
||||||
self.entered = False
|
self.entered = False
|
||||||
self.current_item = None
|
self.current_item: t.Optional[V] = None
|
||||||
self.is_hidden = not isatty(self.file)
|
self.is_hidden = not isatty(self.file)
|
||||||
self._last_line = None
|
self._last_line: t.Optional[str] = None
|
||||||
self.short_limit = 0.5
|
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> "ProgressBar":
|
||||||
self.entered = True
|
self.entered = True
|
||||||
self.render_progress()
|
self.render_progress()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb):
|
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||||
self.render_finish()
|
self.render_finish()
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self) -> t.Iterator[V]:
|
||||||
if not self.entered:
|
if not self.entered:
|
||||||
raise RuntimeError("You need to use progress bars in a with block.")
|
raise RuntimeError("You need to use progress bars in a with block.")
|
||||||
self.render_progress()
|
self.render_progress()
|
||||||
return self.generator()
|
return self.generator()
|
||||||
|
|
||||||
def __next__(self):
|
def __next__(self) -> V:
|
||||||
# Iteration is defined in terms of a generator function,
|
# Iteration is defined in terms of a generator function,
|
||||||
# returned by iter(self); use that to define next(). This works
|
# returned by iter(self); use that to define next(). This works
|
||||||
# because `self.iter` is an iterable consumed by that generator,
|
# because `self.iter` is an iterable consumed by that generator,
|
||||||
|
@ -126,37 +115,31 @@ class ProgressBar(object):
|
||||||
# twice works and does "what you want".
|
# twice works and does "what you want".
|
||||||
return next(iter(self))
|
return next(iter(self))
|
||||||
|
|
||||||
# Python 2 compat
|
def render_finish(self) -> None:
|
||||||
next = __next__
|
if self.is_hidden:
|
||||||
|
|
||||||
def is_fast(self):
|
|
||||||
return time.time() - self.start <= self.short_limit
|
|
||||||
|
|
||||||
def render_finish(self):
|
|
||||||
if self.is_hidden or self.is_fast():
|
|
||||||
return
|
return
|
||||||
self.file.write(AFTER_BAR)
|
self.file.write(AFTER_BAR)
|
||||||
self.file.flush()
|
self.file.flush()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pct(self):
|
def pct(self) -> float:
|
||||||
if self.finished:
|
if self.finished:
|
||||||
return 1.0
|
return 1.0
|
||||||
return min(self.pos / (float(self.length) or 1), 1.0)
|
return min(self.pos / (float(self.length or 1) or 1), 1.0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def time_per_iteration(self):
|
def time_per_iteration(self) -> float:
|
||||||
if not self.avg:
|
if not self.avg:
|
||||||
return 0.0
|
return 0.0
|
||||||
return sum(self.avg) / float(len(self.avg))
|
return sum(self.avg) / float(len(self.avg))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def eta(self):
|
def eta(self) -> float:
|
||||||
if self.length_known and not self.finished:
|
if self.length is not None and not self.finished:
|
||||||
return self.time_per_iteration * (self.length - self.pos)
|
return self.time_per_iteration * (self.length - self.pos)
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
def format_eta(self):
|
def format_eta(self) -> str:
|
||||||
if self.eta_known:
|
if self.eta_known:
|
||||||
t = int(self.eta)
|
t = int(self.eta)
|
||||||
seconds = t % 60
|
seconds = t % 60
|
||||||
|
@ -166,44 +149,44 @@ class ProgressBar(object):
|
||||||
hours = t % 24
|
hours = t % 24
|
||||||
t //= 24
|
t //= 24
|
||||||
if t > 0:
|
if t > 0:
|
||||||
return "{}d {:02}:{:02}:{:02}".format(t, hours, minutes, seconds)
|
return f"{t}d {hours:02}:{minutes:02}:{seconds:02}"
|
||||||
else:
|
else:
|
||||||
return "{:02}:{:02}:{:02}".format(hours, minutes, seconds)
|
return f"{hours:02}:{minutes:02}:{seconds:02}"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def format_pos(self):
|
def format_pos(self) -> str:
|
||||||
pos = str(self.pos)
|
pos = str(self.pos)
|
||||||
if self.length_known:
|
if self.length is not None:
|
||||||
pos += "/{}".format(self.length)
|
pos += f"/{self.length}"
|
||||||
return pos
|
return pos
|
||||||
|
|
||||||
def format_pct(self):
|
def format_pct(self) -> str:
|
||||||
return "{: 4}%".format(int(self.pct * 100))[1:]
|
return f"{int(self.pct * 100): 4}%"[1:]
|
||||||
|
|
||||||
def format_bar(self):
|
def format_bar(self) -> str:
|
||||||
if self.length_known:
|
if self.length is not None:
|
||||||
bar_length = int(self.pct * self.width)
|
bar_length = int(self.pct * self.width)
|
||||||
bar = self.fill_char * bar_length
|
bar = self.fill_char * bar_length
|
||||||
bar += self.empty_char * (self.width - bar_length)
|
bar += self.empty_char * (self.width - bar_length)
|
||||||
elif self.finished:
|
elif self.finished:
|
||||||
bar = self.fill_char * self.width
|
bar = self.fill_char * self.width
|
||||||
else:
|
else:
|
||||||
bar = list(self.empty_char * (self.width or 1))
|
chars = list(self.empty_char * (self.width or 1))
|
||||||
if self.time_per_iteration != 0:
|
if self.time_per_iteration != 0:
|
||||||
bar[
|
chars[
|
||||||
int(
|
int(
|
||||||
(math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
|
(math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
|
||||||
* self.width
|
* self.width
|
||||||
)
|
)
|
||||||
] = self.fill_char
|
] = self.fill_char
|
||||||
bar = "".join(bar)
|
bar = "".join(chars)
|
||||||
return bar
|
return bar
|
||||||
|
|
||||||
def format_progress_line(self):
|
def format_progress_line(self) -> str:
|
||||||
show_percent = self.show_percent
|
show_percent = self.show_percent
|
||||||
|
|
||||||
info_bits = []
|
info_bits = []
|
||||||
if self.length_known and show_percent is None:
|
if self.length is not None and show_percent is None:
|
||||||
show_percent = not self.show_pos
|
show_percent = not self.show_pos
|
||||||
|
|
||||||
if self.show_pos:
|
if self.show_pos:
|
||||||
|
@ -226,10 +209,16 @@ class ProgressBar(object):
|
||||||
}
|
}
|
||||||
).rstrip()
|
).rstrip()
|
||||||
|
|
||||||
def render_progress(self):
|
def render_progress(self) -> None:
|
||||||
from .termui import get_terminal_size
|
import shutil
|
||||||
|
|
||||||
if self.is_hidden:
|
if self.is_hidden:
|
||||||
|
# Only output the label as it changes if the output is not a
|
||||||
|
# TTY. Use file=stderr if you expect to be piping stdout.
|
||||||
|
if self._last_line != self.label:
|
||||||
|
self._last_line = self.label
|
||||||
|
echo(self.label, file=self.file, color=self.color)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
buf = []
|
buf = []
|
||||||
|
@ -238,10 +227,10 @@ class ProgressBar(object):
|
||||||
old_width = self.width
|
old_width = self.width
|
||||||
self.width = 0
|
self.width = 0
|
||||||
clutter_length = term_len(self.format_progress_line())
|
clutter_length = term_len(self.format_progress_line())
|
||||||
new_width = max(0, get_terminal_size()[0] - clutter_length)
|
new_width = max(0, shutil.get_terminal_size().columns - clutter_length)
|
||||||
if new_width < old_width:
|
if new_width < old_width:
|
||||||
buf.append(BEFORE_BAR)
|
buf.append(BEFORE_BAR)
|
||||||
buf.append(" " * self.max_width)
|
buf.append(" " * self.max_width) # type: ignore
|
||||||
self.max_width = new_width
|
self.max_width = new_width
|
||||||
self.width = new_width
|
self.width = new_width
|
||||||
|
|
||||||
|
@ -260,14 +249,14 @@ class ProgressBar(object):
|
||||||
line = "".join(buf)
|
line = "".join(buf)
|
||||||
# Render the line only if it changed.
|
# Render the line only if it changed.
|
||||||
|
|
||||||
if line != self._last_line and not self.is_fast():
|
if line != self._last_line:
|
||||||
self._last_line = line
|
self._last_line = line
|
||||||
echo(line, file=self.file, color=self.color, nl=False)
|
echo(line, file=self.file, color=self.color, nl=False)
|
||||||
self.file.flush()
|
self.file.flush()
|
||||||
|
|
||||||
def make_step(self, n_steps):
|
def make_step(self, n_steps: int) -> None:
|
||||||
self.pos += n_steps
|
self.pos += n_steps
|
||||||
if self.length_known and self.pos >= self.length:
|
if self.length is not None and self.pos >= self.length:
|
||||||
self.finished = True
|
self.finished = True
|
||||||
|
|
||||||
if (time.time() - self.last_eta) < 1.0:
|
if (time.time() - self.last_eta) < 1.0:
|
||||||
|
@ -285,18 +274,40 @@ class ProgressBar(object):
|
||||||
|
|
||||||
self.avg = self.avg[-6:] + [step]
|
self.avg = self.avg[-6:] + [step]
|
||||||
|
|
||||||
self.eta_known = self.length_known
|
self.eta_known = self.length is not None
|
||||||
|
|
||||||
def update(self, n_steps):
|
def update(self, n_steps: int, current_item: t.Optional[V] = None) -> None:
|
||||||
self.make_step(n_steps)
|
"""Update the progress bar by advancing a specified number of
|
||||||
self.render_progress()
|
steps, and optionally set the ``current_item`` for this new
|
||||||
|
position.
|
||||||
|
|
||||||
def finish(self):
|
:param n_steps: Number of steps to advance.
|
||||||
self.eta_known = 0
|
:param current_item: Optional item to set as ``current_item``
|
||||||
|
for the updated position.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the ``current_item`` optional parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Only render when the number of steps meets the
|
||||||
|
``update_min_steps`` threshold.
|
||||||
|
"""
|
||||||
|
if current_item is not None:
|
||||||
|
self.current_item = current_item
|
||||||
|
|
||||||
|
self._completed_intervals += n_steps
|
||||||
|
|
||||||
|
if self._completed_intervals >= self.update_min_steps:
|
||||||
|
self.make_step(self._completed_intervals)
|
||||||
|
self.render_progress()
|
||||||
|
self._completed_intervals = 0
|
||||||
|
|
||||||
|
def finish(self) -> None:
|
||||||
|
self.eta_known = False
|
||||||
self.current_item = None
|
self.current_item = None
|
||||||
self.finished = True
|
self.finished = True
|
||||||
|
|
||||||
def generator(self):
|
def generator(self) -> t.Iterator[V]:
|
||||||
"""Return a generator which yields the items added to the bar
|
"""Return a generator which yields the items added to the bar
|
||||||
during construction, and updates the progress bar *after* the
|
during construction, and updates the progress bar *after* the
|
||||||
yielded block returns.
|
yielded block returns.
|
||||||
|
@ -312,18 +323,25 @@ class ProgressBar(object):
|
||||||
raise RuntimeError("You need to use progress bars in a with block.")
|
raise RuntimeError("You need to use progress bars in a with block.")
|
||||||
|
|
||||||
if self.is_hidden:
|
if self.is_hidden:
|
||||||
for rv in self.iter:
|
yield from self.iter
|
||||||
yield rv
|
|
||||||
else:
|
else:
|
||||||
for rv in self.iter:
|
for rv in self.iter:
|
||||||
self.current_item = rv
|
self.current_item = rv
|
||||||
|
|
||||||
|
# This allows show_item_func to be updated before the
|
||||||
|
# item is processed. Only trigger at the beginning of
|
||||||
|
# the update interval.
|
||||||
|
if self._completed_intervals == 0:
|
||||||
|
self.render_progress()
|
||||||
|
|
||||||
yield rv
|
yield rv
|
||||||
self.update(1)
|
self.update(1)
|
||||||
|
|
||||||
self.finish()
|
self.finish()
|
||||||
self.render_progress()
|
self.render_progress()
|
||||||
|
|
||||||
|
|
||||||
def pager(generator, color=None):
|
def pager(generator: t.Iterable[str], color: t.Optional[bool] = None) -> None:
|
||||||
"""Decide what method to use for paging through text."""
|
"""Decide what method to use for paging through text."""
|
||||||
stdout = _default_text_stdout()
|
stdout = _default_text_stdout()
|
||||||
if not isatty(sys.stdin) or not isatty(stdout):
|
if not isatty(sys.stdin) or not isatty(stdout):
|
||||||
|
@ -345,14 +363,14 @@ def pager(generator, color=None):
|
||||||
fd, filename = tempfile.mkstemp()
|
fd, filename = tempfile.mkstemp()
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
try:
|
try:
|
||||||
if hasattr(os, "system") and os.system('more "{}"'.format(filename)) == 0:
|
if hasattr(os, "system") and os.system(f'more "{filename}"') == 0:
|
||||||
return _pipepager(generator, "more", color)
|
return _pipepager(generator, "more", color)
|
||||||
return _nullpager(stdout, generator, color)
|
return _nullpager(stdout, generator, color)
|
||||||
finally:
|
finally:
|
||||||
os.unlink(filename)
|
os.unlink(filename)
|
||||||
|
|
||||||
|
|
||||||
def _pipepager(generator, cmd, color):
|
def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> None:
|
||||||
"""Page through text by feeding it to another program. Invoking a
|
"""Page through text by feeding it to another program. Invoking a
|
||||||
pager through this might support colors.
|
pager through this might support colors.
|
||||||
"""
|
"""
|
||||||
|
@ -364,7 +382,7 @@ def _pipepager(generator, cmd, color):
|
||||||
# condition that
|
# condition that
|
||||||
cmd_detail = cmd.rsplit("/", 1)[-1].split()
|
cmd_detail = cmd.rsplit("/", 1)[-1].split()
|
||||||
if color is None and cmd_detail[0] == "less":
|
if color is None and cmd_detail[0] == "less":
|
||||||
less_flags = "{}{}".format(os.environ.get("LESS", ""), " ".join(cmd_detail[1:]))
|
less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_detail[1:])}"
|
||||||
if not less_flags:
|
if not less_flags:
|
||||||
env["LESS"] = "-R"
|
env["LESS"] = "-R"
|
||||||
color = True
|
color = True
|
||||||
|
@ -372,17 +390,18 @@ def _pipepager(generator, cmd, color):
|
||||||
color = True
|
color = True
|
||||||
|
|
||||||
c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env)
|
c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env)
|
||||||
encoding = get_best_encoding(c.stdin)
|
stdin = t.cast(t.BinaryIO, c.stdin)
|
||||||
|
encoding = get_best_encoding(stdin)
|
||||||
try:
|
try:
|
||||||
for text in generator:
|
for text in generator:
|
||||||
if not color:
|
if not color:
|
||||||
text = strip_ansi(text)
|
text = strip_ansi(text)
|
||||||
|
|
||||||
c.stdin.write(text.encode(encoding, "replace"))
|
stdin.write(text.encode(encoding, "replace"))
|
||||||
except (IOError, KeyboardInterrupt):
|
except (OSError, KeyboardInterrupt):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
c.stdin.close()
|
stdin.close()
|
||||||
|
|
||||||
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting
|
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting
|
||||||
# search or other commands inside less).
|
# search or other commands inside less).
|
||||||
|
@ -401,11 +420,13 @@ def _pipepager(generator, cmd, color):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def _tempfilepager(generator, cmd, color):
|
def _tempfilepager(
|
||||||
|
generator: t.Iterable[str], cmd: str, color: t.Optional[bool]
|
||||||
|
) -> None:
|
||||||
"""Page through text by invoking a program on a temporary file."""
|
"""Page through text by invoking a program on a temporary file."""
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
filename = tempfile.mktemp()
|
fd, filename = tempfile.mkstemp()
|
||||||
# TODO: This never terminates if the passed generator never terminates.
|
# TODO: This never terminates if the passed generator never terminates.
|
||||||
text = "".join(generator)
|
text = "".join(generator)
|
||||||
if not color:
|
if not color:
|
||||||
|
@ -414,12 +435,15 @@ def _tempfilepager(generator, cmd, color):
|
||||||
with open_stream(filename, "wb")[0] as f:
|
with open_stream(filename, "wb")[0] as f:
|
||||||
f.write(text.encode(encoding))
|
f.write(text.encode(encoding))
|
||||||
try:
|
try:
|
||||||
os.system('{} "{}"'.format(cmd, filename))
|
os.system(f'{cmd} "{filename}"')
|
||||||
finally:
|
finally:
|
||||||
|
os.close(fd)
|
||||||
os.unlink(filename)
|
os.unlink(filename)
|
||||||
|
|
||||||
|
|
||||||
def _nullpager(stream, generator, color):
|
def _nullpager(
|
||||||
|
stream: t.TextIO, generator: t.Iterable[str], color: t.Optional[bool]
|
||||||
|
) -> None:
|
||||||
"""Simply print unformatted text. This is the ultimate fallback."""
|
"""Simply print unformatted text. This is the ultimate fallback."""
|
||||||
for text in generator:
|
for text in generator:
|
||||||
if not color:
|
if not color:
|
||||||
|
@ -427,14 +451,20 @@ def _nullpager(stream, generator, color):
|
||||||
stream.write(text)
|
stream.write(text)
|
||||||
|
|
||||||
|
|
||||||
class Editor(object):
|
class Editor:
|
||||||
def __init__(self, editor=None, env=None, require_save=True, extension=".txt"):
|
def __init__(
|
||||||
|
self,
|
||||||
|
editor: t.Optional[str] = None,
|
||||||
|
env: t.Optional[t.Mapping[str, str]] = None,
|
||||||
|
require_save: bool = True,
|
||||||
|
extension: str = ".txt",
|
||||||
|
) -> None:
|
||||||
self.editor = editor
|
self.editor = editor
|
||||||
self.env = env
|
self.env = env
|
||||||
self.require_save = require_save
|
self.require_save = require_save
|
||||||
self.extension = extension
|
self.extension = extension
|
||||||
|
|
||||||
def get_editor(self):
|
def get_editor(self) -> str:
|
||||||
if self.editor is not None:
|
if self.editor is not None:
|
||||||
return self.editor
|
return self.editor
|
||||||
for key in "VISUAL", "EDITOR":
|
for key in "VISUAL", "EDITOR":
|
||||||
|
@ -444,48 +474,62 @@ class Editor(object):
|
||||||
if WIN:
|
if WIN:
|
||||||
return "notepad"
|
return "notepad"
|
||||||
for editor in "sensible-editor", "vim", "nano":
|
for editor in "sensible-editor", "vim", "nano":
|
||||||
if os.system("which {} >/dev/null 2>&1".format(editor)) == 0:
|
if os.system(f"which {editor} >/dev/null 2>&1") == 0:
|
||||||
return editor
|
return editor
|
||||||
return "vi"
|
return "vi"
|
||||||
|
|
||||||
def edit_file(self, filename):
|
def edit_file(self, filename: str) -> None:
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
editor = self.get_editor()
|
editor = self.get_editor()
|
||||||
|
environ: t.Optional[t.Dict[str, str]] = None
|
||||||
|
|
||||||
if self.env:
|
if self.env:
|
||||||
environ = os.environ.copy()
|
environ = os.environ.copy()
|
||||||
environ.update(self.env)
|
environ.update(self.env)
|
||||||
else:
|
|
||||||
environ = None
|
|
||||||
try:
|
try:
|
||||||
c = subprocess.Popen(
|
c = subprocess.Popen(f'{editor} "{filename}"', env=environ, shell=True)
|
||||||
'{} "{}"'.format(editor, filename), env=environ, shell=True,
|
|
||||||
)
|
|
||||||
exit_code = c.wait()
|
exit_code = c.wait()
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
raise ClickException("{}: Editing failed!".format(editor))
|
raise ClickException(
|
||||||
|
_("{editor}: Editing failed").format(editor=editor)
|
||||||
|
)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise ClickException("{}: Editing failed: {}".format(editor, e))
|
raise ClickException(
|
||||||
|
_("{editor}: Editing failed: {e}").format(editor=editor, e=e)
|
||||||
|
) from e
|
||||||
|
|
||||||
def edit(self, text):
|
def edit(self, text: t.Optional[t.AnyStr]) -> t.Optional[t.AnyStr]:
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
text = text or ""
|
if not text:
|
||||||
if text and not text.endswith("\n"):
|
data = b""
|
||||||
text += "\n"
|
elif isinstance(text, (bytes, bytearray)):
|
||||||
|
data = text
|
||||||
|
else:
|
||||||
|
if text and not text.endswith("\n"):
|
||||||
|
text += "\n"
|
||||||
|
|
||||||
|
if WIN:
|
||||||
|
data = text.replace("\n", "\r\n").encode("utf-8-sig")
|
||||||
|
else:
|
||||||
|
data = text.encode("utf-8")
|
||||||
|
|
||||||
fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
|
fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
|
||||||
try:
|
f: t.BinaryIO
|
||||||
if WIN:
|
|
||||||
encoding = "utf-8-sig"
|
|
||||||
text = text.replace("\n", "\r\n")
|
|
||||||
else:
|
|
||||||
encoding = "utf-8"
|
|
||||||
text = text.encode(encoding)
|
|
||||||
|
|
||||||
f = os.fdopen(fd, "wb")
|
try:
|
||||||
f.write(text)
|
with os.fdopen(fd, "wb") as f:
|
||||||
f.close()
|
f.write(data)
|
||||||
|
|
||||||
|
# If the filesystem resolution is 1 second, like Mac OS
|
||||||
|
# 10.12 Extended, or 2 seconds, like FAT32, and the editor
|
||||||
|
# closes very fast, require_save can fail. Set the modified
|
||||||
|
# time to be 2 seconds in the past to work around this.
|
||||||
|
os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2))
|
||||||
|
# Depending on the resolution, the exact value might not be
|
||||||
|
# recorded, so get the new recorded value.
|
||||||
timestamp = os.path.getmtime(name)
|
timestamp = os.path.getmtime(name)
|
||||||
|
|
||||||
self.edit_file(name)
|
self.edit_file(name)
|
||||||
|
@ -493,26 +537,26 @@ class Editor(object):
|
||||||
if self.require_save and os.path.getmtime(name) == timestamp:
|
if self.require_save and os.path.getmtime(name) == timestamp:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
f = open(name, "rb")
|
with open(name, "rb") as f:
|
||||||
try:
|
|
||||||
rv = f.read()
|
rv = f.read()
|
||||||
finally:
|
|
||||||
f.close()
|
if isinstance(text, (bytes, bytearray)):
|
||||||
return rv.decode("utf-8-sig").replace("\r\n", "\n")
|
return rv
|
||||||
|
|
||||||
|
return rv.decode("utf-8-sig").replace("\r\n", "\n") # type: ignore
|
||||||
finally:
|
finally:
|
||||||
os.unlink(name)
|
os.unlink(name)
|
||||||
|
|
||||||
|
|
||||||
def open_url(url, wait=False, locate=False):
|
def open_url(url: str, wait: bool = False, locate: bool = False) -> int:
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
def _unquote_file(url):
|
def _unquote_file(url: str) -> str:
|
||||||
try:
|
from urllib.parse import unquote
|
||||||
import urllib
|
|
||||||
except ImportError:
|
|
||||||
import urllib
|
|
||||||
if url.startswith("file://"):
|
if url.startswith("file://"):
|
||||||
url = urllib.unquote(url[7:])
|
url = unquote(url[7:])
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
|
||||||
if sys.platform == "darwin":
|
if sys.platform == "darwin":
|
||||||
|
@ -529,19 +573,21 @@ def open_url(url, wait=False, locate=False):
|
||||||
null.close()
|
null.close()
|
||||||
elif WIN:
|
elif WIN:
|
||||||
if locate:
|
if locate:
|
||||||
url = _unquote_file(url)
|
url = _unquote_file(url.replace('"', ""))
|
||||||
args = 'explorer /select,"{}"'.format(_unquote_file(url.replace('"', "")))
|
args = f'explorer /select,"{url}"'
|
||||||
else:
|
else:
|
||||||
args = 'start {} "" "{}"'.format(
|
url = url.replace('"', "")
|
||||||
"/WAIT" if wait else "", url.replace('"', "")
|
wait_str = "/WAIT" if wait else ""
|
||||||
)
|
args = f'start {wait_str} "" "{url}"'
|
||||||
return os.system(args)
|
return os.system(args)
|
||||||
elif CYGWIN:
|
elif CYGWIN:
|
||||||
if locate:
|
if locate:
|
||||||
url = _unquote_file(url)
|
url = os.path.dirname(_unquote_file(url).replace('"', ""))
|
||||||
args = 'cygstart "{}"'.format(os.path.dirname(url).replace('"', ""))
|
args = f'cygstart "{url}"'
|
||||||
else:
|
else:
|
||||||
args = 'cygstart {} "{}"'.format("-w" if wait else "", url.replace('"', ""))
|
url = url.replace('"', "")
|
||||||
|
wait_str = "-w" if wait else ""
|
||||||
|
args = f'cygstart {wait_str} "{url}"'
|
||||||
return os.system(args)
|
return os.system(args)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -562,23 +608,27 @@ def open_url(url, wait=False, locate=False):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
def _translate_ch_to_exc(ch):
|
def _translate_ch_to_exc(ch: str) -> t.Optional[BaseException]:
|
||||||
if ch == u"\x03":
|
if ch == "\x03":
|
||||||
raise KeyboardInterrupt()
|
raise KeyboardInterrupt()
|
||||||
if ch == u"\x04" and not WIN: # Unix-like, Ctrl+D
|
|
||||||
|
if ch == "\x04" and not WIN: # Unix-like, Ctrl+D
|
||||||
raise EOFError()
|
raise EOFError()
|
||||||
if ch == u"\x1a" and WIN: # Windows, Ctrl+Z
|
|
||||||
|
if ch == "\x1a" and WIN: # Windows, Ctrl+Z
|
||||||
raise EOFError()
|
raise EOFError()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
if WIN:
|
if WIN:
|
||||||
import msvcrt
|
import msvcrt
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def raw_terminal():
|
def raw_terminal() -> t.Iterator[int]:
|
||||||
yield
|
yield -1
|
||||||
|
|
||||||
def getchar(echo):
|
def getchar(echo: bool) -> str:
|
||||||
# The function `getch` will return a bytes object corresponding to
|
# The function `getch` will return a bytes object corresponding to
|
||||||
# the pressed character. Since Windows 10 build 1803, it will also
|
# the pressed character. Since Windows 10 build 1803, it will also
|
||||||
# return \x00 when called a second time after pressing a regular key.
|
# return \x00 when called a second time after pressing a regular key.
|
||||||
|
@ -608,16 +658,20 @@ if WIN:
|
||||||
#
|
#
|
||||||
# Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
|
# Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
|
||||||
# is doing the right thing in more situations than with `getch`.
|
# is doing the right thing in more situations than with `getch`.
|
||||||
|
func: t.Callable[[], str]
|
||||||
|
|
||||||
if echo:
|
if echo:
|
||||||
func = msvcrt.getwche
|
func = msvcrt.getwche # type: ignore
|
||||||
else:
|
else:
|
||||||
func = msvcrt.getwch
|
func = msvcrt.getwch # type: ignore
|
||||||
|
|
||||||
rv = func()
|
rv = func()
|
||||||
if rv in (u"\x00", u"\xe0"):
|
|
||||||
|
if rv in ("\x00", "\xe0"):
|
||||||
# \x00 and \xe0 are control characters that indicate special key,
|
# \x00 and \xe0 are control characters that indicate special key,
|
||||||
# see above.
|
# see above.
|
||||||
rv += func()
|
rv += func()
|
||||||
|
|
||||||
_translate_ch_to_exc(rv)
|
_translate_ch_to_exc(rv)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
@ -627,31 +681,38 @@ else:
|
||||||
import termios
|
import termios
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def raw_terminal():
|
def raw_terminal() -> t.Iterator[int]:
|
||||||
|
f: t.Optional[t.TextIO]
|
||||||
|
fd: int
|
||||||
|
|
||||||
if not isatty(sys.stdin):
|
if not isatty(sys.stdin):
|
||||||
f = open("/dev/tty")
|
f = open("/dev/tty")
|
||||||
fd = f.fileno()
|
fd = f.fileno()
|
||||||
else:
|
else:
|
||||||
fd = sys.stdin.fileno()
|
fd = sys.stdin.fileno()
|
||||||
f = None
|
f = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
old_settings = termios.tcgetattr(fd)
|
old_settings = termios.tcgetattr(fd)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tty.setraw(fd)
|
tty.setraw(fd)
|
||||||
yield fd
|
yield fd
|
||||||
finally:
|
finally:
|
||||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
if f is not None:
|
if f is not None:
|
||||||
f.close()
|
f.close()
|
||||||
except termios.error:
|
except termios.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def getchar(echo):
|
def getchar(echo: bool) -> str:
|
||||||
with raw_terminal() as fd:
|
with raw_terminal() as fd:
|
||||||
ch = os.read(fd, 32)
|
ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace")
|
||||||
ch = ch.decode(get_best_encoding(sys.stdin), "replace")
|
|
||||||
if echo and isatty(sys.stdout):
|
if echo and isatty(sys.stdout):
|
||||||
sys.stdout.write(ch)
|
sys.stdout.write(ch)
|
||||||
|
|
||||||
_translate_ch_to_exc(ch)
|
_translate_ch_to_exc(ch)
|
||||||
return ch
|
return ch
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import textwrap
|
import textwrap
|
||||||
|
import typing as t
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
|
||||||
class TextWrapper(textwrap.TextWrapper):
|
class TextWrapper(textwrap.TextWrapper):
|
||||||
def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
|
def _handle_long_word(
|
||||||
|
self,
|
||||||
|
reversed_chunks: t.List[str],
|
||||||
|
cur_line: t.List[str],
|
||||||
|
cur_len: int,
|
||||||
|
width: int,
|
||||||
|
) -> None:
|
||||||
space_left = max(width - cur_len, 1)
|
space_left = max(width - cur_len, 1)
|
||||||
|
|
||||||
if self.break_long_words:
|
if self.break_long_words:
|
||||||
|
@ -16,22 +23,27 @@ class TextWrapper(textwrap.TextWrapper):
|
||||||
cur_line.append(reversed_chunks.pop())
|
cur_line.append(reversed_chunks.pop())
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def extra_indent(self, indent):
|
def extra_indent(self, indent: str) -> t.Iterator[None]:
|
||||||
old_initial_indent = self.initial_indent
|
old_initial_indent = self.initial_indent
|
||||||
old_subsequent_indent = self.subsequent_indent
|
old_subsequent_indent = self.subsequent_indent
|
||||||
self.initial_indent += indent
|
self.initial_indent += indent
|
||||||
self.subsequent_indent += indent
|
self.subsequent_indent += indent
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
self.initial_indent = old_initial_indent
|
self.initial_indent = old_initial_indent
|
||||||
self.subsequent_indent = old_subsequent_indent
|
self.subsequent_indent = old_subsequent_indent
|
||||||
|
|
||||||
def indent_only(self, text):
|
def indent_only(self, text: str) -> str:
|
||||||
rv = []
|
rv = []
|
||||||
|
|
||||||
for idx, line in enumerate(text.splitlines()):
|
for idx, line in enumerate(text.splitlines()):
|
||||||
indent = self.initial_indent
|
indent = self.initial_indent
|
||||||
|
|
||||||
if idx > 0:
|
if idx > 0:
|
||||||
indent = self.subsequent_indent
|
indent = self.subsequent_indent
|
||||||
rv.append(indent + line)
|
|
||||||
|
rv.append(f"{indent}{line}")
|
||||||
|
|
||||||
return "\n".join(rv)
|
return "\n".join(rv)
|
||||||
|
|
|
@ -1,131 +1,100 @@
|
||||||
import codecs
|
import codecs
|
||||||
import os
|
import os
|
||||||
import sys
|
from gettext import gettext as _
|
||||||
|
|
||||||
from ._compat import PY2
|
|
||||||
|
|
||||||
|
|
||||||
def _find_unicode_literals_frame():
|
def _verify_python_env() -> None:
|
||||||
import __future__
|
"""Ensures that the environment is good for Unicode."""
|
||||||
|
|
||||||
if not hasattr(sys, "_getframe"): # not all Python implementations have it
|
|
||||||
return 0
|
|
||||||
frm = sys._getframe(1)
|
|
||||||
idx = 1
|
|
||||||
while frm is not None:
|
|
||||||
if frm.f_globals.get("__name__", "").startswith("click."):
|
|
||||||
frm = frm.f_back
|
|
||||||
idx += 1
|
|
||||||
elif frm.f_code.co_flags & __future__.unicode_literals.compiler_flag:
|
|
||||||
return idx
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def _check_for_unicode_literals():
|
|
||||||
if not __debug__:
|
|
||||||
return
|
|
||||||
|
|
||||||
from . import disable_unicode_literals_warning
|
|
||||||
|
|
||||||
if not PY2 or disable_unicode_literals_warning:
|
|
||||||
return
|
|
||||||
bad_frame = _find_unicode_literals_frame()
|
|
||||||
if bad_frame <= 0:
|
|
||||||
return
|
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
warn(
|
|
||||||
Warning(
|
|
||||||
"Click detected the use of the unicode_literals __future__"
|
|
||||||
" import. This is heavily discouraged because it can"
|
|
||||||
" introduce subtle bugs in your code. You should instead"
|
|
||||||
' use explicit u"" literals for your unicode strings. For'
|
|
||||||
" more information see"
|
|
||||||
" https://click.palletsprojects.com/python3/"
|
|
||||||
),
|
|
||||||
stacklevel=bad_frame,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _verify_python3_env():
|
|
||||||
"""Ensures that the environment is good for unicode on Python 3."""
|
|
||||||
if PY2:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
import locale
|
from locale import getpreferredencoding
|
||||||
|
|
||||||
fs_enc = codecs.lookup(locale.getpreferredencoding()).name
|
fs_enc = codecs.lookup(getpreferredencoding()).name
|
||||||
except Exception:
|
except Exception:
|
||||||
fs_enc = "ascii"
|
fs_enc = "ascii"
|
||||||
|
|
||||||
if fs_enc != "ascii":
|
if fs_enc != "ascii":
|
||||||
return
|
return
|
||||||
|
|
||||||
extra = ""
|
extra = [
|
||||||
|
_(
|
||||||
|
"Click will abort further execution because Python was"
|
||||||
|
" configured to use ASCII as encoding for the environment."
|
||||||
|
" Consult https://click.palletsprojects.com/unicode-support/"
|
||||||
|
" for mitigation steps."
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
if os.name == "posix":
|
if os.name == "posix":
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rv = subprocess.Popen(
|
rv = subprocess.Popen(
|
||||||
["locale", "-a"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
["locale", "-a"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
encoding="ascii",
|
||||||
|
errors="replace",
|
||||||
).communicate()[0]
|
).communicate()[0]
|
||||||
except OSError:
|
except OSError:
|
||||||
rv = b""
|
rv = ""
|
||||||
|
|
||||||
good_locales = set()
|
good_locales = set()
|
||||||
has_c_utf8 = False
|
has_c_utf8 = False
|
||||||
|
|
||||||
# Make sure we're operating on text here.
|
|
||||||
if isinstance(rv, bytes):
|
|
||||||
rv = rv.decode("ascii", "replace")
|
|
||||||
|
|
||||||
for line in rv.splitlines():
|
for line in rv.splitlines():
|
||||||
locale = line.strip()
|
locale = line.strip()
|
||||||
|
|
||||||
if locale.lower().endswith((".utf-8", ".utf8")):
|
if locale.lower().endswith((".utf-8", ".utf8")):
|
||||||
good_locales.add(locale)
|
good_locales.add(locale)
|
||||||
|
|
||||||
if locale.lower() in ("c.utf8", "c.utf-8"):
|
if locale.lower() in ("c.utf8", "c.utf-8"):
|
||||||
has_c_utf8 = True
|
has_c_utf8 = True
|
||||||
|
|
||||||
extra += "\n\n"
|
|
||||||
if not good_locales:
|
if not good_locales:
|
||||||
extra += (
|
extra.append(
|
||||||
"Additional information: on this system no suitable"
|
_(
|
||||||
" UTF-8 locales were discovered. This most likely"
|
"Additional information: on this system no suitable"
|
||||||
" requires resolving by reconfiguring the locale"
|
" UTF-8 locales were discovered. This most likely"
|
||||||
" system."
|
" requires resolving by reconfiguring the locale"
|
||||||
|
" system."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
elif has_c_utf8:
|
elif has_c_utf8:
|
||||||
extra += (
|
extra.append(
|
||||||
"This system supports the C.UTF-8 locale which is"
|
_(
|
||||||
" recommended. You might be able to resolve your issue"
|
"This system supports the C.UTF-8 locale which is"
|
||||||
" by exporting the following environment variables:\n\n"
|
" recommended. You might be able to resolve your"
|
||||||
" export LC_ALL=C.UTF-8\n"
|
" issue by exporting the following environment"
|
||||||
" export LANG=C.UTF-8"
|
" variables:"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
extra.append(" export LC_ALL=C.UTF-8\n export LANG=C.UTF-8")
|
||||||
else:
|
else:
|
||||||
extra += (
|
extra.append(
|
||||||
"This system lists a couple of UTF-8 supporting locales"
|
_(
|
||||||
" that you can pick from. The following suitable"
|
"This system lists some UTF-8 supporting locales"
|
||||||
" locales were discovered: {}".format(", ".join(sorted(good_locales)))
|
" that you can pick from. The following suitable"
|
||||||
|
" locales were discovered: {locales}"
|
||||||
|
).format(locales=", ".join(sorted(good_locales)))
|
||||||
)
|
)
|
||||||
|
|
||||||
bad_locale = None
|
bad_locale = None
|
||||||
for locale in os.environ.get("LC_ALL"), os.environ.get("LANG"):
|
|
||||||
if locale and locale.lower().endswith((".utf-8", ".utf8")):
|
for env_locale in os.environ.get("LC_ALL"), os.environ.get("LANG"):
|
||||||
bad_locale = locale
|
if env_locale and env_locale.lower().endswith((".utf-8", ".utf8")):
|
||||||
if locale is not None:
|
bad_locale = env_locale
|
||||||
|
|
||||||
|
if env_locale is not None:
|
||||||
break
|
break
|
||||||
|
|
||||||
if bad_locale is not None:
|
if bad_locale is not None:
|
||||||
extra += (
|
extra.append(
|
||||||
"\n\nClick discovered that you exported a UTF-8 locale"
|
_(
|
||||||
" but the locale system could not pick up from it"
|
"Click discovered that you exported a UTF-8 locale"
|
||||||
" because it does not exist. The exported locale is"
|
" but the locale system could not pick up from it"
|
||||||
" '{}' but it is not supported".format(bad_locale)
|
" because it does not exist. The exported locale is"
|
||||||
|
" {locale!r} but it is not supported."
|
||||||
|
).format(locale=bad_locale)
|
||||||
)
|
)
|
||||||
|
|
||||||
raise RuntimeError(
|
raise RuntimeError("\n\n".join(extra))
|
||||||
"Click will abort further execution because Python 3 was"
|
|
||||||
" configured to use ASCII as encoding for the environment."
|
|
||||||
" Consult https://click.palletsprojects.com/python3/ for"
|
|
||||||
" mitigation steps.{}".format(extra)
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# This module is based on the excellent work by Adam Bartoš who
|
# This module is based on the excellent work by Adam Bartoš who
|
||||||
# provided a lot of what went into the implementation here in
|
# provided a lot of what went into the implementation here in
|
||||||
# the discussion to issue1602 in the Python bug tracker.
|
# the discussion to issue1602 in the Python bug tracker.
|
||||||
|
@ -6,13 +5,11 @@
|
||||||
# There are some general differences in regards to how this works
|
# There are some general differences in regards to how this works
|
||||||
# compared to the original patches as we do not need to patch
|
# compared to the original patches as we do not need to patch
|
||||||
# the entire interpreter but just work in our little world of
|
# the entire interpreter but just work in our little world of
|
||||||
# echo and prmopt.
|
# echo and prompt.
|
||||||
import ctypes
|
|
||||||
import io
|
import io
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import zlib
|
import typing as t
|
||||||
from ctypes import byref
|
from ctypes import byref
|
||||||
from ctypes import c_char
|
from ctypes import c_char
|
||||||
from ctypes import c_char_p
|
from ctypes import c_char_p
|
||||||
|
@ -22,28 +19,18 @@ from ctypes import c_ulong
|
||||||
from ctypes import c_void_p
|
from ctypes import c_void_p
|
||||||
from ctypes import POINTER
|
from ctypes import POINTER
|
||||||
from ctypes import py_object
|
from ctypes import py_object
|
||||||
from ctypes import windll
|
from ctypes import Structure
|
||||||
from ctypes import WinError
|
|
||||||
from ctypes import WINFUNCTYPE
|
|
||||||
from ctypes.wintypes import DWORD
|
from ctypes.wintypes import DWORD
|
||||||
from ctypes.wintypes import HANDLE
|
from ctypes.wintypes import HANDLE
|
||||||
from ctypes.wintypes import LPCWSTR
|
from ctypes.wintypes import LPCWSTR
|
||||||
from ctypes.wintypes import LPWSTR
|
from ctypes.wintypes import LPWSTR
|
||||||
|
|
||||||
import msvcrt
|
|
||||||
|
|
||||||
from ._compat import _NonClosingTextIOWrapper
|
from ._compat import _NonClosingTextIOWrapper
|
||||||
from ._compat import PY2
|
|
||||||
from ._compat import text_type
|
|
||||||
|
|
||||||
try:
|
|
||||||
from ctypes import pythonapi
|
|
||||||
|
|
||||||
PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
|
|
||||||
PyBuffer_Release = pythonapi.PyBuffer_Release
|
|
||||||
except ImportError:
|
|
||||||
pythonapi = None
|
|
||||||
|
|
||||||
|
assert sys.platform == "win32"
|
||||||
|
import msvcrt # noqa: E402
|
||||||
|
from ctypes import windll # noqa: E402
|
||||||
|
from ctypes import WINFUNCTYPE # noqa: E402
|
||||||
|
|
||||||
c_ssize_p = POINTER(c_ssize_t)
|
c_ssize_p = POINTER(c_ssize_t)
|
||||||
|
|
||||||
|
@ -57,16 +44,12 @@ GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
|
||||||
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
|
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
|
||||||
("CommandLineToArgvW", windll.shell32)
|
("CommandLineToArgvW", windll.shell32)
|
||||||
)
|
)
|
||||||
LocalFree = WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)(
|
LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32))
|
||||||
("LocalFree", windll.kernel32)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
STDIN_HANDLE = GetStdHandle(-10)
|
STDIN_HANDLE = GetStdHandle(-10)
|
||||||
STDOUT_HANDLE = GetStdHandle(-11)
|
STDOUT_HANDLE = GetStdHandle(-11)
|
||||||
STDERR_HANDLE = GetStdHandle(-12)
|
STDERR_HANDLE = GetStdHandle(-12)
|
||||||
|
|
||||||
|
|
||||||
PyBUF_SIMPLE = 0
|
PyBUF_SIMPLE = 0
|
||||||
PyBUF_WRITABLE = 1
|
PyBUF_WRITABLE = 1
|
||||||
|
|
||||||
|
@ -81,36 +64,37 @@ STDERR_FILENO = 2
|
||||||
EOF = b"\x1a"
|
EOF = b"\x1a"
|
||||||
MAX_BYTES_WRITTEN = 32767
|
MAX_BYTES_WRITTEN = 32767
|
||||||
|
|
||||||
|
try:
|
||||||
class Py_buffer(ctypes.Structure):
|
from ctypes import pythonapi
|
||||||
_fields_ = [
|
except ImportError:
|
||||||
("buf", c_void_p),
|
# On PyPy we cannot get buffers so our ability to operate here is
|
||||||
("obj", py_object),
|
# severely limited.
|
||||||
("len", c_ssize_t),
|
|
||||||
("itemsize", c_ssize_t),
|
|
||||||
("readonly", c_int),
|
|
||||||
("ndim", c_int),
|
|
||||||
("format", c_char_p),
|
|
||||||
("shape", c_ssize_p),
|
|
||||||
("strides", c_ssize_p),
|
|
||||||
("suboffsets", c_ssize_p),
|
|
||||||
("internal", c_void_p),
|
|
||||||
]
|
|
||||||
|
|
||||||
if PY2:
|
|
||||||
_fields_.insert(-1, ("smalltable", c_ssize_t * 2))
|
|
||||||
|
|
||||||
|
|
||||||
# On PyPy we cannot get buffers so our ability to operate here is
|
|
||||||
# serverly limited.
|
|
||||||
if pythonapi is None:
|
|
||||||
get_buffer = None
|
get_buffer = None
|
||||||
else:
|
else:
|
||||||
|
|
||||||
|
class Py_buffer(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("buf", c_void_p),
|
||||||
|
("obj", py_object),
|
||||||
|
("len", c_ssize_t),
|
||||||
|
("itemsize", c_ssize_t),
|
||||||
|
("readonly", c_int),
|
||||||
|
("ndim", c_int),
|
||||||
|
("format", c_char_p),
|
||||||
|
("shape", c_ssize_p),
|
||||||
|
("strides", c_ssize_p),
|
||||||
|
("suboffsets", c_ssize_p),
|
||||||
|
("internal", c_void_p),
|
||||||
|
]
|
||||||
|
|
||||||
|
PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
|
||||||
|
PyBuffer_Release = pythonapi.PyBuffer_Release
|
||||||
|
|
||||||
def get_buffer(obj, writable=False):
|
def get_buffer(obj, writable=False):
|
||||||
buf = Py_buffer()
|
buf = Py_buffer()
|
||||||
flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
|
flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
|
||||||
PyObject_GetBuffer(py_object(obj), byref(buf), flags)
|
PyObject_GetBuffer(py_object(obj), byref(buf), flags)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
buffer_type = c_char * buf.len
|
buffer_type = c_char * buf.len
|
||||||
return buffer_type.from_address(buf.buf)
|
return buffer_type.from_address(buf.buf)
|
||||||
|
@ -123,7 +107,7 @@ class _WindowsConsoleRawIOBase(io.RawIOBase):
|
||||||
self.handle = handle
|
self.handle = handle
|
||||||
|
|
||||||
def isatty(self):
|
def isatty(self):
|
||||||
io.RawIOBase.isatty(self)
|
super().isatty()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -155,7 +139,7 @@ class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
|
||||||
# wait for KeyboardInterrupt
|
# wait for KeyboardInterrupt
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
if not rv:
|
if not rv:
|
||||||
raise OSError("Windows error: {}".format(GetLastError()))
|
raise OSError(f"Windows error: {GetLastError()}")
|
||||||
|
|
||||||
if buffer[0] == EOF:
|
if buffer[0] == EOF:
|
||||||
return 0
|
return 0
|
||||||
|
@ -172,7 +156,7 @@ class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
|
||||||
return "ERROR_SUCCESS"
|
return "ERROR_SUCCESS"
|
||||||
elif errno == ERROR_NOT_ENOUGH_MEMORY:
|
elif errno == ERROR_NOT_ENOUGH_MEMORY:
|
||||||
return "ERROR_NOT_ENOUGH_MEMORY"
|
return "ERROR_NOT_ENOUGH_MEMORY"
|
||||||
return "Windows error {}".format(errno)
|
return f"Windows error {errno}"
|
||||||
|
|
||||||
def write(self, b):
|
def write(self, b):
|
||||||
bytes_to_be_written = len(b)
|
bytes_to_be_written = len(b)
|
||||||
|
@ -194,17 +178,17 @@ class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
|
||||||
return bytes_written
|
return bytes_written
|
||||||
|
|
||||||
|
|
||||||
class ConsoleStream(object):
|
class ConsoleStream:
|
||||||
def __init__(self, text_stream, byte_stream):
|
def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None:
|
||||||
self._text_stream = text_stream
|
self._text_stream = text_stream
|
||||||
self.buffer = byte_stream
|
self.buffer = byte_stream
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
return self.buffer.name
|
return self.buffer.name
|
||||||
|
|
||||||
def write(self, x):
|
def write(self, x: t.AnyStr) -> int:
|
||||||
if isinstance(x, text_type):
|
if isinstance(x, str):
|
||||||
return self._text_stream.write(x)
|
return self._text_stream.write(x)
|
||||||
try:
|
try:
|
||||||
self.flush()
|
self.flush()
|
||||||
|
@ -212,159 +196,84 @@ class ConsoleStream(object):
|
||||||
pass
|
pass
|
||||||
return self.buffer.write(x)
|
return self.buffer.write(x)
|
||||||
|
|
||||||
def writelines(self, lines):
|
def writelines(self, lines: t.Iterable[t.AnyStr]) -> None:
|
||||||
for line in lines:
|
for line in lines:
|
||||||
self.write(line)
|
self.write(line)
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
return getattr(self._text_stream, name)
|
return getattr(self._text_stream, name)
|
||||||
|
|
||||||
def isatty(self):
|
def isatty(self) -> bool:
|
||||||
return self.buffer.isatty()
|
return self.buffer.isatty()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<ConsoleStream name={!r} encoding={!r}>".format(
|
return f"<ConsoleStream name={self.name!r} encoding={self.encoding!r}>"
|
||||||
self.name, self.encoding
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WindowsChunkedWriter(object):
|
def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO:
|
||||||
"""
|
|
||||||
Wraps a stream (such as stdout), acting as a transparent proxy for all
|
|
||||||
attribute access apart from method 'write()' which we wrap to write in
|
|
||||||
limited chunks due to a Windows limitation on binary console streams.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, wrapped):
|
|
||||||
# double-underscore everything to prevent clashes with names of
|
|
||||||
# attributes on the wrapped stream object.
|
|
||||||
self.__wrapped = wrapped
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
return getattr(self.__wrapped, name)
|
|
||||||
|
|
||||||
def write(self, text):
|
|
||||||
total_to_write = len(text)
|
|
||||||
written = 0
|
|
||||||
|
|
||||||
while written < total_to_write:
|
|
||||||
to_write = min(total_to_write - written, MAX_BYTES_WRITTEN)
|
|
||||||
self.__wrapped.write(text[written : written + to_write])
|
|
||||||
written += to_write
|
|
||||||
|
|
||||||
|
|
||||||
_wrapped_std_streams = set()
|
|
||||||
|
|
||||||
|
|
||||||
def _wrap_std_stream(name):
|
|
||||||
# Python 2 & Windows 7 and below
|
|
||||||
if (
|
|
||||||
PY2
|
|
||||||
and sys.getwindowsversion()[:2] <= (6, 1)
|
|
||||||
and name not in _wrapped_std_streams
|
|
||||||
):
|
|
||||||
setattr(sys, name, WindowsChunkedWriter(getattr(sys, name)))
|
|
||||||
_wrapped_std_streams.add(name)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_text_stdin(buffer_stream):
|
|
||||||
text_stream = _NonClosingTextIOWrapper(
|
text_stream = _NonClosingTextIOWrapper(
|
||||||
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
|
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
|
||||||
"utf-16-le",
|
"utf-16-le",
|
||||||
"strict",
|
"strict",
|
||||||
line_buffering=True,
|
line_buffering=True,
|
||||||
)
|
)
|
||||||
return ConsoleStream(text_stream, buffer_stream)
|
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
||||||
|
|
||||||
|
|
||||||
def _get_text_stdout(buffer_stream):
|
def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO:
|
||||||
text_stream = _NonClosingTextIOWrapper(
|
text_stream = _NonClosingTextIOWrapper(
|
||||||
io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
|
io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
|
||||||
"utf-16-le",
|
"utf-16-le",
|
||||||
"strict",
|
"strict",
|
||||||
line_buffering=True,
|
line_buffering=True,
|
||||||
)
|
)
|
||||||
return ConsoleStream(text_stream, buffer_stream)
|
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
||||||
|
|
||||||
|
|
||||||
def _get_text_stderr(buffer_stream):
|
def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO:
|
||||||
text_stream = _NonClosingTextIOWrapper(
|
text_stream = _NonClosingTextIOWrapper(
|
||||||
io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
|
io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
|
||||||
"utf-16-le",
|
"utf-16-le",
|
||||||
"strict",
|
"strict",
|
||||||
line_buffering=True,
|
line_buffering=True,
|
||||||
)
|
)
|
||||||
return ConsoleStream(text_stream, buffer_stream)
|
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
||||||
|
|
||||||
|
|
||||||
if PY2:
|
_stream_factories: t.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = {
|
||||||
|
|
||||||
def _hash_py_argv():
|
|
||||||
return zlib.crc32("\x00".join(sys.argv[1:]))
|
|
||||||
|
|
||||||
_initial_argv_hash = _hash_py_argv()
|
|
||||||
|
|
||||||
def _get_windows_argv():
|
|
||||||
argc = c_int(0)
|
|
||||||
argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc))
|
|
||||||
if not argv_unicode:
|
|
||||||
raise WinError()
|
|
||||||
try:
|
|
||||||
argv = [argv_unicode[i] for i in range(0, argc.value)]
|
|
||||||
finally:
|
|
||||||
LocalFree(argv_unicode)
|
|
||||||
del argv_unicode
|
|
||||||
|
|
||||||
if not hasattr(sys, "frozen"):
|
|
||||||
argv = argv[1:]
|
|
||||||
while len(argv) > 0:
|
|
||||||
arg = argv[0]
|
|
||||||
if not arg.startswith("-") or arg == "-":
|
|
||||||
break
|
|
||||||
argv = argv[1:]
|
|
||||||
if arg.startswith(("-c", "-m")):
|
|
||||||
break
|
|
||||||
|
|
||||||
return argv[1:]
|
|
||||||
|
|
||||||
|
|
||||||
_stream_factories = {
|
|
||||||
0: _get_text_stdin,
|
0: _get_text_stdin,
|
||||||
1: _get_text_stdout,
|
1: _get_text_stdout,
|
||||||
2: _get_text_stderr,
|
2: _get_text_stderr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _is_console(f):
|
def _is_console(f: t.TextIO) -> bool:
|
||||||
if not hasattr(f, "fileno"):
|
if not hasattr(f, "fileno"):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fileno = f.fileno()
|
fileno = f.fileno()
|
||||||
except OSError:
|
except (OSError, io.UnsupportedOperation):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
handle = msvcrt.get_osfhandle(fileno)
|
handle = msvcrt.get_osfhandle(fileno)
|
||||||
return bool(GetConsoleMode(handle, byref(DWORD())))
|
return bool(GetConsoleMode(handle, byref(DWORD())))
|
||||||
|
|
||||||
|
|
||||||
def _get_windows_console_stream(f, encoding, errors):
|
def _get_windows_console_stream(
|
||||||
|
f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str]
|
||||||
|
) -> t.Optional[t.TextIO]:
|
||||||
if (
|
if (
|
||||||
get_buffer is not None
|
get_buffer is not None
|
||||||
and encoding in ("utf-16-le", None)
|
and encoding in {"utf-16-le", None}
|
||||||
and errors in ("strict", None)
|
and errors in {"strict", None}
|
||||||
and _is_console(f)
|
and _is_console(f)
|
||||||
):
|
):
|
||||||
func = _stream_factories.get(f.fileno())
|
func = _stream_factories.get(f.fileno())
|
||||||
if func is not None:
|
if func is not None:
|
||||||
if not PY2:
|
b = getattr(f, "buffer", None)
|
||||||
f = getattr(f, "buffer", None)
|
|
||||||
if f is None:
|
if b is None:
|
||||||
return None
|
return None
|
||||||
else:
|
|
||||||
# If we are on Python 2 we need to set the stream that we
|
return func(b)
|
||||||
# deal with to binary mode as otherwise the exercise if a
|
|
||||||
# bit moot. The same problems apply as for
|
|
||||||
# get_binary_stdin and friends from _compat.
|
|
||||||
msvcrt.setmode(f.fileno(), os.O_BINARY)
|
|
||||||
return func(f)
|
|
||||||
|
|
2171
src/click/core.py
2171
src/click/core.py
|
@ -1,17 +1,20 @@
|
||||||
|
import enum
|
||||||
import errno
|
import errno
|
||||||
import inspect
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import typing
|
||||||
|
import typing as t
|
||||||
|
from collections import abc
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from contextlib import ExitStack
|
||||||
|
from functools import partial
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
|
from gettext import gettext as _
|
||||||
|
from gettext import ngettext
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
|
|
||||||
from ._compat import isidentifier
|
from . import types
|
||||||
from ._compat import iteritems
|
from ._unicodefun import _verify_python_env
|
||||||
from ._compat import PY2
|
|
||||||
from ._compat import string_types
|
|
||||||
from ._unicodefun import _check_for_unicode_literals
|
|
||||||
from ._unicodefun import _verify_python3_env
|
|
||||||
from .exceptions import Abort
|
from .exceptions import Abort
|
||||||
from .exceptions import BadParameter
|
from .exceptions import BadParameter
|
||||||
from .exceptions import ClickException
|
from .exceptions import ClickException
|
||||||
|
@ -22,58 +25,49 @@ from .formatting import HelpFormatter
|
||||||
from .formatting import join_options
|
from .formatting import join_options
|
||||||
from .globals import pop_context
|
from .globals import pop_context
|
||||||
from .globals import push_context
|
from .globals import push_context
|
||||||
|
from .parser import _flag_needs_value
|
||||||
from .parser import OptionParser
|
from .parser import OptionParser
|
||||||
from .parser import split_opt
|
from .parser import split_opt
|
||||||
from .termui import confirm
|
from .termui import confirm
|
||||||
from .termui import prompt
|
from .termui import prompt
|
||||||
from .termui import style
|
from .termui import style
|
||||||
from .types import BOOL
|
from .utils import _detect_program_name
|
||||||
from .types import convert_type
|
from .utils import _expand_args
|
||||||
from .types import IntRange
|
|
||||||
from .utils import echo
|
from .utils import echo
|
||||||
from .utils import get_os_args
|
|
||||||
from .utils import make_default_short_help
|
from .utils import make_default_short_help
|
||||||
from .utils import make_str
|
from .utils import make_str
|
||||||
from .utils import PacifyFlushWrapper
|
from .utils import PacifyFlushWrapper
|
||||||
|
|
||||||
_missing = object()
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .shell_completion import CompletionItem
|
||||||
|
|
||||||
SUBCOMMAND_METAVAR = "COMMAND [ARGS]..."
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
SUBCOMMANDS_METAVAR = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
|
V = t.TypeVar("V")
|
||||||
|
|
||||||
DEPRECATED_HELP_NOTICE = " (DEPRECATED)"
|
|
||||||
DEPRECATED_INVOKE_NOTICE = "DeprecationWarning: The command %(name)s is deprecated."
|
|
||||||
|
|
||||||
|
|
||||||
def _maybe_show_deprecated_notice(cmd):
|
def _complete_visible_commands(
|
||||||
if cmd.deprecated:
|
ctx: "Context", incomplete: str
|
||||||
echo(style(DEPRECATED_INVOKE_NOTICE % {"name": cmd.name}, fg="red"), err=True)
|
) -> t.Iterator[t.Tuple[str, "Command"]]:
|
||||||
|
"""List all the subcommands of a group that start with the
|
||||||
|
incomplete value and aren't hidden.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for the group.
|
||||||
def fast_exit(code):
|
:param incomplete: Value being completed. May be empty.
|
||||||
"""Exit without garbage collection, this speeds up exit by about 10ms for
|
|
||||||
things like bash completion.
|
|
||||||
"""
|
"""
|
||||||
sys.stdout.flush()
|
multi = t.cast(MultiCommand, ctx.command)
|
||||||
sys.stderr.flush()
|
|
||||||
os._exit(code)
|
for name in multi.list_commands(ctx):
|
||||||
|
if name.startswith(incomplete):
|
||||||
|
command = multi.get_command(ctx, name)
|
||||||
|
|
||||||
|
if command is not None and not command.hidden:
|
||||||
|
yield name, command
|
||||||
|
|
||||||
|
|
||||||
def _bashcomplete(cmd, prog_name, complete_var=None):
|
def _check_multicommand(
|
||||||
"""Internal handler for the bash completion support."""
|
base_command: "MultiCommand", cmd_name: str, cmd: "Command", register: bool = False
|
||||||
if complete_var is None:
|
) -> None:
|
||||||
complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper())
|
|
||||||
complete_instr = os.environ.get(complete_var)
|
|
||||||
if not complete_instr:
|
|
||||||
return
|
|
||||||
|
|
||||||
from ._bashcomplete import bashcomplete
|
|
||||||
|
|
||||||
if bashcomplete(cmd, prog_name, complete_var, complete_instr):
|
|
||||||
fast_exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_multicommand(base_command, cmd_name, cmd, register=False):
|
|
||||||
if not base_command.chain or not isinstance(cmd, MultiCommand):
|
if not base_command.chain or not isinstance(cmd, MultiCommand):
|
||||||
return
|
return
|
||||||
if register:
|
if register:
|
||||||
|
@ -87,44 +81,22 @@ def _check_multicommand(base_command, cmd_name, cmd, register=False):
|
||||||
" that is in chain mode. This is not supported."
|
" that is in chain mode. This is not supported."
|
||||||
)
|
)
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"{}. Command '{}' is set to chain and '{}' was added as"
|
f"{hint}. Command {base_command.name!r} is set to chain and"
|
||||||
" subcommand but it in itself is a multi command. ('{}' is a {}"
|
f" {cmd_name!r} was added as a subcommand but it in itself is a"
|
||||||
" within a chained {} named '{}').".format(
|
f" multi command. ({cmd_name!r} is a {type(cmd).__name__}"
|
||||||
hint,
|
f" within a chained {type(base_command).__name__} named"
|
||||||
base_command.name,
|
f" {base_command.name!r})."
|
||||||
cmd_name,
|
|
||||||
cmd_name,
|
|
||||||
cmd.__class__.__name__,
|
|
||||||
base_command.__class__.__name__,
|
|
||||||
base_command.name,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def batch(iterable, batch_size):
|
def batch(iterable: t.Iterable[V], batch_size: int) -> t.List[t.Tuple[V, ...]]:
|
||||||
return list(zip(*repeat(iter(iterable), batch_size)))
|
return list(zip(*repeat(iter(iterable), batch_size)))
|
||||||
|
|
||||||
|
|
||||||
def invoke_param_callback(callback, ctx, param, value):
|
|
||||||
code = getattr(callback, "__code__", None)
|
|
||||||
args = getattr(code, "co_argcount", 3)
|
|
||||||
|
|
||||||
if args < 3:
|
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
warn(
|
|
||||||
"Parameter callbacks take 3 args, (ctx, param, value). The"
|
|
||||||
" 2-arg style is deprecated and will be removed in 8.0.".format(callback),
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=3,
|
|
||||||
)
|
|
||||||
return callback(ctx, value)
|
|
||||||
|
|
||||||
return callback(ctx, param, value)
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def augment_usage_errors(ctx, param=None):
|
def augment_usage_errors(
|
||||||
|
ctx: "Context", param: t.Optional["Parameter"] = None
|
||||||
|
) -> t.Iterator[None]:
|
||||||
"""Context manager that attaches extra information to exceptions."""
|
"""Context manager that attaches extra information to exceptions."""
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
|
@ -140,23 +112,53 @@ def augment_usage_errors(ctx, param=None):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def iter_params_for_processing(invocation_order, declaration_order):
|
def iter_params_for_processing(
|
||||||
|
invocation_order: t.Sequence["Parameter"],
|
||||||
|
declaration_order: t.Sequence["Parameter"],
|
||||||
|
) -> t.List["Parameter"]:
|
||||||
"""Given a sequence of parameters in the order as should be considered
|
"""Given a sequence of parameters in the order as should be considered
|
||||||
for processing and an iterable of parameters that exist, this returns
|
for processing and an iterable of parameters that exist, this returns
|
||||||
a list in the correct order as they should be processed.
|
a list in the correct order as they should be processed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def sort_key(item):
|
def sort_key(item: "Parameter") -> t.Tuple[bool, float]:
|
||||||
try:
|
try:
|
||||||
idx = invocation_order.index(item)
|
idx: float = invocation_order.index(item)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
idx = float("inf")
|
idx = float("inf")
|
||||||
return (not item.is_eager, idx)
|
|
||||||
|
return not item.is_eager, idx
|
||||||
|
|
||||||
return sorted(declaration_order, key=sort_key)
|
return sorted(declaration_order, key=sort_key)
|
||||||
|
|
||||||
|
|
||||||
class Context(object):
|
class ParameterSource(enum.Enum):
|
||||||
|
"""This is an :class:`~enum.Enum` that indicates the source of a
|
||||||
|
parameter's value.
|
||||||
|
|
||||||
|
Use :meth:`click.Context.get_parameter_source` to get the
|
||||||
|
source for a parameter by name.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Use :class:`~enum.Enum` and drop the ``validate`` method.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the ``PROMPT`` value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMMANDLINE = enum.auto()
|
||||||
|
"""The value was provided by the command line args."""
|
||||||
|
ENVIRONMENT = enum.auto()
|
||||||
|
"""The value was provided with an environment variable."""
|
||||||
|
DEFAULT = enum.auto()
|
||||||
|
"""Used the default specified by the parameter."""
|
||||||
|
DEFAULT_MAP = enum.auto()
|
||||||
|
"""Used a default provided by :attr:`Context.default_map`."""
|
||||||
|
PROMPT = enum.auto()
|
||||||
|
"""Used a prompt to confirm a default or provide a value."""
|
||||||
|
|
||||||
|
|
||||||
|
class Context:
|
||||||
"""The context is a special internal object that holds state relevant
|
"""The context is a special internal object that holds state relevant
|
||||||
for the script execution at every single level. It's normally invisible
|
for the script execution at every single level. It's normally invisible
|
||||||
to commands unless they opt-in to getting access to it.
|
to commands unless they opt-in to getting access to it.
|
||||||
|
@ -168,21 +170,6 @@ class Context(object):
|
||||||
A context can be used as context manager in which case it will call
|
A context can be used as context manager in which case it will call
|
||||||
:meth:`close` on teardown.
|
:meth:`close` on teardown.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
Added the `resilient_parsing`, `help_option_names`,
|
|
||||||
`token_normalize_func` parameters.
|
|
||||||
|
|
||||||
.. versionadded:: 3.0
|
|
||||||
Added the `allow_extra_args` and `allow_interspersed_args`
|
|
||||||
parameters.
|
|
||||||
|
|
||||||
.. versionadded:: 4.0
|
|
||||||
Added the `color`, `ignore_unknown_options`, and
|
|
||||||
`max_content_width` parameters.
|
|
||||||
|
|
||||||
.. versionadded:: 7.1
|
|
||||||
Added the `show_default` parameter.
|
|
||||||
|
|
||||||
:param command: the command class for this context.
|
:param command: the command class for this context.
|
||||||
:param parent: the parent context.
|
:param parent: the parent context.
|
||||||
:param info_name: the info name for this invocation. Generally this
|
:param info_name: the info name for this invocation. Generally this
|
||||||
|
@ -237,60 +224,88 @@ class Context(object):
|
||||||
codes are used in texts that Click prints which is by
|
codes are used in texts that Click prints which is by
|
||||||
default not the case. This for instance would affect
|
default not the case. This for instance would affect
|
||||||
help output.
|
help output.
|
||||||
:param show_default: if True, shows defaults for all options.
|
:param show_default: Show defaults for all options. If not set,
|
||||||
Even if an option is later created with show_default=False,
|
defaults to the value from a parent context. Overrides an
|
||||||
this command-level setting overrides it.
|
option's ``show_default`` argument.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
The ``show_default`` parameter defaults to the value from the
|
||||||
|
parent context.
|
||||||
|
|
||||||
|
.. versionchanged:: 7.1
|
||||||
|
Added the ``show_default`` parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
Added the ``color``, ``ignore_unknown_options``, and
|
||||||
|
``max_content_width`` parameters.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Added the ``allow_extra_args`` and ``allow_interspersed_args``
|
||||||
|
parameters.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
Added the ``resilient_parsing``, ``help_option_names``, and
|
||||||
|
``token_normalize_func`` parameters.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
#: The formatter class to create with :meth:`make_formatter`.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 8.0
|
||||||
|
formatter_class: t.Type["HelpFormatter"] = HelpFormatter
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
command,
|
command: "Command",
|
||||||
parent=None,
|
parent: t.Optional["Context"] = None,
|
||||||
info_name=None,
|
info_name: t.Optional[str] = None,
|
||||||
obj=None,
|
obj: t.Optional[t.Any] = None,
|
||||||
auto_envvar_prefix=None,
|
auto_envvar_prefix: t.Optional[str] = None,
|
||||||
default_map=None,
|
default_map: t.Optional[t.Dict[str, t.Any]] = None,
|
||||||
terminal_width=None,
|
terminal_width: t.Optional[int] = None,
|
||||||
max_content_width=None,
|
max_content_width: t.Optional[int] = None,
|
||||||
resilient_parsing=False,
|
resilient_parsing: bool = False,
|
||||||
allow_extra_args=None,
|
allow_extra_args: t.Optional[bool] = None,
|
||||||
allow_interspersed_args=None,
|
allow_interspersed_args: t.Optional[bool] = None,
|
||||||
ignore_unknown_options=None,
|
ignore_unknown_options: t.Optional[bool] = None,
|
||||||
help_option_names=None,
|
help_option_names: t.Optional[t.List[str]] = None,
|
||||||
token_normalize_func=None,
|
token_normalize_func: t.Optional[t.Callable[[str], str]] = None,
|
||||||
color=None,
|
color: t.Optional[bool] = None,
|
||||||
show_default=None,
|
show_default: t.Optional[bool] = None,
|
||||||
):
|
) -> None:
|
||||||
#: the parent context or `None` if none exists.
|
#: the parent context or `None` if none exists.
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
#: the :class:`Command` for this context.
|
#: the :class:`Command` for this context.
|
||||||
self.command = command
|
self.command = command
|
||||||
#: the descriptive information name
|
#: the descriptive information name
|
||||||
self.info_name = info_name
|
self.info_name = info_name
|
||||||
#: the parsed parameters except if the value is hidden in which
|
#: Map of parameter names to their parsed values. Parameters
|
||||||
#: case it's not remembered.
|
#: with ``expose_value=False`` are not stored.
|
||||||
self.params = {}
|
self.params: t.Dict[str, t.Any] = {}
|
||||||
#: the leftover arguments.
|
#: the leftover arguments.
|
||||||
self.args = []
|
self.args: t.List[str] = []
|
||||||
#: protected arguments. These are arguments that are prepended
|
#: protected arguments. These are arguments that are prepended
|
||||||
#: to `args` when certain parsing scenarios are encountered but
|
#: to `args` when certain parsing scenarios are encountered but
|
||||||
#: must be never propagated to another arguments. This is used
|
#: must be never propagated to another arguments. This is used
|
||||||
#: to implement nested parsing.
|
#: to implement nested parsing.
|
||||||
self.protected_args = []
|
self.protected_args: t.List[str] = []
|
||||||
|
|
||||||
if obj is None and parent is not None:
|
if obj is None and parent is not None:
|
||||||
obj = parent.obj
|
obj = parent.obj
|
||||||
|
|
||||||
#: the user object stored.
|
#: the user object stored.
|
||||||
self.obj = obj
|
self.obj: t.Any = obj
|
||||||
self._meta = getattr(parent, "meta", {})
|
self._meta: t.Dict[str, t.Any] = getattr(parent, "meta", {})
|
||||||
|
|
||||||
#: A dictionary (-like object) with defaults for parameters.
|
#: A dictionary (-like object) with defaults for parameters.
|
||||||
if (
|
if (
|
||||||
default_map is None
|
default_map is None
|
||||||
|
and info_name is not None
|
||||||
and parent is not None
|
and parent is not None
|
||||||
and parent.default_map is not None
|
and parent.default_map is not None
|
||||||
):
|
):
|
||||||
default_map = parent.default_map.get(info_name)
|
default_map = parent.default_map.get(info_name)
|
||||||
self.default_map = default_map
|
|
||||||
|
self.default_map: t.Optional[t.Dict[str, t.Any]] = default_map
|
||||||
|
|
||||||
#: This flag indicates if a subcommand is going to be executed. A
|
#: This flag indicates if a subcommand is going to be executed. A
|
||||||
#: group callback can use this information to figure out if it's
|
#: group callback can use this information to figure out if it's
|
||||||
|
@ -301,22 +316,25 @@ class Context(object):
|
||||||
#: If chaining is enabled this will be set to ``'*'`` in case
|
#: If chaining is enabled this will be set to ``'*'`` in case
|
||||||
#: any commands are executed. It is however not possible to
|
#: any commands are executed. It is however not possible to
|
||||||
#: figure out which ones. If you require this knowledge you
|
#: figure out which ones. If you require this knowledge you
|
||||||
#: should use a :func:`resultcallback`.
|
#: should use a :func:`result_callback`.
|
||||||
self.invoked_subcommand = None
|
self.invoked_subcommand: t.Optional[str] = None
|
||||||
|
|
||||||
if terminal_width is None and parent is not None:
|
if terminal_width is None and parent is not None:
|
||||||
terminal_width = parent.terminal_width
|
terminal_width = parent.terminal_width
|
||||||
|
|
||||||
#: The width of the terminal (None is autodetection).
|
#: The width of the terminal (None is autodetection).
|
||||||
self.terminal_width = terminal_width
|
self.terminal_width: t.Optional[int] = terminal_width
|
||||||
|
|
||||||
if max_content_width is None and parent is not None:
|
if max_content_width is None and parent is not None:
|
||||||
max_content_width = parent.max_content_width
|
max_content_width = parent.max_content_width
|
||||||
|
|
||||||
#: The maximum width of formatted content (None implies a sensible
|
#: The maximum width of formatted content (None implies a sensible
|
||||||
#: default which is 80 for most things).
|
#: default which is 80 for most things).
|
||||||
self.max_content_width = max_content_width
|
self.max_content_width: t.Optional[int] = max_content_width
|
||||||
|
|
||||||
if allow_extra_args is None:
|
if allow_extra_args is None:
|
||||||
allow_extra_args = command.allow_extra_args
|
allow_extra_args = command.allow_extra_args
|
||||||
|
|
||||||
#: Indicates if the context allows extra args or if it should
|
#: Indicates if the context allows extra args or if it should
|
||||||
#: fail on parsing.
|
#: fail on parsing.
|
||||||
#:
|
#:
|
||||||
|
@ -325,14 +343,16 @@ class Context(object):
|
||||||
|
|
||||||
if allow_interspersed_args is None:
|
if allow_interspersed_args is None:
|
||||||
allow_interspersed_args = command.allow_interspersed_args
|
allow_interspersed_args = command.allow_interspersed_args
|
||||||
|
|
||||||
#: Indicates if the context allows mixing of arguments and
|
#: Indicates if the context allows mixing of arguments and
|
||||||
#: options or not.
|
#: options or not.
|
||||||
#:
|
#:
|
||||||
#: .. versionadded:: 3.0
|
#: .. versionadded:: 3.0
|
||||||
self.allow_interspersed_args = allow_interspersed_args
|
self.allow_interspersed_args: bool = allow_interspersed_args
|
||||||
|
|
||||||
if ignore_unknown_options is None:
|
if ignore_unknown_options is None:
|
||||||
ignore_unknown_options = command.ignore_unknown_options
|
ignore_unknown_options = command.ignore_unknown_options
|
||||||
|
|
||||||
#: Instructs click to ignore options that a command does not
|
#: Instructs click to ignore options that a command does not
|
||||||
#: understand and will store it on the context for later
|
#: understand and will store it on the context for later
|
||||||
#: processing. This is primarily useful for situations where you
|
#: processing. This is primarily useful for situations where you
|
||||||
|
@ -341,7 +361,7 @@ class Context(object):
|
||||||
#: forward all arguments.
|
#: forward all arguments.
|
||||||
#:
|
#:
|
||||||
#: .. versionadded:: 4.0
|
#: .. versionadded:: 4.0
|
||||||
self.ignore_unknown_options = ignore_unknown_options
|
self.ignore_unknown_options: bool = ignore_unknown_options
|
||||||
|
|
||||||
if help_option_names is None:
|
if help_option_names is None:
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
|
@ -350,19 +370,21 @@ class Context(object):
|
||||||
help_option_names = ["--help"]
|
help_option_names = ["--help"]
|
||||||
|
|
||||||
#: The names for the help options.
|
#: The names for the help options.
|
||||||
self.help_option_names = help_option_names
|
self.help_option_names: t.List[str] = help_option_names
|
||||||
|
|
||||||
if token_normalize_func is None and parent is not None:
|
if token_normalize_func is None and parent is not None:
|
||||||
token_normalize_func = parent.token_normalize_func
|
token_normalize_func = parent.token_normalize_func
|
||||||
|
|
||||||
#: An optional normalization function for tokens. This is
|
#: An optional normalization function for tokens. This is
|
||||||
#: options, choices, commands etc.
|
#: options, choices, commands etc.
|
||||||
self.token_normalize_func = token_normalize_func
|
self.token_normalize_func: t.Optional[
|
||||||
|
t.Callable[[str], str]
|
||||||
|
] = token_normalize_func
|
||||||
|
|
||||||
#: Indicates if resilient parsing is enabled. In that case Click
|
#: Indicates if resilient parsing is enabled. In that case Click
|
||||||
#: will do its best to not cause any failures and default values
|
#: will do its best to not cause any failures and default values
|
||||||
#: will be ignored. Useful for completion.
|
#: will be ignored. Useful for completion.
|
||||||
self.resilient_parsing = resilient_parsing
|
self.resilient_parsing: bool = resilient_parsing
|
||||||
|
|
||||||
# If there is no envvar prefix yet, but the parent has one and
|
# If there is no envvar prefix yet, but the parent has one and
|
||||||
# the command on this level has a name, we can expand the envvar
|
# the command on this level has a name, we can expand the envvar
|
||||||
|
@ -373,39 +395,68 @@ class Context(object):
|
||||||
and parent.auto_envvar_prefix is not None
|
and parent.auto_envvar_prefix is not None
|
||||||
and self.info_name is not None
|
and self.info_name is not None
|
||||||
):
|
):
|
||||||
auto_envvar_prefix = "{}_{}".format(
|
auto_envvar_prefix = (
|
||||||
parent.auto_envvar_prefix, self.info_name.upper()
|
f"{parent.auto_envvar_prefix}_{self.info_name.upper()}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
auto_envvar_prefix = auto_envvar_prefix.upper()
|
auto_envvar_prefix = auto_envvar_prefix.upper()
|
||||||
|
|
||||||
if auto_envvar_prefix is not None:
|
if auto_envvar_prefix is not None:
|
||||||
auto_envvar_prefix = auto_envvar_prefix.replace("-", "_")
|
auto_envvar_prefix = auto_envvar_prefix.replace("-", "_")
|
||||||
self.auto_envvar_prefix = auto_envvar_prefix
|
|
||||||
|
self.auto_envvar_prefix: t.Optional[str] = auto_envvar_prefix
|
||||||
|
|
||||||
if color is None and parent is not None:
|
if color is None and parent is not None:
|
||||||
color = parent.color
|
color = parent.color
|
||||||
|
|
||||||
#: Controls if styling output is wanted or not.
|
#: Controls if styling output is wanted or not.
|
||||||
self.color = color
|
self.color: t.Optional[bool] = color
|
||||||
|
|
||||||
self.show_default = show_default
|
if show_default is None and parent is not None:
|
||||||
|
show_default = parent.show_default
|
||||||
|
|
||||||
self._close_callbacks = []
|
#: Show option default values when formatting help text.
|
||||||
|
self.show_default: t.Optional[bool] = show_default
|
||||||
|
|
||||||
|
self._close_callbacks: t.List[t.Callable[[], t.Any]] = []
|
||||||
self._depth = 0
|
self._depth = 0
|
||||||
|
self._parameter_source: t.Dict[str, ParameterSource] = {}
|
||||||
|
self._exit_stack = ExitStack()
|
||||||
|
|
||||||
def __enter__(self):
|
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||||
|
"""Gather information that could be useful for a tool generating
|
||||||
|
user-facing documentation. This traverses the entire CLI
|
||||||
|
structure.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
with Context(cli) as ctx:
|
||||||
|
info = ctx.to_info_dict()
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"command": self.command.to_info_dict(self),
|
||||||
|
"info_name": self.info_name,
|
||||||
|
"allow_extra_args": self.allow_extra_args,
|
||||||
|
"allow_interspersed_args": self.allow_interspersed_args,
|
||||||
|
"ignore_unknown_options": self.ignore_unknown_options,
|
||||||
|
"auto_envvar_prefix": self.auto_envvar_prefix,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __enter__(self) -> "Context":
|
||||||
self._depth += 1
|
self._depth += 1
|
||||||
push_context(self)
|
push_context(self)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb):
|
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||||
self._depth -= 1
|
self._depth -= 1
|
||||||
if self._depth == 0:
|
if self._depth == 0:
|
||||||
self.close()
|
self.close()
|
||||||
pop_context()
|
pop_context()
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def scope(self, cleanup=True):
|
def scope(self, cleanup: bool = True) -> t.Iterator["Context"]:
|
||||||
"""This helper method can be used with the context object to promote
|
"""This helper method can be used with the context object to promote
|
||||||
it to the current thread local (see :func:`get_current_context`).
|
it to the current thread local (see :func:`get_current_context`).
|
||||||
The default behavior of this is to invoke the cleanup functions which
|
The default behavior of this is to invoke the cleanup functions which
|
||||||
|
@ -443,7 +494,7 @@ class Context(object):
|
||||||
self._depth -= 1
|
self._depth -= 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def meta(self):
|
def meta(self) -> t.Dict[str, t.Any]:
|
||||||
"""This is a dictionary which is shared with all the contexts
|
"""This is a dictionary which is shared with all the contexts
|
||||||
that are nested. It exists so that click utilities can store some
|
that are nested. It exists so that click utilities can store some
|
||||||
state here if they need to. It is however the responsibility of
|
state here if they need to. It is however the responsibility of
|
||||||
|
@ -470,32 +521,72 @@ class Context(object):
|
||||||
"""
|
"""
|
||||||
return self._meta
|
return self._meta
|
||||||
|
|
||||||
def make_formatter(self):
|
def make_formatter(self) -> HelpFormatter:
|
||||||
"""Creates the formatter for the help and usage output."""
|
"""Creates the :class:`~click.HelpFormatter` for the help and
|
||||||
return HelpFormatter(
|
usage output.
|
||||||
|
|
||||||
|
To quickly customize the formatter class used without overriding
|
||||||
|
this method, set the :attr:`formatter_class` attribute.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the :attr:`formatter_class` attribute.
|
||||||
|
"""
|
||||||
|
return self.formatter_class(
|
||||||
width=self.terminal_width, max_width=self.max_content_width
|
width=self.terminal_width, max_width=self.max_content_width
|
||||||
)
|
)
|
||||||
|
|
||||||
def call_on_close(self, f):
|
def with_resource(self, context_manager: t.ContextManager[V]) -> V:
|
||||||
"""This decorator remembers a function as callback that should be
|
"""Register a resource as if it were used in a ``with``
|
||||||
executed when the context tears down. This is most useful to bind
|
statement. The resource will be cleaned up when the context is
|
||||||
resource handling to the script execution. For instance, file objects
|
popped.
|
||||||
opened by the :class:`File` type will register their close callbacks
|
|
||||||
here.
|
|
||||||
|
|
||||||
:param f: the function to execute on teardown.
|
Uses :meth:`contextlib.ExitStack.enter_context`. It calls the
|
||||||
|
resource's ``__enter__()`` method and returns the result. When
|
||||||
|
the context is popped, it closes the stack, which calls the
|
||||||
|
resource's ``__exit__()`` method.
|
||||||
|
|
||||||
|
To register a cleanup function for something that isn't a
|
||||||
|
context manager, use :meth:`call_on_close`. Or use something
|
||||||
|
from :mod:`contextlib` to turn it into a context manager first.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option("--name")
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx):
|
||||||
|
ctx.obj = ctx.with_resource(connect_db(name))
|
||||||
|
|
||||||
|
:param context_manager: The context manager to enter.
|
||||||
|
:return: Whatever ``context_manager.__enter__()`` returns.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
"""
|
"""
|
||||||
self._close_callbacks.append(f)
|
return self._exit_stack.enter_context(context_manager)
|
||||||
return f
|
|
||||||
|
|
||||||
def close(self):
|
def call_on_close(self, f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
||||||
"""Invokes all close callbacks."""
|
"""Register a function to be called when the context tears down.
|
||||||
for cb in self._close_callbacks:
|
|
||||||
cb()
|
This can be used to close resources opened during the script
|
||||||
self._close_callbacks = []
|
execution. Resources that support Python's context manager
|
||||||
|
protocol which would be used in a ``with`` statement should be
|
||||||
|
registered with :meth:`with_resource` instead.
|
||||||
|
|
||||||
|
:param f: The function to execute on teardown.
|
||||||
|
"""
|
||||||
|
return self._exit_stack.callback(f)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Invoke all close callbacks registered with
|
||||||
|
:meth:`call_on_close`, and exit all context managers entered
|
||||||
|
with :meth:`with_resource`.
|
||||||
|
"""
|
||||||
|
self._exit_stack.close()
|
||||||
|
# In case the context is reused, create a new exit stack.
|
||||||
|
self._exit_stack = ExitStack()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def command_path(self):
|
def command_path(self) -> str:
|
||||||
"""The computed command path. This is used for the ``usage``
|
"""The computed command path. This is used for the ``usage``
|
||||||
information on the help page. It's automatically created by
|
information on the help page. It's automatically created by
|
||||||
combining the info names of the chain of contexts to the root.
|
combining the info names of the chain of contexts to the root.
|
||||||
|
@ -504,25 +595,35 @@ class Context(object):
|
||||||
if self.info_name is not None:
|
if self.info_name is not None:
|
||||||
rv = self.info_name
|
rv = self.info_name
|
||||||
if self.parent is not None:
|
if self.parent is not None:
|
||||||
rv = "{} {}".format(self.parent.command_path, rv)
|
parent_command_path = [self.parent.command_path]
|
||||||
|
|
||||||
|
if isinstance(self.parent.command, Command):
|
||||||
|
for param in self.parent.command.get_params(self):
|
||||||
|
parent_command_path.extend(param.get_usage_pieces(self))
|
||||||
|
|
||||||
|
rv = f"{' '.join(parent_command_path)} {rv}"
|
||||||
return rv.lstrip()
|
return rv.lstrip()
|
||||||
|
|
||||||
def find_root(self):
|
def find_root(self) -> "Context":
|
||||||
"""Finds the outermost context."""
|
"""Finds the outermost context."""
|
||||||
node = self
|
node = self
|
||||||
while node.parent is not None:
|
while node.parent is not None:
|
||||||
node = node.parent
|
node = node.parent
|
||||||
return node
|
return node
|
||||||
|
|
||||||
def find_object(self, object_type):
|
def find_object(self, object_type: t.Type[V]) -> t.Optional[V]:
|
||||||
"""Finds the closest object of a given type."""
|
"""Finds the closest object of a given type."""
|
||||||
node = self
|
node: t.Optional["Context"] = self
|
||||||
|
|
||||||
while node is not None:
|
while node is not None:
|
||||||
if isinstance(node.obj, object_type):
|
if isinstance(node.obj, object_type):
|
||||||
return node.obj
|
return node.obj
|
||||||
|
|
||||||
node = node.parent
|
node = node.parent
|
||||||
|
|
||||||
def ensure_object(self, object_type):
|
return None
|
||||||
|
|
||||||
|
def ensure_object(self, object_type: t.Type[V]) -> V:
|
||||||
"""Like :meth:`find_object` but sets the innermost object to a
|
"""Like :meth:`find_object` but sets the innermost object to a
|
||||||
new instance of `object_type` if it does not exist.
|
new instance of `object_type` if it does not exist.
|
||||||
"""
|
"""
|
||||||
|
@ -531,17 +632,39 @@ class Context(object):
|
||||||
self.obj = rv = object_type()
|
self.obj = rv = object_type()
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def lookup_default(self, name):
|
@typing.overload
|
||||||
"""Looks up the default for a parameter name. This by default
|
def lookup_default(
|
||||||
looks into the :attr:`default_map` if available.
|
self, name: str, call: "te.Literal[True]" = True
|
||||||
|
) -> t.Optional[t.Any]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@typing.overload
|
||||||
|
def lookup_default(
|
||||||
|
self, name: str, call: "te.Literal[False]" = ...
|
||||||
|
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def lookup_default(self, name: str, call: bool = True) -> t.Optional[t.Any]:
|
||||||
|
"""Get the default for a parameter from :attr:`default_map`.
|
||||||
|
|
||||||
|
:param name: Name of the parameter.
|
||||||
|
:param call: If the default is a callable, call it. Disable to
|
||||||
|
return the callable instead.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the ``call`` parameter.
|
||||||
"""
|
"""
|
||||||
if self.default_map is not None:
|
if self.default_map is not None:
|
||||||
rv = self.default_map.get(name)
|
value = self.default_map.get(name)
|
||||||
if callable(rv):
|
|
||||||
rv = rv()
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def fail(self, message):
|
if call and callable(value):
|
||||||
|
return value()
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def fail(self, message: str) -> "te.NoReturn":
|
||||||
"""Aborts the execution of the program with a specific error
|
"""Aborts the execution of the program with a specific error
|
||||||
message.
|
message.
|
||||||
|
|
||||||
|
@ -549,27 +672,40 @@ class Context(object):
|
||||||
"""
|
"""
|
||||||
raise UsageError(message, self)
|
raise UsageError(message, self)
|
||||||
|
|
||||||
def abort(self):
|
def abort(self) -> "te.NoReturn":
|
||||||
"""Aborts the script."""
|
"""Aborts the script."""
|
||||||
raise Abort()
|
raise Abort()
|
||||||
|
|
||||||
def exit(self, code=0):
|
def exit(self, code: int = 0) -> "te.NoReturn":
|
||||||
"""Exits the application with a given exit code."""
|
"""Exits the application with a given exit code."""
|
||||||
raise Exit(code)
|
raise Exit(code)
|
||||||
|
|
||||||
def get_usage(self):
|
def get_usage(self) -> str:
|
||||||
"""Helper method to get formatted usage string for the current
|
"""Helper method to get formatted usage string for the current
|
||||||
context and command.
|
context and command.
|
||||||
"""
|
"""
|
||||||
return self.command.get_usage(self)
|
return self.command.get_usage(self)
|
||||||
|
|
||||||
def get_help(self):
|
def get_help(self) -> str:
|
||||||
"""Helper method to get formatted help page for the current
|
"""Helper method to get formatted help page for the current
|
||||||
context and command.
|
context and command.
|
||||||
"""
|
"""
|
||||||
return self.command.get_help(self)
|
return self.command.get_help(self)
|
||||||
|
|
||||||
def invoke(*args, **kwargs): # noqa: B902
|
def _make_sub_context(self, command: "Command") -> "Context":
|
||||||
|
"""Create a new context of the same type as this context, but
|
||||||
|
for a new command.
|
||||||
|
|
||||||
|
:meta private:
|
||||||
|
"""
|
||||||
|
return type(self)(command, info_name=command.name, parent=self)
|
||||||
|
|
||||||
|
def invoke(
|
||||||
|
__self, # noqa: B902
|
||||||
|
__callback: t.Union["Command", t.Callable[..., t.Any]],
|
||||||
|
*args: t.Any,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> t.Any:
|
||||||
"""Invokes a command callback in exactly the way it expects. There
|
"""Invokes a command callback in exactly the way it expects. There
|
||||||
are two ways to invoke this method:
|
are two ways to invoke this method:
|
||||||
|
|
||||||
|
@ -584,51 +720,87 @@ class Context(object):
|
||||||
in against the intention of this code and no context was created. For
|
in against the intention of this code and no context was created. For
|
||||||
more information about this change and why it was done in a bugfix
|
more information about this change and why it was done in a bugfix
|
||||||
release see :ref:`upgrade-to-3.2`.
|
release see :ref:`upgrade-to-3.2`.
|
||||||
"""
|
|
||||||
self, callback = args[:2]
|
|
||||||
ctx = self
|
|
||||||
|
|
||||||
# It's also possible to invoke another command which might or
|
.. versionchanged:: 8.0
|
||||||
# might not have a callback. In that case we also fill
|
All ``kwargs`` are tracked in :attr:`params` so they will be
|
||||||
# in defaults and make a new context for this command.
|
passed if :meth:`forward` is called at multiple levels.
|
||||||
if isinstance(callback, Command):
|
"""
|
||||||
other_cmd = callback
|
if isinstance(__callback, Command):
|
||||||
callback = other_cmd.callback
|
other_cmd = __callback
|
||||||
ctx = Context(other_cmd, info_name=other_cmd.name, parent=self)
|
|
||||||
if callback is None:
|
if other_cmd.callback is None:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"The given command does not have a callback that can be invoked."
|
"The given command does not have a callback that can be invoked."
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
__callback = other_cmd.callback
|
||||||
|
|
||||||
|
ctx = __self._make_sub_context(other_cmd)
|
||||||
|
|
||||||
for param in other_cmd.params:
|
for param in other_cmd.params:
|
||||||
if param.name not in kwargs and param.expose_value:
|
if param.name not in kwargs and param.expose_value:
|
||||||
kwargs[param.name] = param.get_default(ctx)
|
kwargs[param.name] = param.get_default(ctx) # type: ignore
|
||||||
|
|
||||||
args = args[2:]
|
# Track all kwargs as params, so that forward() will pass
|
||||||
with augment_usage_errors(self):
|
# them on in subsequent calls.
|
||||||
|
ctx.params.update(kwargs)
|
||||||
|
else:
|
||||||
|
ctx = __self
|
||||||
|
|
||||||
|
with augment_usage_errors(__self):
|
||||||
with ctx:
|
with ctx:
|
||||||
return callback(*args, **kwargs)
|
return __callback(*args, **kwargs)
|
||||||
|
|
||||||
def forward(*args, **kwargs): # noqa: B902
|
def forward(
|
||||||
|
__self, __cmd: "Command", *args: t.Any, **kwargs: t.Any # noqa: B902
|
||||||
|
) -> t.Any:
|
||||||
"""Similar to :meth:`invoke` but fills in default keyword
|
"""Similar to :meth:`invoke` but fills in default keyword
|
||||||
arguments from the current context if the other command expects
|
arguments from the current context if the other command expects
|
||||||
it. This cannot invoke callbacks directly, only other commands.
|
it. This cannot invoke callbacks directly, only other commands.
|
||||||
"""
|
|
||||||
self, cmd = args[:2]
|
|
||||||
|
|
||||||
# It's also possible to invoke another command which might or
|
.. versionchanged:: 8.0
|
||||||
# might not have a callback.
|
All ``kwargs`` are tracked in :attr:`params` so they will be
|
||||||
if not isinstance(cmd, Command):
|
passed if ``forward`` is called at multiple levels.
|
||||||
|
"""
|
||||||
|
# Can only forward to other commands, not direct callbacks.
|
||||||
|
if not isinstance(__cmd, Command):
|
||||||
raise TypeError("Callback is not a command.")
|
raise TypeError("Callback is not a command.")
|
||||||
|
|
||||||
for param in self.params:
|
for param in __self.params:
|
||||||
if param not in kwargs:
|
if param not in kwargs:
|
||||||
kwargs[param] = self.params[param]
|
kwargs[param] = __self.params[param]
|
||||||
|
|
||||||
return self.invoke(cmd, **kwargs)
|
return __self.invoke(__cmd, *args, **kwargs)
|
||||||
|
|
||||||
|
def set_parameter_source(self, name: str, source: ParameterSource) -> None:
|
||||||
|
"""Set the source of a parameter. This indicates the location
|
||||||
|
from which the value of the parameter was obtained.
|
||||||
|
|
||||||
|
:param name: The name of the parameter.
|
||||||
|
:param source: A member of :class:`~click.core.ParameterSource`.
|
||||||
|
"""
|
||||||
|
self._parameter_source[name] = source
|
||||||
|
|
||||||
|
def get_parameter_source(self, name: str) -> t.Optional[ParameterSource]:
|
||||||
|
"""Get the source of a parameter. This indicates the location
|
||||||
|
from which the value of the parameter was obtained.
|
||||||
|
|
||||||
|
This can be useful for determining when a user specified a value
|
||||||
|
on the command line that is the same as the default value. It
|
||||||
|
will be :attr:`~click.core.ParameterSource.DEFAULT` only if the
|
||||||
|
value was actually taken from the default.
|
||||||
|
|
||||||
|
:param name: The name of the parameter.
|
||||||
|
:rtype: ParameterSource
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Returns ``None`` if the parameter was not provided from any
|
||||||
|
source.
|
||||||
|
"""
|
||||||
|
return self._parameter_source.get(name)
|
||||||
|
|
||||||
|
|
||||||
class BaseCommand(object):
|
class BaseCommand:
|
||||||
"""The base command implements the minimal API contract of commands.
|
"""The base command implements the minimal API contract of commands.
|
||||||
Most code will never use this as it does not implement a lot of useful
|
Most code will never use this as it does not implement a lot of useful
|
||||||
functionality but it can act as the direct subclass of alternative
|
functionality but it can act as the direct subclass of alternative
|
||||||
|
@ -650,6 +822,10 @@ class BaseCommand(object):
|
||||||
passed to the context object.
|
passed to the context object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
#: The context class to create with :meth:`make_context`.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 8.0
|
||||||
|
context_class: t.Type[Context] = Context
|
||||||
#: the default for the :attr:`Context.allow_extra_args` flag.
|
#: the default for the :attr:`Context.allow_extra_args` flag.
|
||||||
allow_extra_args = False
|
allow_extra_args = False
|
||||||
#: the default for the :attr:`Context.allow_interspersed_args` flag.
|
#: the default for the :attr:`Context.allow_interspersed_args` flag.
|
||||||
|
@ -657,70 +833,158 @@ class BaseCommand(object):
|
||||||
#: the default for the :attr:`Context.ignore_unknown_options` flag.
|
#: the default for the :attr:`Context.ignore_unknown_options` flag.
|
||||||
ignore_unknown_options = False
|
ignore_unknown_options = False
|
||||||
|
|
||||||
def __init__(self, name, context_settings=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: t.Optional[str],
|
||||||
|
context_settings: t.Optional[t.Dict[str, t.Any]] = None,
|
||||||
|
) -> None:
|
||||||
#: the name the command thinks it has. Upon registering a command
|
#: the name the command thinks it has. Upon registering a command
|
||||||
#: on a :class:`Group` the group will default the command name
|
#: on a :class:`Group` the group will default the command name
|
||||||
#: with this information. You should instead use the
|
#: with this information. You should instead use the
|
||||||
#: :class:`Context`\'s :attr:`~Context.info_name` attribute.
|
#: :class:`Context`\'s :attr:`~Context.info_name` attribute.
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
if context_settings is None:
|
if context_settings is None:
|
||||||
context_settings = {}
|
context_settings = {}
|
||||||
|
|
||||||
#: an optional dictionary with defaults passed to the context.
|
#: an optional dictionary with defaults passed to the context.
|
||||||
self.context_settings = context_settings
|
self.context_settings: t.Dict[str, t.Any] = context_settings
|
||||||
|
|
||||||
def __repr__(self):
|
def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]:
|
||||||
return "<{} {}>".format(self.__class__.__name__, self.name)
|
"""Gather information that could be useful for a tool generating
|
||||||
|
user-facing documentation. This traverses the entire structure
|
||||||
|
below this command.
|
||||||
|
|
||||||
def get_usage(self, ctx):
|
Use :meth:`click.Context.to_info_dict` to traverse the entire
|
||||||
|
CLI structure.
|
||||||
|
|
||||||
|
:param ctx: A :class:`Context` representing this command.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
return {"name": self.name}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__} {self.name}>"
|
||||||
|
|
||||||
|
def get_usage(self, ctx: Context) -> str:
|
||||||
raise NotImplementedError("Base commands cannot get usage")
|
raise NotImplementedError("Base commands cannot get usage")
|
||||||
|
|
||||||
def get_help(self, ctx):
|
def get_help(self, ctx: Context) -> str:
|
||||||
raise NotImplementedError("Base commands cannot get help")
|
raise NotImplementedError("Base commands cannot get help")
|
||||||
|
|
||||||
def make_context(self, info_name, args, parent=None, **extra):
|
def make_context(
|
||||||
|
self,
|
||||||
|
info_name: t.Optional[str],
|
||||||
|
args: t.List[str],
|
||||||
|
parent: t.Optional[Context] = None,
|
||||||
|
**extra: t.Any,
|
||||||
|
) -> Context:
|
||||||
"""This function when given an info name and arguments will kick
|
"""This function when given an info name and arguments will kick
|
||||||
off the parsing and create a new :class:`Context`. It does not
|
off the parsing and create a new :class:`Context`. It does not
|
||||||
invoke the actual command callback though.
|
invoke the actual command callback though.
|
||||||
|
|
||||||
:param info_name: the info name for this invokation. Generally this
|
To quickly customize the context class used without overriding
|
||||||
|
this method, set the :attr:`context_class` attribute.
|
||||||
|
|
||||||
|
:param info_name: the info name for this invocation. Generally this
|
||||||
is the most descriptive name for the script or
|
is the most descriptive name for the script or
|
||||||
command. For the toplevel script it's usually
|
command. For the toplevel script it's usually
|
||||||
the name of the script, for commands below it it's
|
the name of the script, for commands below it it's
|
||||||
the name of the script.
|
the name of the command.
|
||||||
:param args: the arguments to parse as list of strings.
|
:param args: the arguments to parse as list of strings.
|
||||||
:param parent: the parent context if available.
|
:param parent: the parent context if available.
|
||||||
:param extra: extra keyword arguments forwarded to the context
|
:param extra: extra keyword arguments forwarded to the context
|
||||||
constructor.
|
constructor.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the :attr:`context_class` attribute.
|
||||||
"""
|
"""
|
||||||
for key, value in iteritems(self.context_settings):
|
for key, value in self.context_settings.items():
|
||||||
if key not in extra:
|
if key not in extra:
|
||||||
extra[key] = value
|
extra[key] = value
|
||||||
ctx = Context(self, info_name=info_name, parent=parent, **extra)
|
|
||||||
|
ctx = self.context_class(
|
||||||
|
self, info_name=info_name, parent=parent, **extra # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
with ctx.scope(cleanup=False):
|
with ctx.scope(cleanup=False):
|
||||||
self.parse_args(ctx, args)
|
self.parse_args(ctx, args)
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def parse_args(self, ctx, args):
|
def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]:
|
||||||
"""Given a context and a list of arguments this creates the parser
|
"""Given a context and a list of arguments this creates the parser
|
||||||
and parses the arguments, then modifies the context as necessary.
|
and parses the arguments, then modifies the context as necessary.
|
||||||
This is automatically invoked by :meth:`make_context`.
|
This is automatically invoked by :meth:`make_context`.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Base commands do not know how to parse arguments.")
|
raise NotImplementedError("Base commands do not know how to parse arguments.")
|
||||||
|
|
||||||
def invoke(self, ctx):
|
def invoke(self, ctx: Context) -> t.Any:
|
||||||
"""Given a context, this invokes the command. The default
|
"""Given a context, this invokes the command. The default
|
||||||
implementation is raising a not implemented error.
|
implementation is raising a not implemented error.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Base commands are not invokable by default")
|
raise NotImplementedError("Base commands are not invokable by default")
|
||||||
|
|
||||||
|
def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]:
|
||||||
|
"""Return a list of completions for the incomplete value. Looks
|
||||||
|
at the names of chained multi-commands.
|
||||||
|
|
||||||
|
Any command could be part of a chained multi-command, so sibling
|
||||||
|
commands are valid at any point during command completion. Other
|
||||||
|
command classes will return more completions.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for this command.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
from click.shell_completion import CompletionItem
|
||||||
|
|
||||||
|
results: t.List["CompletionItem"] = []
|
||||||
|
|
||||||
|
while ctx.parent is not None:
|
||||||
|
ctx = ctx.parent
|
||||||
|
|
||||||
|
if isinstance(ctx.command, MultiCommand) and ctx.command.chain:
|
||||||
|
results.extend(
|
||||||
|
CompletionItem(name, help=command.get_short_help_str())
|
||||||
|
for name, command in _complete_visible_commands(ctx, incomplete)
|
||||||
|
if name not in ctx.protected_args
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@typing.overload
|
||||||
def main(
|
def main(
|
||||||
self,
|
self,
|
||||||
args=None,
|
args: t.Optional[t.Sequence[str]] = None,
|
||||||
prog_name=None,
|
prog_name: t.Optional[str] = None,
|
||||||
complete_var=None,
|
complete_var: t.Optional[str] = None,
|
||||||
standalone_mode=True,
|
standalone_mode: "te.Literal[True]" = True,
|
||||||
**extra
|
**extra: t.Any,
|
||||||
):
|
) -> "te.NoReturn":
|
||||||
|
...
|
||||||
|
|
||||||
|
@typing.overload
|
||||||
|
def main(
|
||||||
|
self,
|
||||||
|
args: t.Optional[t.Sequence[str]] = None,
|
||||||
|
prog_name: t.Optional[str] = None,
|
||||||
|
complete_var: t.Optional[str] = None,
|
||||||
|
standalone_mode: bool = ...,
|
||||||
|
**extra: t.Any,
|
||||||
|
) -> t.Any:
|
||||||
|
...
|
||||||
|
|
||||||
|
def main(
|
||||||
|
self,
|
||||||
|
args: t.Optional[t.Sequence[str]] = None,
|
||||||
|
prog_name: t.Optional[str] = None,
|
||||||
|
complete_var: t.Optional[str] = None,
|
||||||
|
standalone_mode: bool = True,
|
||||||
|
windows_expand_args: bool = True,
|
||||||
|
**extra: t.Any,
|
||||||
|
) -> t.Any:
|
||||||
"""This is the way to invoke a script with all the bells and
|
"""This is the way to invoke a script with all the bells and
|
||||||
whistles as a command line application. This will always terminate
|
whistles as a command line application. This will always terminate
|
||||||
the application after a call. If this is not wanted, ``SystemExit``
|
the application after a call. If this is not wanted, ``SystemExit``
|
||||||
|
@ -729,9 +993,6 @@ class BaseCommand(object):
|
||||||
This method is also available by directly calling the instance of
|
This method is also available by directly calling the instance of
|
||||||
a :class:`Command`.
|
a :class:`Command`.
|
||||||
|
|
||||||
.. versionadded:: 3.0
|
|
||||||
Added the `standalone_mode` flag to control the standalone mode.
|
|
||||||
|
|
||||||
:param args: the arguments that should be used for parsing. If not
|
:param args: the arguments that should be used for parsing. If not
|
||||||
provided, ``sys.argv[1:]`` is used.
|
provided, ``sys.argv[1:]`` is used.
|
||||||
:param prog_name: the program name that should be used. By default
|
:param prog_name: the program name that should be used. By default
|
||||||
|
@ -750,31 +1011,39 @@ class BaseCommand(object):
|
||||||
propagated to the caller and the return
|
propagated to the caller and the return
|
||||||
value of this function is the return value
|
value of this function is the return value
|
||||||
of :meth:`invoke`.
|
of :meth:`invoke`.
|
||||||
|
:param windows_expand_args: Expand glob patterns, user dir, and
|
||||||
|
env vars in command line args on Windows.
|
||||||
:param extra: extra keyword arguments are forwarded to the context
|
:param extra: extra keyword arguments are forwarded to the context
|
||||||
constructor. See :class:`Context` for more information.
|
constructor. See :class:`Context` for more information.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0.1
|
||||||
|
Added the ``windows_expand_args`` parameter to allow
|
||||||
|
disabling command line arg expansion on Windows.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
When taking arguments from ``sys.argv`` on Windows, glob
|
||||||
|
patterns, user dir, and env vars are expanded.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Added the ``standalone_mode`` parameter.
|
||||||
"""
|
"""
|
||||||
# If we are in Python 3, we will verify that the environment is
|
# Verify that the environment is configured correctly, or reject
|
||||||
# sane at this point or reject further execution to avoid a
|
# further execution to avoid a broken script.
|
||||||
# broken script.
|
_verify_python_env()
|
||||||
if not PY2:
|
|
||||||
_verify_python3_env()
|
|
||||||
else:
|
|
||||||
_check_for_unicode_literals()
|
|
||||||
|
|
||||||
if args is None:
|
if args is None:
|
||||||
args = get_os_args()
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
if os.name == "nt" and windows_expand_args:
|
||||||
|
args = _expand_args(args)
|
||||||
else:
|
else:
|
||||||
args = list(args)
|
args = list(args)
|
||||||
|
|
||||||
if prog_name is None:
|
if prog_name is None:
|
||||||
prog_name = make_str(
|
prog_name = _detect_program_name()
|
||||||
os.path.basename(sys.argv[0] if sys.argv else __file__)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Hook for the Bash completion. This only activates if the Bash
|
# Process shell completion requests and exit early.
|
||||||
# completion is actually enabled, otherwise this is quite a fast
|
self._main_shell_completion(extra, prog_name, complete_var)
|
||||||
# noop.
|
|
||||||
_bashcomplete(self, prog_name, complete_var)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
@ -792,16 +1061,16 @@ class BaseCommand(object):
|
||||||
ctx.exit()
|
ctx.exit()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
echo(file=sys.stderr)
|
echo(file=sys.stderr)
|
||||||
raise Abort()
|
raise Abort() from None
|
||||||
except ClickException as e:
|
except ClickException as e:
|
||||||
if not standalone_mode:
|
if not standalone_mode:
|
||||||
raise
|
raise
|
||||||
e.show()
|
e.show()
|
||||||
sys.exit(e.exit_code)
|
sys.exit(e.exit_code)
|
||||||
except IOError as e:
|
except OSError as e:
|
||||||
if e.errno == errno.EPIPE:
|
if e.errno == errno.EPIPE:
|
||||||
sys.stdout = PacifyFlushWrapper(sys.stdout)
|
sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout))
|
||||||
sys.stderr = PacifyFlushWrapper(sys.stderr)
|
sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
@ -821,10 +1090,38 @@ class BaseCommand(object):
|
||||||
except Abort:
|
except Abort:
|
||||||
if not standalone_mode:
|
if not standalone_mode:
|
||||||
raise
|
raise
|
||||||
echo("Aborted!", file=sys.stderr)
|
echo(_("Aborted!"), file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def _main_shell_completion(
|
||||||
|
self,
|
||||||
|
ctx_args: t.Dict[str, t.Any],
|
||||||
|
prog_name: str,
|
||||||
|
complete_var: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Check if the shell is asking for tab completion, process
|
||||||
|
that, then exit early. Called from :meth:`main` before the
|
||||||
|
program is invoked.
|
||||||
|
|
||||||
|
:param prog_name: Name of the executable in the shell.
|
||||||
|
:param complete_var: Name of the environment variable that holds
|
||||||
|
the completion instruction. Defaults to
|
||||||
|
``_{PROG_NAME}_COMPLETE``.
|
||||||
|
"""
|
||||||
|
if complete_var is None:
|
||||||
|
complete_var = f"_{prog_name}_COMPLETE".replace("-", "_").upper()
|
||||||
|
|
||||||
|
instruction = os.environ.get(complete_var)
|
||||||
|
|
||||||
|
if not instruction:
|
||||||
|
return
|
||||||
|
|
||||||
|
from .shell_completion import shell_complete
|
||||||
|
|
||||||
|
rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction)
|
||||||
|
sys.exit(rv)
|
||||||
|
|
||||||
|
def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
"""Alias for :meth:`main`."""
|
"""Alias for :meth:`main`."""
|
||||||
return self.main(*args, **kwargs)
|
return self.main(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -836,6 +1133,8 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
.. versionchanged:: 2.0
|
.. versionchanged:: 2.0
|
||||||
Added the `context_settings` parameter.
|
Added the `context_settings` parameter.
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added repr showing the command name
|
||||||
.. versionchanged:: 7.1
|
.. versionchanged:: 7.1
|
||||||
Added the `no_args_is_help` parameter.
|
Added the `no_args_is_help` parameter.
|
||||||
|
|
||||||
|
@ -864,31 +1163,33 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name,
|
name: t.Optional[str],
|
||||||
context_settings=None,
|
context_settings: t.Optional[t.Dict[str, t.Any]] = None,
|
||||||
callback=None,
|
callback: t.Optional[t.Callable[..., t.Any]] = None,
|
||||||
params=None,
|
params: t.Optional[t.List["Parameter"]] = None,
|
||||||
help=None,
|
help: t.Optional[str] = None,
|
||||||
epilog=None,
|
epilog: t.Optional[str] = None,
|
||||||
short_help=None,
|
short_help: t.Optional[str] = None,
|
||||||
options_metavar="[OPTIONS]",
|
options_metavar: t.Optional[str] = "[OPTIONS]",
|
||||||
add_help_option=True,
|
add_help_option: bool = True,
|
||||||
no_args_is_help=False,
|
no_args_is_help: bool = False,
|
||||||
hidden=False,
|
hidden: bool = False,
|
||||||
deprecated=False,
|
deprecated: bool = False,
|
||||||
):
|
) -> None:
|
||||||
BaseCommand.__init__(self, name, context_settings)
|
super().__init__(name, context_settings)
|
||||||
#: the callback to execute when the command fires. This might be
|
#: the callback to execute when the command fires. This might be
|
||||||
#: `None` in which case nothing happens.
|
#: `None` in which case nothing happens.
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
#: the list of parameters for this command in the order they
|
#: the list of parameters for this command in the order they
|
||||||
#: should show up in the help page and execute. Eager parameters
|
#: should show up in the help page and execute. Eager parameters
|
||||||
#: will automatically be handled before non eager ones.
|
#: will automatically be handled before non eager ones.
|
||||||
self.params = params or []
|
self.params: t.List["Parameter"] = params or []
|
||||||
|
|
||||||
# if a form feed (page break) is found in the help text, truncate help
|
# if a form feed (page break) is found in the help text, truncate help
|
||||||
# text to the content preceding the first form feed
|
# text to the content preceding the first form feed
|
||||||
if help and "\f" in help:
|
if help and "\f" in help:
|
||||||
help = help.split("\f", 1)[0]
|
help = help.split("\f", 1)[0]
|
||||||
|
|
||||||
self.help = help
|
self.help = help
|
||||||
self.epilog = epilog
|
self.epilog = epilog
|
||||||
self.options_metavar = options_metavar
|
self.options_metavar = options_metavar
|
||||||
|
@ -898,7 +1199,19 @@ class Command(BaseCommand):
|
||||||
self.hidden = hidden
|
self.hidden = hidden
|
||||||
self.deprecated = deprecated
|
self.deprecated = deprecated
|
||||||
|
|
||||||
def get_usage(self, ctx):
|
def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]:
|
||||||
|
info_dict = super().to_info_dict(ctx)
|
||||||
|
info_dict.update(
|
||||||
|
params=[param.to_info_dict() for param in self.get_params(ctx)],
|
||||||
|
help=self.help,
|
||||||
|
epilog=self.epilog,
|
||||||
|
short_help=self.short_help,
|
||||||
|
hidden=self.hidden,
|
||||||
|
deprecated=self.deprecated,
|
||||||
|
)
|
||||||
|
return info_dict
|
||||||
|
|
||||||
|
def get_usage(self, ctx: Context) -> str:
|
||||||
"""Formats the usage line into a string and returns it.
|
"""Formats the usage line into a string and returns it.
|
||||||
|
|
||||||
Calls :meth:`format_usage` internally.
|
Calls :meth:`format_usage` internally.
|
||||||
|
@ -907,14 +1220,16 @@ class Command(BaseCommand):
|
||||||
self.format_usage(ctx, formatter)
|
self.format_usage(ctx, formatter)
|
||||||
return formatter.getvalue().rstrip("\n")
|
return formatter.getvalue().rstrip("\n")
|
||||||
|
|
||||||
def get_params(self, ctx):
|
def get_params(self, ctx: Context) -> t.List["Parameter"]:
|
||||||
rv = self.params
|
rv = self.params
|
||||||
help_option = self.get_help_option(ctx)
|
help_option = self.get_help_option(ctx)
|
||||||
|
|
||||||
if help_option is not None:
|
if help_option is not None:
|
||||||
rv = rv + [help_option]
|
rv = [*rv, help_option]
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def format_usage(self, ctx, formatter):
|
def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None:
|
||||||
"""Writes the usage line into the formatter.
|
"""Writes the usage line into the formatter.
|
||||||
|
|
||||||
This is a low-level method called by :meth:`get_usage`.
|
This is a low-level method called by :meth:`get_usage`.
|
||||||
|
@ -922,30 +1237,33 @@ class Command(BaseCommand):
|
||||||
pieces = self.collect_usage_pieces(ctx)
|
pieces = self.collect_usage_pieces(ctx)
|
||||||
formatter.write_usage(ctx.command_path, " ".join(pieces))
|
formatter.write_usage(ctx.command_path, " ".join(pieces))
|
||||||
|
|
||||||
def collect_usage_pieces(self, ctx):
|
def collect_usage_pieces(self, ctx: Context) -> t.List[str]:
|
||||||
"""Returns all the pieces that go into the usage line and returns
|
"""Returns all the pieces that go into the usage line and returns
|
||||||
it as a list of strings.
|
it as a list of strings.
|
||||||
"""
|
"""
|
||||||
rv = [self.options_metavar]
|
rv = [self.options_metavar] if self.options_metavar else []
|
||||||
|
|
||||||
for param in self.get_params(ctx):
|
for param in self.get_params(ctx):
|
||||||
rv.extend(param.get_usage_pieces(ctx))
|
rv.extend(param.get_usage_pieces(ctx))
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def get_help_option_names(self, ctx):
|
def get_help_option_names(self, ctx: Context) -> t.List[str]:
|
||||||
"""Returns the names for the help option."""
|
"""Returns the names for the help option."""
|
||||||
all_names = set(ctx.help_option_names)
|
all_names = set(ctx.help_option_names)
|
||||||
for param in self.params:
|
for param in self.params:
|
||||||
all_names.difference_update(param.opts)
|
all_names.difference_update(param.opts)
|
||||||
all_names.difference_update(param.secondary_opts)
|
all_names.difference_update(param.secondary_opts)
|
||||||
return all_names
|
return list(all_names)
|
||||||
|
|
||||||
def get_help_option(self, ctx):
|
def get_help_option(self, ctx: Context) -> t.Optional["Option"]:
|
||||||
"""Returns the help option object."""
|
"""Returns the help option object."""
|
||||||
help_options = self.get_help_option_names(ctx)
|
help_options = self.get_help_option_names(ctx)
|
||||||
if not help_options or not self.add_help_option:
|
|
||||||
return
|
|
||||||
|
|
||||||
def show_help(ctx, param, value):
|
if not help_options or not self.add_help_option:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def show_help(ctx: Context, param: "Parameter", value: str) -> None:
|
||||||
if value and not ctx.resilient_parsing:
|
if value and not ctx.resilient_parsing:
|
||||||
echo(ctx.get_help(), color=ctx.color)
|
echo(ctx.get_help(), color=ctx.color)
|
||||||
ctx.exit()
|
ctx.exit()
|
||||||
|
@ -956,17 +1274,17 @@ class Command(BaseCommand):
|
||||||
is_eager=True,
|
is_eager=True,
|
||||||
expose_value=False,
|
expose_value=False,
|
||||||
callback=show_help,
|
callback=show_help,
|
||||||
help="Show this message and exit.",
|
help=_("Show this message and exit."),
|
||||||
)
|
)
|
||||||
|
|
||||||
def make_parser(self, ctx):
|
def make_parser(self, ctx: Context) -> OptionParser:
|
||||||
"""Creates the underlying option parser for this command."""
|
"""Creates the underlying option parser for this command."""
|
||||||
parser = OptionParser(ctx)
|
parser = OptionParser(ctx)
|
||||||
for param in self.get_params(ctx):
|
for param in self.get_params(ctx):
|
||||||
param.add_to_parser(parser, ctx)
|
param.add_to_parser(parser, ctx)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def get_help(self, ctx):
|
def get_help(self, ctx: Context) -> str:
|
||||||
"""Formats the help into a string and returns it.
|
"""Formats the help into a string and returns it.
|
||||||
|
|
||||||
Calls :meth:`format_help` internally.
|
Calls :meth:`format_help` internally.
|
||||||
|
@ -975,18 +1293,21 @@ class Command(BaseCommand):
|
||||||
self.format_help(ctx, formatter)
|
self.format_help(ctx, formatter)
|
||||||
return formatter.getvalue().rstrip("\n")
|
return formatter.getvalue().rstrip("\n")
|
||||||
|
|
||||||
def get_short_help_str(self, limit=45):
|
def get_short_help_str(self, limit: int = 45) -> str:
|
||||||
"""Gets short help for the command or makes it by shortening the
|
"""Gets short help for the command or makes it by shortening the
|
||||||
long help string.
|
long help string.
|
||||||
"""
|
"""
|
||||||
return (
|
text = self.short_help or ""
|
||||||
self.short_help
|
|
||||||
or self.help
|
|
||||||
and make_default_short_help(self.help, limit)
|
|
||||||
or ""
|
|
||||||
)
|
|
||||||
|
|
||||||
def format_help(self, ctx, formatter):
|
if not text and self.help:
|
||||||
|
text = make_default_short_help(self.help, limit)
|
||||||
|
|
||||||
|
if self.deprecated:
|
||||||
|
text = _("(Deprecated) {text}").format(text=text)
|
||||||
|
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
def format_help(self, ctx: Context, formatter: HelpFormatter) -> None:
|
||||||
"""Writes the help into the formatter if it exists.
|
"""Writes the help into the formatter if it exists.
|
||||||
|
|
||||||
This is a low-level method called by :meth:`get_help`.
|
This is a low-level method called by :meth:`get_help`.
|
||||||
|
@ -1003,21 +1324,20 @@ class Command(BaseCommand):
|
||||||
self.format_options(ctx, formatter)
|
self.format_options(ctx, formatter)
|
||||||
self.format_epilog(ctx, formatter)
|
self.format_epilog(ctx, formatter)
|
||||||
|
|
||||||
def format_help_text(self, ctx, formatter):
|
def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None:
|
||||||
"""Writes the help text to the formatter if it exists."""
|
"""Writes the help text to the formatter if it exists."""
|
||||||
if self.help:
|
text = self.help or ""
|
||||||
formatter.write_paragraph()
|
|
||||||
with formatter.indentation():
|
|
||||||
help_text = self.help
|
|
||||||
if self.deprecated:
|
|
||||||
help_text += DEPRECATED_HELP_NOTICE
|
|
||||||
formatter.write_text(help_text)
|
|
||||||
elif self.deprecated:
|
|
||||||
formatter.write_paragraph()
|
|
||||||
with formatter.indentation():
|
|
||||||
formatter.write_text(DEPRECATED_HELP_NOTICE)
|
|
||||||
|
|
||||||
def format_options(self, ctx, formatter):
|
if self.deprecated:
|
||||||
|
text = _("(Deprecated) {text}").format(text=text)
|
||||||
|
|
||||||
|
if text:
|
||||||
|
formatter.write_paragraph()
|
||||||
|
|
||||||
|
with formatter.indentation():
|
||||||
|
formatter.write_text(text)
|
||||||
|
|
||||||
|
def format_options(self, ctx: Context, formatter: HelpFormatter) -> None:
|
||||||
"""Writes all the options into the formatter if they exist."""
|
"""Writes all the options into the formatter if they exist."""
|
||||||
opts = []
|
opts = []
|
||||||
for param in self.get_params(ctx):
|
for param in self.get_params(ctx):
|
||||||
|
@ -1026,17 +1346,17 @@ class Command(BaseCommand):
|
||||||
opts.append(rv)
|
opts.append(rv)
|
||||||
|
|
||||||
if opts:
|
if opts:
|
||||||
with formatter.section("Options"):
|
with formatter.section(_("Options")):
|
||||||
formatter.write_dl(opts)
|
formatter.write_dl(opts)
|
||||||
|
|
||||||
def format_epilog(self, ctx, formatter):
|
def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None:
|
||||||
"""Writes the epilog into the formatter if it exists."""
|
"""Writes the epilog into the formatter if it exists."""
|
||||||
if self.epilog:
|
if self.epilog:
|
||||||
formatter.write_paragraph()
|
formatter.write_paragraph()
|
||||||
with formatter.indentation():
|
with formatter.indentation():
|
||||||
formatter.write_text(self.epilog)
|
formatter.write_text(self.epilog)
|
||||||
|
|
||||||
def parse_args(self, ctx, args):
|
def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]:
|
||||||
if not args and self.no_args_is_help and not ctx.resilient_parsing:
|
if not args and self.no_args_is_help and not ctx.resilient_parsing:
|
||||||
echo(ctx.get_help(), color=ctx.color)
|
echo(ctx.get_help(), color=ctx.color)
|
||||||
ctx.exit()
|
ctx.exit()
|
||||||
|
@ -1049,22 +1369,64 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
|
if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
|
||||||
ctx.fail(
|
ctx.fail(
|
||||||
"Got unexpected extra argument{} ({})".format(
|
ngettext(
|
||||||
"s" if len(args) != 1 else "", " ".join(map(make_str, args))
|
"Got unexpected extra argument ({args})",
|
||||||
)
|
"Got unexpected extra arguments ({args})",
|
||||||
|
len(args),
|
||||||
|
).format(args=" ".join(map(str, args)))
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx.args = args
|
ctx.args = args
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def invoke(self, ctx):
|
def invoke(self, ctx: Context) -> t.Any:
|
||||||
"""Given a context, this invokes the attached callback (if it exists)
|
"""Given a context, this invokes the attached callback (if it exists)
|
||||||
in the right way.
|
in the right way.
|
||||||
"""
|
"""
|
||||||
_maybe_show_deprecated_notice(self)
|
if self.deprecated:
|
||||||
|
message = _(
|
||||||
|
"DeprecationWarning: The command {name!r} is deprecated."
|
||||||
|
).format(name=self.name)
|
||||||
|
echo(style(message, fg="red"), err=True)
|
||||||
|
|
||||||
if self.callback is not None:
|
if self.callback is not None:
|
||||||
return ctx.invoke(self.callback, **ctx.params)
|
return ctx.invoke(self.callback, **ctx.params)
|
||||||
|
|
||||||
|
def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]:
|
||||||
|
"""Return a list of completions for the incomplete value. Looks
|
||||||
|
at the names of options and chained multi-commands.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for this command.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
from click.shell_completion import CompletionItem
|
||||||
|
|
||||||
|
results: t.List["CompletionItem"] = []
|
||||||
|
|
||||||
|
if incomplete and not incomplete[0].isalnum():
|
||||||
|
for param in self.get_params(ctx):
|
||||||
|
if (
|
||||||
|
not isinstance(param, Option)
|
||||||
|
or param.hidden
|
||||||
|
or (
|
||||||
|
not param.multiple
|
||||||
|
and ctx.get_parameter_source(param.name) # type: ignore
|
||||||
|
is ParameterSource.COMMANDLINE
|
||||||
|
)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
results.extend(
|
||||||
|
CompletionItem(name, help=param.help)
|
||||||
|
for name in [*param.opts, *param.secondary_opts]
|
||||||
|
if name.startswith(incomplete)
|
||||||
|
)
|
||||||
|
|
||||||
|
results.extend(super().shell_complete(ctx, incomplete))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
class MultiCommand(Command):
|
class MultiCommand(Command):
|
||||||
"""A multi command is the basic implementation of a command that
|
"""A multi command is the basic implementation of a command that
|
||||||
|
@ -1086,8 +1448,9 @@ class MultiCommand(Command):
|
||||||
is enabled. This restricts the form of commands in that
|
is enabled. This restricts the form of commands in that
|
||||||
they cannot have optional arguments but it allows
|
they cannot have optional arguments but it allows
|
||||||
multiple commands to be chained together.
|
multiple commands to be chained together.
|
||||||
:param result_callback: the result callback to attach to this multi
|
:param result_callback: The result callback to attach to this multi
|
||||||
command.
|
command. This can be set or changed later with the
|
||||||
|
:meth:`result_callback` decorator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allow_extra_args = True
|
allow_extra_args = True
|
||||||
|
@ -1095,29 +1458,33 @@ class MultiCommand(Command):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name=None,
|
name: t.Optional[str] = None,
|
||||||
invoke_without_command=False,
|
invoke_without_command: bool = False,
|
||||||
no_args_is_help=None,
|
no_args_is_help: t.Optional[bool] = None,
|
||||||
subcommand_metavar=None,
|
subcommand_metavar: t.Optional[str] = None,
|
||||||
chain=False,
|
chain: bool = False,
|
||||||
result_callback=None,
|
result_callback: t.Optional[t.Callable[..., t.Any]] = None,
|
||||||
**attrs
|
**attrs: t.Any,
|
||||||
):
|
) -> None:
|
||||||
Command.__init__(self, name, **attrs)
|
super().__init__(name, **attrs)
|
||||||
|
|
||||||
if no_args_is_help is None:
|
if no_args_is_help is None:
|
||||||
no_args_is_help = not invoke_without_command
|
no_args_is_help = not invoke_without_command
|
||||||
|
|
||||||
self.no_args_is_help = no_args_is_help
|
self.no_args_is_help = no_args_is_help
|
||||||
self.invoke_without_command = invoke_without_command
|
self.invoke_without_command = invoke_without_command
|
||||||
|
|
||||||
if subcommand_metavar is None:
|
if subcommand_metavar is None:
|
||||||
if chain:
|
if chain:
|
||||||
subcommand_metavar = SUBCOMMANDS_METAVAR
|
subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
|
||||||
else:
|
else:
|
||||||
subcommand_metavar = SUBCOMMAND_METAVAR
|
subcommand_metavar = "COMMAND [ARGS]..."
|
||||||
|
|
||||||
self.subcommand_metavar = subcommand_metavar
|
self.subcommand_metavar = subcommand_metavar
|
||||||
self.chain = chain
|
self.chain = chain
|
||||||
#: The result callback that is stored. This can be set or
|
# The result callback that is stored. This can be set or
|
||||||
#: overridden with the :func:`resultcallback` decorator.
|
# overridden with the :func:`result_callback` decorator.
|
||||||
self.result_callback = result_callback
|
self._result_callback = result_callback
|
||||||
|
|
||||||
if self.chain:
|
if self.chain:
|
||||||
for param in self.params:
|
for param in self.params:
|
||||||
|
@ -1127,17 +1494,35 @@ class MultiCommand(Command):
|
||||||
" optional arguments."
|
" optional arguments."
|
||||||
)
|
)
|
||||||
|
|
||||||
def collect_usage_pieces(self, ctx):
|
def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]:
|
||||||
rv = Command.collect_usage_pieces(self, ctx)
|
info_dict = super().to_info_dict(ctx)
|
||||||
|
commands = {}
|
||||||
|
|
||||||
|
for name in self.list_commands(ctx):
|
||||||
|
command = self.get_command(ctx, name)
|
||||||
|
|
||||||
|
if command is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sub_ctx = ctx._make_sub_context(command)
|
||||||
|
|
||||||
|
with sub_ctx.scope(cleanup=False):
|
||||||
|
commands[name] = command.to_info_dict(sub_ctx)
|
||||||
|
|
||||||
|
info_dict.update(commands=commands, chain=self.chain)
|
||||||
|
return info_dict
|
||||||
|
|
||||||
|
def collect_usage_pieces(self, ctx: Context) -> t.List[str]:
|
||||||
|
rv = super().collect_usage_pieces(ctx)
|
||||||
rv.append(self.subcommand_metavar)
|
rv.append(self.subcommand_metavar)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def format_options(self, ctx, formatter):
|
def format_options(self, ctx: Context, formatter: HelpFormatter) -> None:
|
||||||
Command.format_options(self, ctx, formatter)
|
super().format_options(ctx, formatter)
|
||||||
self.format_commands(ctx, formatter)
|
self.format_commands(ctx, formatter)
|
||||||
|
|
||||||
def resultcallback(self, replace=False):
|
def result_callback(self, replace: bool = False) -> t.Callable[[F], F]:
|
||||||
"""Adds a result callback to the chain command. By default if a
|
"""Adds a result callback to the command. By default if a
|
||||||
result callback is already registered this will chain them but
|
result callback is already registered this will chain them but
|
||||||
this can be disabled with the `replace` parameter. The result
|
this can be disabled with the `replace` parameter. The result
|
||||||
callback is invoked with the return value of the subcommand
|
callback is invoked with the return value of the subcommand
|
||||||
|
@ -1152,31 +1537,47 @@ class MultiCommand(Command):
|
||||||
def cli(input):
|
def cli(input):
|
||||||
return 42
|
return 42
|
||||||
|
|
||||||
@cli.resultcallback()
|
@cli.result_callback()
|
||||||
def process_result(result, input):
|
def process_result(result, input):
|
||||||
return result + input
|
return result + input
|
||||||
|
|
||||||
.. versionadded:: 3.0
|
|
||||||
|
|
||||||
:param replace: if set to `True` an already existing result
|
:param replace: if set to `True` an already existing result
|
||||||
callback will be removed.
|
callback will be removed.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Renamed from ``resultcallback``.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f: F) -> F:
|
||||||
old_callback = self.result_callback
|
old_callback = self._result_callback
|
||||||
|
|
||||||
if old_callback is None or replace:
|
if old_callback is None or replace:
|
||||||
self.result_callback = f
|
self._result_callback = f
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def function(__value, *args, **kwargs):
|
def function(__value, *args, **kwargs): # type: ignore
|
||||||
return f(old_callback(__value, *args, **kwargs), *args, **kwargs)
|
inner = old_callback(__value, *args, **kwargs) # type: ignore
|
||||||
|
return f(inner, *args, **kwargs)
|
||||||
|
|
||||||
self.result_callback = rv = update_wrapper(function, f)
|
self._result_callback = rv = update_wrapper(t.cast(F, function), f)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def format_commands(self, ctx, formatter):
|
def resultcallback(self, replace: bool = False) -> t.Callable[[F], F]:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'resultcallback' has been renamed to 'result_callback'."
|
||||||
|
" The old name will be removed in Click 8.1.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return self.result_callback(replace=replace)
|
||||||
|
|
||||||
|
def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None:
|
||||||
"""Extra format methods for multi methods that adds all the commands
|
"""Extra format methods for multi methods that adds all the commands
|
||||||
after the options.
|
after the options.
|
||||||
"""
|
"""
|
||||||
|
@ -1201,15 +1602,16 @@ class MultiCommand(Command):
|
||||||
rows.append((subcommand, help))
|
rows.append((subcommand, help))
|
||||||
|
|
||||||
if rows:
|
if rows:
|
||||||
with formatter.section("Commands"):
|
with formatter.section(_("Commands")):
|
||||||
formatter.write_dl(rows)
|
formatter.write_dl(rows)
|
||||||
|
|
||||||
def parse_args(self, ctx, args):
|
def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]:
|
||||||
if not args and self.no_args_is_help and not ctx.resilient_parsing:
|
if not args and self.no_args_is_help and not ctx.resilient_parsing:
|
||||||
echo(ctx.get_help(), color=ctx.color)
|
echo(ctx.get_help(), color=ctx.color)
|
||||||
ctx.exit()
|
ctx.exit()
|
||||||
|
|
||||||
rest = Command.parse_args(self, ctx, args)
|
rest = super().parse_args(ctx, args)
|
||||||
|
|
||||||
if self.chain:
|
if self.chain:
|
||||||
ctx.protected_args = rest
|
ctx.protected_args = rest
|
||||||
ctx.args = []
|
ctx.args = []
|
||||||
|
@ -1218,29 +1620,24 @@ class MultiCommand(Command):
|
||||||
|
|
||||||
return ctx.args
|
return ctx.args
|
||||||
|
|
||||||
def invoke(self, ctx):
|
def invoke(self, ctx: Context) -> t.Any:
|
||||||
def _process_result(value):
|
def _process_result(value: t.Any) -> t.Any:
|
||||||
if self.result_callback is not None:
|
if self._result_callback is not None:
|
||||||
value = ctx.invoke(self.result_callback, value, **ctx.params)
|
value = ctx.invoke(self._result_callback, value, **ctx.params)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if not ctx.protected_args:
|
if not ctx.protected_args:
|
||||||
# If we are invoked without command the chain flag controls
|
|
||||||
# how this happens. If we are not in chain mode, the return
|
|
||||||
# value here is the return value of the command.
|
|
||||||
# If however we are in chain mode, the return value is the
|
|
||||||
# return value of the result processor invoked with an empty
|
|
||||||
# list (which means that no subcommand actually was executed).
|
|
||||||
if self.invoke_without_command:
|
if self.invoke_without_command:
|
||||||
if not self.chain:
|
# No subcommand was invoked, so the result callback is
|
||||||
return Command.invoke(self, ctx)
|
# invoked with None for regular groups, or an empty list
|
||||||
|
# for chained groups.
|
||||||
with ctx:
|
with ctx:
|
||||||
Command.invoke(self, ctx)
|
super().invoke(ctx)
|
||||||
return _process_result([])
|
return _process_result([] if self.chain else None)
|
||||||
ctx.fail("Missing command.")
|
ctx.fail(_("Missing command."))
|
||||||
|
|
||||||
# Fetch args back out
|
# Fetch args back out
|
||||||
args = ctx.protected_args + ctx.args
|
args = [*ctx.protected_args, *ctx.args]
|
||||||
ctx.args = []
|
ctx.args = []
|
||||||
ctx.protected_args = []
|
ctx.protected_args = []
|
||||||
|
|
||||||
|
@ -1252,8 +1649,9 @@ class MultiCommand(Command):
|
||||||
# resources until the result processor has worked.
|
# resources until the result processor has worked.
|
||||||
with ctx:
|
with ctx:
|
||||||
cmd_name, cmd, args = self.resolve_command(ctx, args)
|
cmd_name, cmd, args = self.resolve_command(ctx, args)
|
||||||
|
assert cmd is not None
|
||||||
ctx.invoked_subcommand = cmd_name
|
ctx.invoked_subcommand = cmd_name
|
||||||
Command.invoke(self, ctx)
|
super().invoke(ctx)
|
||||||
sub_ctx = cmd.make_context(cmd_name, args, parent=ctx)
|
sub_ctx = cmd.make_context(cmd_name, args, parent=ctx)
|
||||||
with sub_ctx:
|
with sub_ctx:
|
||||||
return _process_result(sub_ctx.command.invoke(sub_ctx))
|
return _process_result(sub_ctx.command.invoke(sub_ctx))
|
||||||
|
@ -1265,7 +1663,7 @@ class MultiCommand(Command):
|
||||||
# but nothing else.
|
# but nothing else.
|
||||||
with ctx:
|
with ctx:
|
||||||
ctx.invoked_subcommand = "*" if args else None
|
ctx.invoked_subcommand = "*" if args else None
|
||||||
Command.invoke(self, ctx)
|
super().invoke(ctx)
|
||||||
|
|
||||||
# Otherwise we make every single context and invoke them in a
|
# Otherwise we make every single context and invoke them in a
|
||||||
# chain. In that case the return value to the result processor
|
# chain. In that case the return value to the result processor
|
||||||
|
@ -1273,6 +1671,7 @@ class MultiCommand(Command):
|
||||||
contexts = []
|
contexts = []
|
||||||
while args:
|
while args:
|
||||||
cmd_name, cmd, args = self.resolve_command(ctx, args)
|
cmd_name, cmd, args = self.resolve_command(ctx, args)
|
||||||
|
assert cmd is not None
|
||||||
sub_ctx = cmd.make_context(
|
sub_ctx = cmd.make_context(
|
||||||
cmd_name,
|
cmd_name,
|
||||||
args,
|
args,
|
||||||
|
@ -1289,7 +1688,9 @@ class MultiCommand(Command):
|
||||||
rv.append(sub_ctx.command.invoke(sub_ctx))
|
rv.append(sub_ctx.command.invoke(sub_ctx))
|
||||||
return _process_result(rv)
|
return _process_result(rv)
|
||||||
|
|
||||||
def resolve_command(self, ctx, args):
|
def resolve_command(
|
||||||
|
self, ctx: Context, args: t.List[str]
|
||||||
|
) -> t.Tuple[t.Optional[str], t.Optional[Command], t.List[str]]:
|
||||||
cmd_name = make_str(args[0])
|
cmd_name = make_str(args[0])
|
||||||
original_cmd_name = cmd_name
|
original_cmd_name = cmd_name
|
||||||
|
|
||||||
|
@ -1311,36 +1712,94 @@ class MultiCommand(Command):
|
||||||
if cmd is None and not ctx.resilient_parsing:
|
if cmd is None and not ctx.resilient_parsing:
|
||||||
if split_opt(cmd_name)[0]:
|
if split_opt(cmd_name)[0]:
|
||||||
self.parse_args(ctx, ctx.args)
|
self.parse_args(ctx, ctx.args)
|
||||||
ctx.fail("No such command '{}'.".format(original_cmd_name))
|
ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name))
|
||||||
|
return cmd_name if cmd else None, cmd, args[1:]
|
||||||
|
|
||||||
return cmd_name, cmd, args[1:]
|
def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]:
|
||||||
|
|
||||||
def get_command(self, ctx, cmd_name):
|
|
||||||
"""Given a context and a command name, this returns a
|
"""Given a context and a command name, this returns a
|
||||||
:class:`Command` object if it exists or returns `None`.
|
:class:`Command` object if it exists or returns `None`.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError
|
||||||
|
|
||||||
def list_commands(self, ctx):
|
def list_commands(self, ctx: Context) -> t.List[str]:
|
||||||
"""Returns a list of subcommand names in the order they should
|
"""Returns a list of subcommand names in the order they should
|
||||||
appear.
|
appear.
|
||||||
"""
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]:
|
||||||
|
"""Return a list of completions for the incomplete value. Looks
|
||||||
|
at the names of options, subcommands, and chained
|
||||||
|
multi-commands.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for this command.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
from click.shell_completion import CompletionItem
|
||||||
|
|
||||||
|
results = [
|
||||||
|
CompletionItem(name, help=command.get_short_help_str())
|
||||||
|
for name, command in _complete_visible_commands(ctx, incomplete)
|
||||||
|
]
|
||||||
|
results.extend(super().shell_complete(ctx, incomplete))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
class Group(MultiCommand):
|
class Group(MultiCommand):
|
||||||
"""A group allows a command to have subcommands attached. This is the
|
"""A group allows a command to have subcommands attached. This is
|
||||||
most common way to implement nesting in Click.
|
the most common way to implement nesting in Click.
|
||||||
|
|
||||||
:param commands: a dictionary of commands.
|
:param name: The name of the group command.
|
||||||
|
:param commands: A dict mapping names to :class:`Command` objects.
|
||||||
|
Can also be a list of :class:`Command`, which will use
|
||||||
|
:attr:`Command.name` to create the dict.
|
||||||
|
:param attrs: Other command arguments described in
|
||||||
|
:class:`MultiCommand`, :class:`Command`, and
|
||||||
|
:class:`BaseCommand`.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
The ``commmands`` argument can be a list of command objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name=None, commands=None, **attrs):
|
#: If set, this is used by the group's :meth:`command` decorator
|
||||||
MultiCommand.__init__(self, name, **attrs)
|
#: as the default :class:`Command` class. This is useful to make all
|
||||||
#: the registered subcommands by their exported names.
|
#: subcommands use a custom command class.
|
||||||
self.commands = commands or {}
|
#:
|
||||||
|
#: .. versionadded:: 8.0
|
||||||
|
command_class: t.Optional[t.Type[Command]] = None
|
||||||
|
|
||||||
def add_command(self, cmd, name=None):
|
#: If set, this is used by the group's :meth:`group` decorator
|
||||||
|
#: as the default :class:`Group` class. This is useful to make all
|
||||||
|
#: subgroups use a custom group class.
|
||||||
|
#:
|
||||||
|
#: If set to the special value :class:`type` (literally
|
||||||
|
#: ``group_class = type``), this group's class will be used as the
|
||||||
|
#: default class. This makes a custom group class continue to make
|
||||||
|
#: custom groups.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 8.0
|
||||||
|
group_class: t.Optional[t.Union[t.Type["Group"], t.Type[type]]] = None
|
||||||
|
# Literal[type] isn't valid, so use Type[type]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
commands: t.Optional[t.Union[t.Dict[str, Command], t.Sequence[Command]]] = None,
|
||||||
|
**attrs: t.Any,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(name, **attrs)
|
||||||
|
|
||||||
|
if commands is None:
|
||||||
|
commands = {}
|
||||||
|
elif isinstance(commands, abc.Sequence):
|
||||||
|
commands = {c.name: c for c in commands if c.name is not None}
|
||||||
|
|
||||||
|
#: The registered subcommands by their exported names.
|
||||||
|
self.commands: t.Dict[str, Command] = commands
|
||||||
|
|
||||||
|
def add_command(self, cmd: Command, name: t.Optional[str] = None) -> None:
|
||||||
"""Registers another :class:`Command` with this group. If the name
|
"""Registers another :class:`Command` with this group. If the name
|
||||||
is not provided, the name of the command is used.
|
is not provided, the name of the command is used.
|
||||||
"""
|
"""
|
||||||
|
@ -1350,40 +1809,65 @@ class Group(MultiCommand):
|
||||||
_check_multicommand(self, name, cmd, register=True)
|
_check_multicommand(self, name, cmd, register=True)
|
||||||
self.commands[name] = cmd
|
self.commands[name] = cmd
|
||||||
|
|
||||||
def command(self, *args, **kwargs):
|
def command(
|
||||||
|
self, *args: t.Any, **kwargs: t.Any
|
||||||
|
) -> t.Callable[[t.Callable[..., t.Any]], Command]:
|
||||||
"""A shortcut decorator for declaring and attaching a command to
|
"""A shortcut decorator for declaring and attaching a command to
|
||||||
the group. This takes the same arguments as :func:`command` but
|
the group. This takes the same arguments as :func:`command` and
|
||||||
immediately registers the created command with this instance by
|
immediately registers the created command with this group by
|
||||||
calling into :meth:`add_command`.
|
calling :meth:`add_command`.
|
||||||
|
|
||||||
|
To customize the command class used, set the
|
||||||
|
:attr:`command_class` attribute.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the :attr:`command_class` attribute.
|
||||||
"""
|
"""
|
||||||
from .decorators import command
|
from .decorators import command
|
||||||
|
|
||||||
def decorator(f):
|
if self.command_class is not None and "cls" not in kwargs:
|
||||||
|
kwargs["cls"] = self.command_class
|
||||||
|
|
||||||
|
def decorator(f: t.Callable[..., t.Any]) -> Command:
|
||||||
cmd = command(*args, **kwargs)(f)
|
cmd = command(*args, **kwargs)(f)
|
||||||
self.add_command(cmd)
|
self.add_command(cmd)
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def group(self, *args, **kwargs):
|
def group(
|
||||||
|
self, *args: t.Any, **kwargs: t.Any
|
||||||
|
) -> t.Callable[[t.Callable[..., t.Any]], "Group"]:
|
||||||
"""A shortcut decorator for declaring and attaching a group to
|
"""A shortcut decorator for declaring and attaching a group to
|
||||||
the group. This takes the same arguments as :func:`group` but
|
the group. This takes the same arguments as :func:`group` and
|
||||||
immediately registers the created command with this instance by
|
immediately registers the created group with this group by
|
||||||
calling into :meth:`add_command`.
|
calling :meth:`add_command`.
|
||||||
|
|
||||||
|
To customize the group class used, set the :attr:`group_class`
|
||||||
|
attribute.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the :attr:`group_class` attribute.
|
||||||
"""
|
"""
|
||||||
from .decorators import group
|
from .decorators import group
|
||||||
|
|
||||||
def decorator(f):
|
if self.group_class is not None and "cls" not in kwargs:
|
||||||
|
if self.group_class is type:
|
||||||
|
kwargs["cls"] = type(self)
|
||||||
|
else:
|
||||||
|
kwargs["cls"] = self.group_class
|
||||||
|
|
||||||
|
def decorator(f: t.Callable[..., t.Any]) -> "Group":
|
||||||
cmd = group(*args, **kwargs)(f)
|
cmd = group(*args, **kwargs)(f)
|
||||||
self.add_command(cmd)
|
self.add_command(cmd)
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def get_command(self, ctx, cmd_name):
|
def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]:
|
||||||
return self.commands.get(cmd_name)
|
return self.commands.get(cmd_name)
|
||||||
|
|
||||||
def list_commands(self, ctx):
|
def list_commands(self, ctx: Context) -> t.List[str]:
|
||||||
return sorted(self.commands)
|
return sorted(self.commands)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1394,31 +1878,52 @@ class CommandCollection(MultiCommand):
|
||||||
provides all the commands for each of them.
|
provides all the commands for each of them.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name=None, sources=None, **attrs):
|
def __init__(
|
||||||
MultiCommand.__init__(self, name, **attrs)
|
self,
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
sources: t.Optional[t.List[MultiCommand]] = None,
|
||||||
|
**attrs: t.Any,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(name, **attrs)
|
||||||
#: The list of registered multi commands.
|
#: The list of registered multi commands.
|
||||||
self.sources = sources or []
|
self.sources: t.List[MultiCommand] = sources or []
|
||||||
|
|
||||||
def add_source(self, multi_cmd):
|
def add_source(self, multi_cmd: MultiCommand) -> None:
|
||||||
"""Adds a new multi command to the chain dispatcher."""
|
"""Adds a new multi command to the chain dispatcher."""
|
||||||
self.sources.append(multi_cmd)
|
self.sources.append(multi_cmd)
|
||||||
|
|
||||||
def get_command(self, ctx, cmd_name):
|
def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]:
|
||||||
for source in self.sources:
|
for source in self.sources:
|
||||||
rv = source.get_command(ctx, cmd_name)
|
rv = source.get_command(ctx, cmd_name)
|
||||||
|
|
||||||
if rv is not None:
|
if rv is not None:
|
||||||
if self.chain:
|
if self.chain:
|
||||||
_check_multicommand(self, cmd_name, rv)
|
_check_multicommand(self, cmd_name, rv)
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def list_commands(self, ctx):
|
return None
|
||||||
rv = set()
|
|
||||||
|
def list_commands(self, ctx: Context) -> t.List[str]:
|
||||||
|
rv: t.Set[str] = set()
|
||||||
|
|
||||||
for source in self.sources:
|
for source in self.sources:
|
||||||
rv.update(source.list_commands(ctx))
|
rv.update(source.list_commands(ctx))
|
||||||
|
|
||||||
return sorted(rv)
|
return sorted(rv)
|
||||||
|
|
||||||
|
|
||||||
class Parameter(object):
|
def _check_iter(value: t.Any) -> t.Iterator[t.Any]:
|
||||||
|
"""Check if the value is iterable but not a string. Raises a type
|
||||||
|
error, or return an iterator over the value.
|
||||||
|
"""
|
||||||
|
if isinstance(value, str):
|
||||||
|
raise TypeError
|
||||||
|
|
||||||
|
return iter(value)
|
||||||
|
|
||||||
|
|
||||||
|
class Parameter:
|
||||||
r"""A parameter to a command comes in two versions: they are either
|
r"""A parameter to a command comes in two versions: they are either
|
||||||
:class:`Option`\s or :class:`Argument`\s. Other subclasses are currently
|
:class:`Option`\s or :class:`Argument`\s. Other subclasses are currently
|
||||||
not supported by design as some of the internals for parsing are
|
not supported by design as some of the internals for parsing are
|
||||||
|
@ -1436,13 +1941,15 @@ class Parameter(object):
|
||||||
:param default: the default value if omitted. This can also be a callable,
|
:param default: the default value if omitted. This can also be a callable,
|
||||||
in which case it's invoked when the default is needed
|
in which case it's invoked when the default is needed
|
||||||
without any arguments.
|
without any arguments.
|
||||||
:param callback: a callback that should be executed after the parameter
|
:param callback: A function to further process or validate the value
|
||||||
was matched. This is called as ``fn(ctx, param,
|
after type conversion. It is called as ``f(ctx, param, value)``
|
||||||
value)`` and needs to return the value.
|
and must return the value. It is called for all sources,
|
||||||
|
including prompts.
|
||||||
:param nargs: the number of arguments to match. If not ``1`` the return
|
:param nargs: the number of arguments to match. If not ``1`` the return
|
||||||
value is a tuple instead of single value. The default for
|
value is a tuple instead of single value. The default for
|
||||||
nargs is ``1`` (except if the type is a tuple, then it's
|
nargs is ``1`` (except if the type is a tuple, then it's
|
||||||
the arity of the tuple).
|
the arity of the tuple). If ``nargs=-1``, all remaining
|
||||||
|
parameters are collected.
|
||||||
:param metavar: how the value is represented in the help page.
|
:param metavar: how the value is represented in the help page.
|
||||||
:param expose_value: if this is `True` then the value is passed onwards
|
:param expose_value: if this is `True` then the value is passed onwards
|
||||||
to the command callback and stored on the context,
|
to the command callback and stored on the context,
|
||||||
|
@ -1452,6 +1959,32 @@ class Parameter(object):
|
||||||
order of processing.
|
order of processing.
|
||||||
:param envvar: a string or list of strings that are environment variables
|
:param envvar: a string or list of strings that are environment variables
|
||||||
that should be checked.
|
that should be checked.
|
||||||
|
:param shell_complete: A function that returns custom shell
|
||||||
|
completions. Used instead of the param's type completion if
|
||||||
|
given. Takes ``ctx, param, incomplete`` and must return a list
|
||||||
|
of :class:`~click.shell_completion.CompletionItem` or a list of
|
||||||
|
strings.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
``process_value`` validates required parameters and bounded
|
||||||
|
``nargs``, and invokes the parameter callback before returning
|
||||||
|
the value. This allows the callback to validate prompts.
|
||||||
|
``full_process_value`` is removed.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
``autocompletion`` is renamed to ``shell_complete`` and has new
|
||||||
|
semantics described above. The old name is deprecated and will
|
||||||
|
be removed in 8.1, until then it will be wrapped to match the
|
||||||
|
new requirements.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
For ``multiple=True, nargs>1``, the default must be a list of
|
||||||
|
tuples.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Setting a default is no longer required for ``nargs>1``, it will
|
||||||
|
default to ``None``. ``multiple=True`` or ``nargs=-1`` will
|
||||||
|
default to ``()``.
|
||||||
|
|
||||||
.. versionchanged:: 7.1
|
.. versionchanged:: 7.1
|
||||||
Empty environment variables are ignored rather than taking the
|
Empty environment variables are ignored rather than taking the
|
||||||
|
@ -1463,27 +1996,38 @@ class Parameter(object):
|
||||||
parameter. The old callback format will still work, but it will
|
parameter. The old callback format will still work, but it will
|
||||||
raise a warning to give you a chance to migrate the code easier.
|
raise a warning to give you a chance to migrate the code easier.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
param_type_name = "parameter"
|
param_type_name = "parameter"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
param_decls=None,
|
param_decls: t.Optional[t.Sequence[str]] = None,
|
||||||
type=None,
|
type: t.Optional[t.Union[types.ParamType, t.Any]] = None,
|
||||||
required=False,
|
required: bool = False,
|
||||||
default=None,
|
default: t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]] = None,
|
||||||
callback=None,
|
callback: t.Optional[t.Callable[[Context, "Parameter", t.Any], t.Any]] = None,
|
||||||
nargs=None,
|
nargs: t.Optional[int] = None,
|
||||||
metavar=None,
|
multiple: bool = False,
|
||||||
expose_value=True,
|
metavar: t.Optional[str] = None,
|
||||||
is_eager=False,
|
expose_value: bool = True,
|
||||||
envvar=None,
|
is_eager: bool = False,
|
||||||
autocompletion=None,
|
envvar: t.Optional[t.Union[str, t.Sequence[str]]] = None,
|
||||||
):
|
shell_complete: t.Optional[
|
||||||
|
t.Callable[
|
||||||
|
[Context, "Parameter", str],
|
||||||
|
t.Union[t.List["CompletionItem"], t.List[str]],
|
||||||
|
]
|
||||||
|
] = None,
|
||||||
|
autocompletion: t.Optional[
|
||||||
|
t.Callable[
|
||||||
|
[Context, t.List[str], str], t.List[t.Union[t.Tuple[str, str], str]]
|
||||||
|
]
|
||||||
|
] = None,
|
||||||
|
) -> None:
|
||||||
self.name, self.opts, self.secondary_opts = self._parse_decls(
|
self.name, self.opts, self.secondary_opts = self._parse_decls(
|
||||||
param_decls or (), expose_value
|
param_decls or (), expose_value
|
||||||
)
|
)
|
||||||
|
self.type = types.convert_type(type, default)
|
||||||
self.type = convert_type(type, default)
|
|
||||||
|
|
||||||
# Default nargs to what the type tells us if we have that
|
# Default nargs to what the type tells us if we have that
|
||||||
# information available.
|
# information available.
|
||||||
|
@ -1496,158 +2040,355 @@ class Parameter(object):
|
||||||
self.required = required
|
self.required = required
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.nargs = nargs
|
self.nargs = nargs
|
||||||
self.multiple = False
|
self.multiple = multiple
|
||||||
self.expose_value = expose_value
|
self.expose_value = expose_value
|
||||||
self.default = default
|
self.default = default
|
||||||
self.is_eager = is_eager
|
self.is_eager = is_eager
|
||||||
self.metavar = metavar
|
self.metavar = metavar
|
||||||
self.envvar = envvar
|
self.envvar = envvar
|
||||||
self.autocompletion = autocompletion
|
|
||||||
|
|
||||||
def __repr__(self):
|
if autocompletion is not None:
|
||||||
return "<{} {}>".format(self.__class__.__name__, self.name)
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'autocompletion' is renamed to 'shell_complete'. The old name is"
|
||||||
|
" deprecated and will be removed in Click 8.1. See the docs about"
|
||||||
|
" 'Parameter' for information about new behavior.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
def shell_complete(
|
||||||
|
ctx: Context, param: "Parameter", incomplete: str
|
||||||
|
) -> t.List["CompletionItem"]:
|
||||||
|
from click.shell_completion import CompletionItem
|
||||||
|
|
||||||
|
out = []
|
||||||
|
|
||||||
|
for c in autocompletion(ctx, [], incomplete): # type: ignore
|
||||||
|
if isinstance(c, tuple):
|
||||||
|
c = CompletionItem(c[0], help=c[1])
|
||||||
|
elif isinstance(c, str):
|
||||||
|
c = CompletionItem(c)
|
||||||
|
|
||||||
|
if c.value.startswith(incomplete):
|
||||||
|
out.append(c)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
self._custom_shell_complete = shell_complete
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
if self.type.is_composite and nargs != self.type.arity:
|
||||||
|
raise ValueError(
|
||||||
|
f"'nargs' must be {self.type.arity} (or None) for"
|
||||||
|
f" type {self.type!r}, but it was {nargs}."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip no default or callable default.
|
||||||
|
check_default = default if not callable(default) else None
|
||||||
|
|
||||||
|
if check_default is not None:
|
||||||
|
if multiple:
|
||||||
|
try:
|
||||||
|
# Only check the first value against nargs.
|
||||||
|
check_default = next(_check_iter(check_default), None)
|
||||||
|
except TypeError:
|
||||||
|
raise ValueError(
|
||||||
|
"'default' must be a list when 'multiple' is true."
|
||||||
|
) from None
|
||||||
|
|
||||||
|
# Can be None for multiple with empty default.
|
||||||
|
if nargs != 1 and check_default is not None:
|
||||||
|
try:
|
||||||
|
_check_iter(check_default)
|
||||||
|
except TypeError:
|
||||||
|
if multiple:
|
||||||
|
message = (
|
||||||
|
"'default' must be a list of lists when 'multiple' is"
|
||||||
|
" true and 'nargs' != 1."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message = "'default' must be a list when 'nargs' != 1."
|
||||||
|
|
||||||
|
raise ValueError(message) from None
|
||||||
|
|
||||||
|
if nargs > 1 and len(check_default) != nargs:
|
||||||
|
subject = "item length" if multiple else "length"
|
||||||
|
raise ValueError(
|
||||||
|
f"'default' {subject} must match nargs={nargs}."
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||||
|
"""Gather information that could be useful for a tool generating
|
||||||
|
user-facing documentation.
|
||||||
|
|
||||||
|
Use :meth:`click.Context.to_info_dict` to traverse the entire
|
||||||
|
CLI structure.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"param_type_name": self.param_type_name,
|
||||||
|
"opts": self.opts,
|
||||||
|
"secondary_opts": self.secondary_opts,
|
||||||
|
"type": self.type.to_info_dict(),
|
||||||
|
"required": self.required,
|
||||||
|
"nargs": self.nargs,
|
||||||
|
"multiple": self.multiple,
|
||||||
|
"default": self.default,
|
||||||
|
"envvar": self.envvar,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__} {self.name}>"
|
||||||
|
|
||||||
|
def _parse_decls(
|
||||||
|
self, decls: t.Sequence[str], expose_value: bool
|
||||||
|
) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def human_readable_name(self):
|
def human_readable_name(self) -> str:
|
||||||
"""Returns the human readable name of this parameter. This is the
|
"""Returns the human readable name of this parameter. This is the
|
||||||
same as the name for options, but the metavar for arguments.
|
same as the name for options, but the metavar for arguments.
|
||||||
"""
|
"""
|
||||||
return self.name
|
return self.name # type: ignore
|
||||||
|
|
||||||
def make_metavar(self):
|
def make_metavar(self) -> str:
|
||||||
if self.metavar is not None:
|
if self.metavar is not None:
|
||||||
return self.metavar
|
return self.metavar
|
||||||
|
|
||||||
metavar = self.type.get_metavar(self)
|
metavar = self.type.get_metavar(self)
|
||||||
|
|
||||||
if metavar is None:
|
if metavar is None:
|
||||||
metavar = self.type.name.upper()
|
metavar = self.type.name.upper()
|
||||||
|
|
||||||
if self.nargs != 1:
|
if self.nargs != 1:
|
||||||
metavar += "..."
|
metavar += "..."
|
||||||
|
|
||||||
return metavar
|
return metavar
|
||||||
|
|
||||||
def get_default(self, ctx):
|
@typing.overload
|
||||||
"""Given a context variable this calculates the default value."""
|
def get_default(
|
||||||
# Otherwise go with the regular default.
|
self, ctx: Context, call: "te.Literal[True]" = True
|
||||||
if callable(self.default):
|
) -> t.Optional[t.Any]:
|
||||||
rv = self.default()
|
...
|
||||||
else:
|
|
||||||
rv = self.default
|
|
||||||
return self.type_cast_value(ctx, rv)
|
|
||||||
|
|
||||||
def add_to_parser(self, parser, ctx):
|
@typing.overload
|
||||||
pass
|
def get_default(
|
||||||
|
self, ctx: Context, call: bool = ...
|
||||||
|
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def get_default(
|
||||||
|
self, ctx: Context, call: bool = True
|
||||||
|
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
|
||||||
|
"""Get the default for the parameter. Tries
|
||||||
|
:meth:`Context.lookup_default` first, then the local default.
|
||||||
|
|
||||||
|
:param ctx: Current context.
|
||||||
|
:param call: If the default is a callable, call it. Disable to
|
||||||
|
return the callable instead.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0.2
|
||||||
|
Type casting is no longer performed when getting a default.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0.1
|
||||||
|
Type casting can fail in resilient parsing mode. Invalid
|
||||||
|
defaults will not prevent showing help text.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Looks at ``ctx.default_map`` first.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the ``call`` parameter.
|
||||||
|
"""
|
||||||
|
value = ctx.lookup_default(self.name, call=False) # type: ignore
|
||||||
|
|
||||||
def consume_value(self, ctx, opts):
|
|
||||||
value = opts.get(self.name)
|
|
||||||
if value is None:
|
if value is None:
|
||||||
value = self.value_from_envvar(ctx)
|
value = self.default
|
||||||
if value is None:
|
|
||||||
value = ctx.lookup_default(self.name)
|
if call and callable(value):
|
||||||
|
value = value()
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def type_cast_value(self, ctx, value):
|
def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
|
||||||
"""Given a value this runs it properly through the type system.
|
raise NotImplementedError()
|
||||||
This automatically handles things like `nargs` and `multiple` as
|
|
||||||
well as composite types.
|
def consume_value(
|
||||||
|
self, ctx: Context, opts: t.Mapping[str, t.Any]
|
||||||
|
) -> t.Tuple[t.Any, ParameterSource]:
|
||||||
|
value = opts.get(self.name) # type: ignore
|
||||||
|
source = ParameterSource.COMMANDLINE
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
value = self.value_from_envvar(ctx)
|
||||||
|
source = ParameterSource.ENVIRONMENT
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
value = ctx.lookup_default(self.name) # type: ignore
|
||||||
|
source = ParameterSource.DEFAULT_MAP
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
value = self.get_default(ctx)
|
||||||
|
source = ParameterSource.DEFAULT
|
||||||
|
|
||||||
|
return value, source
|
||||||
|
|
||||||
|
def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any:
|
||||||
|
"""Convert and validate a value against the option's
|
||||||
|
:attr:`type`, :attr:`multiple`, and :attr:`nargs`.
|
||||||
"""
|
"""
|
||||||
if self.type.is_composite:
|
if value is None:
|
||||||
if self.nargs <= 1:
|
return () if self.multiple or self.nargs == -1 else None
|
||||||
raise TypeError(
|
|
||||||
"Attempted to invoke composite type but nargs has"
|
|
||||||
" been set to {}. This is not supported; nargs"
|
|
||||||
" needs to be set to a fixed value > 1.".format(self.nargs)
|
|
||||||
)
|
|
||||||
if self.multiple:
|
|
||||||
return tuple(self.type(x or (), self, ctx) for x in value or ())
|
|
||||||
return self.type(value or (), self, ctx)
|
|
||||||
|
|
||||||
def _convert(value, level):
|
def check_iter(value: t.Any) -> t.Iterator:
|
||||||
if level == 0:
|
try:
|
||||||
return self.type(value, self, ctx)
|
return _check_iter(value)
|
||||||
return tuple(_convert(x, level - 1) for x in value or ())
|
except TypeError:
|
||||||
|
# This should only happen when passing in args manually,
|
||||||
|
# the parser should construct an iterable when parsing
|
||||||
|
# the command line.
|
||||||
|
raise BadParameter(
|
||||||
|
_("Value must be an iterable."), ctx=ctx, param=self
|
||||||
|
) from None
|
||||||
|
|
||||||
return _convert(value, (self.nargs != 1) + bool(self.multiple))
|
if self.nargs == 1 or self.type.is_composite:
|
||||||
|
convert: t.Callable[[t.Any], t.Any] = partial(
|
||||||
|
self.type, param=self, ctx=ctx
|
||||||
|
)
|
||||||
|
elif self.nargs == -1:
|
||||||
|
|
||||||
def process_value(self, ctx, value):
|
def convert(value: t.Any) -> t.Tuple:
|
||||||
"""Given a value and context this runs the logic to convert the
|
return tuple(self.type(x, self, ctx) for x in check_iter(value))
|
||||||
value as necessary.
|
|
||||||
"""
|
|
||||||
# If the value we were given is None we do nothing. This way
|
|
||||||
# code that calls this can easily figure out if something was
|
|
||||||
# not provided. Otherwise it would be converted into an empty
|
|
||||||
# tuple for multiple invocations which is inconvenient.
|
|
||||||
if value is not None:
|
|
||||||
return self.type_cast_value(ctx, value)
|
|
||||||
|
|
||||||
def value_is_missing(self, value):
|
else: # nargs > 1
|
||||||
|
|
||||||
|
def convert(value: t.Any) -> t.Tuple:
|
||||||
|
value = tuple(check_iter(value))
|
||||||
|
|
||||||
|
if len(value) != self.nargs:
|
||||||
|
raise BadParameter(
|
||||||
|
ngettext(
|
||||||
|
"Takes {nargs} values but 1 was given.",
|
||||||
|
"Takes {nargs} values but {len} were given.",
|
||||||
|
len(value),
|
||||||
|
).format(nargs=self.nargs, len=len(value)),
|
||||||
|
ctx=ctx,
|
||||||
|
param=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
return tuple(self.type(x, self, ctx) for x in value)
|
||||||
|
|
||||||
|
if self.multiple:
|
||||||
|
return tuple(convert(x) for x in check_iter(value))
|
||||||
|
|
||||||
|
return convert(value)
|
||||||
|
|
||||||
|
def value_is_missing(self, value: t.Any) -> bool:
|
||||||
if value is None:
|
if value is None:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if (self.nargs != 1 or self.multiple) and value == ():
|
if (self.nargs != 1 or self.multiple) and value == ():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def full_process_value(self, ctx, value):
|
def process_value(self, ctx: Context, value: t.Any) -> t.Any:
|
||||||
value = self.process_value(ctx, value)
|
value = self.type_cast_value(ctx, value)
|
||||||
|
|
||||||
if value is None and not ctx.resilient_parsing:
|
|
||||||
value = self.get_default(ctx)
|
|
||||||
|
|
||||||
if self.required and self.value_is_missing(value):
|
if self.required and self.value_is_missing(value):
|
||||||
raise MissingParameter(ctx=ctx, param=self)
|
raise MissingParameter(ctx=ctx, param=self)
|
||||||
|
|
||||||
|
if self.callback is not None:
|
||||||
|
value = self.callback(ctx, self, value)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def resolve_envvar_value(self, ctx):
|
def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]:
|
||||||
if self.envvar is None:
|
if self.envvar is None:
|
||||||
return
|
return None
|
||||||
if isinstance(self.envvar, (tuple, list)):
|
|
||||||
for envvar in self.envvar:
|
if isinstance(self.envvar, str):
|
||||||
rv = os.environ.get(envvar)
|
|
||||||
if rv is not None:
|
|
||||||
return rv
|
|
||||||
else:
|
|
||||||
rv = os.environ.get(self.envvar)
|
rv = os.environ.get(self.envvar)
|
||||||
|
|
||||||
if rv != "":
|
if rv:
|
||||||
return rv
|
return rv
|
||||||
|
else:
|
||||||
|
for envvar in self.envvar:
|
||||||
|
rv = os.environ.get(envvar)
|
||||||
|
|
||||||
|
if rv:
|
||||||
|
return rv
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]:
|
||||||
|
rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx)
|
||||||
|
|
||||||
def value_from_envvar(self, ctx):
|
|
||||||
rv = self.resolve_envvar_value(ctx)
|
|
||||||
if rv is not None and self.nargs != 1:
|
if rv is not None and self.nargs != 1:
|
||||||
rv = self.type.split_envvar_value(rv)
|
rv = self.type.split_envvar_value(rv)
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def handle_parse_result(self, ctx, opts, args):
|
def handle_parse_result(
|
||||||
|
self, ctx: Context, opts: t.Mapping[str, t.Any], args: t.List[str]
|
||||||
|
) -> t.Tuple[t.Any, t.List[str]]:
|
||||||
with augment_usage_errors(ctx, param=self):
|
with augment_usage_errors(ctx, param=self):
|
||||||
value = self.consume_value(ctx, opts)
|
value, source = self.consume_value(ctx, opts)
|
||||||
|
ctx.set_parameter_source(self.name, source) # type: ignore
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value = self.full_process_value(ctx, value)
|
value = self.process_value(ctx, value)
|
||||||
except Exception:
|
except Exception:
|
||||||
if not ctx.resilient_parsing:
|
if not ctx.resilient_parsing:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
value = None
|
value = None
|
||||||
if self.callback is not None:
|
|
||||||
try:
|
|
||||||
value = invoke_param_callback(self.callback, ctx, self, value)
|
|
||||||
except Exception:
|
|
||||||
if not ctx.resilient_parsing:
|
|
||||||
raise
|
|
||||||
|
|
||||||
if self.expose_value:
|
if self.expose_value:
|
||||||
ctx.params[self.name] = value
|
ctx.params[self.name] = value # type: ignore
|
||||||
|
|
||||||
return value, args
|
return value, args
|
||||||
|
|
||||||
def get_help_record(self, ctx):
|
def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_usage_pieces(self, ctx):
|
def get_usage_pieces(self, ctx: Context) -> t.List[str]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_error_hint(self, ctx):
|
def get_error_hint(self, ctx: Context) -> str:
|
||||||
"""Get a stringified version of the param for use in error messages to
|
"""Get a stringified version of the param for use in error messages to
|
||||||
indicate which param caused the error.
|
indicate which param caused the error.
|
||||||
"""
|
"""
|
||||||
hint_list = self.opts or [self.human_readable_name]
|
hint_list = self.opts or [self.human_readable_name]
|
||||||
return " / ".join(repr(x) for x in hint_list)
|
return " / ".join(f"'{x}'" for x in hint_list)
|
||||||
|
|
||||||
|
def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]:
|
||||||
|
"""Return a list of completions for the incomplete value. If a
|
||||||
|
``shell_complete`` function was given during init, it is used.
|
||||||
|
Otherwise, the :attr:`type`
|
||||||
|
:meth:`~click.types.ParamType.shell_complete` function is used.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for this command.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
if self._custom_shell_complete is not None:
|
||||||
|
results = self._custom_shell_complete(ctx, self, incomplete)
|
||||||
|
|
||||||
|
if results and isinstance(results[0], str):
|
||||||
|
from click.shell_completion import CompletionItem
|
||||||
|
|
||||||
|
results = [CompletionItem(c) for c in results]
|
||||||
|
|
||||||
|
return t.cast(t.List["CompletionItem"], results)
|
||||||
|
|
||||||
|
return self.type.shell_complete(ctx, self, incomplete)
|
||||||
|
|
||||||
|
|
||||||
class Option(Parameter):
|
class Option(Parameter):
|
||||||
|
@ -1666,8 +2407,12 @@ class Option(Parameter):
|
||||||
:param prompt: if set to `True` or a non empty string then the user will be
|
:param prompt: if set to `True` or a non empty string then the user will be
|
||||||
prompted for input. If set to `True` the prompt will be the
|
prompted for input. If set to `True` the prompt will be the
|
||||||
option name capitalized.
|
option name capitalized.
|
||||||
:param confirmation_prompt: if set then the value will need to be confirmed
|
:param confirmation_prompt: Prompt a second time to confirm the
|
||||||
if it was prompted for.
|
value if it was prompted for. Can be set to a string instead of
|
||||||
|
``True`` to customize the message.
|
||||||
|
:param prompt_required: If set to ``False``, the user will be
|
||||||
|
prompted for input only when the option was specified as a flag
|
||||||
|
without a value.
|
||||||
:param hide_input: if this is `True` then the input on the prompt will be
|
:param hide_input: if this is `True` then the input on the prompt will be
|
||||||
hidden from the user. This is useful for password
|
hidden from the user. This is useful for password
|
||||||
input.
|
input.
|
||||||
|
@ -1687,106 +2432,146 @@ class Option(Parameter):
|
||||||
context.
|
context.
|
||||||
:param help: the help string.
|
:param help: the help string.
|
||||||
:param hidden: hide this option from help outputs.
|
:param hidden: hide this option from help outputs.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0.1
|
||||||
|
``type`` is detected from ``flag_value`` if given.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
param_type_name = "option"
|
param_type_name = "option"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
param_decls=None,
|
param_decls: t.Optional[t.Sequence[str]] = None,
|
||||||
show_default=False,
|
show_default: t.Union[bool, str] = False,
|
||||||
prompt=False,
|
prompt: t.Union[bool, str] = False,
|
||||||
confirmation_prompt=False,
|
confirmation_prompt: t.Union[bool, str] = False,
|
||||||
hide_input=False,
|
prompt_required: bool = True,
|
||||||
is_flag=None,
|
hide_input: bool = False,
|
||||||
flag_value=None,
|
is_flag: t.Optional[bool] = None,
|
||||||
multiple=False,
|
flag_value: t.Optional[t.Any] = None,
|
||||||
count=False,
|
multiple: bool = False,
|
||||||
allow_from_autoenv=True,
|
count: bool = False,
|
||||||
type=None,
|
allow_from_autoenv: bool = True,
|
||||||
help=None,
|
type: t.Optional[t.Union[types.ParamType, t.Any]] = None,
|
||||||
hidden=False,
|
help: t.Optional[str] = None,
|
||||||
show_choices=True,
|
hidden: bool = False,
|
||||||
show_envvar=False,
|
show_choices: bool = True,
|
||||||
**attrs
|
show_envvar: bool = False,
|
||||||
):
|
**attrs: t.Any,
|
||||||
default_is_missing = attrs.get("default", _missing) is _missing
|
) -> None:
|
||||||
Parameter.__init__(self, param_decls, type=type, **attrs)
|
default_is_missing = "default" not in attrs
|
||||||
|
super().__init__(param_decls, type=type, multiple=multiple, **attrs)
|
||||||
|
|
||||||
if prompt is True:
|
if prompt is True:
|
||||||
prompt_text = self.name.replace("_", " ").capitalize()
|
if self.name is None:
|
||||||
|
raise TypeError("'name' is required with 'prompt=True'.")
|
||||||
|
|
||||||
|
prompt_text: t.Optional[str] = self.name.replace("_", " ").capitalize()
|
||||||
elif prompt is False:
|
elif prompt is False:
|
||||||
prompt_text = None
|
prompt_text = None
|
||||||
else:
|
else:
|
||||||
prompt_text = prompt
|
prompt_text = t.cast(str, prompt)
|
||||||
|
|
||||||
self.prompt = prompt_text
|
self.prompt = prompt_text
|
||||||
self.confirmation_prompt = confirmation_prompt
|
self.confirmation_prompt = confirmation_prompt
|
||||||
|
self.prompt_required = prompt_required
|
||||||
self.hide_input = hide_input
|
self.hide_input = hide_input
|
||||||
self.hidden = hidden
|
self.hidden = hidden
|
||||||
|
|
||||||
# Flags
|
# If prompt is enabled but not required, then the option can be
|
||||||
|
# used as a flag to indicate using prompt or flag_value.
|
||||||
|
self._flag_needs_value = self.prompt is not None and not self.prompt_required
|
||||||
|
|
||||||
if is_flag is None:
|
if is_flag is None:
|
||||||
if flag_value is not None:
|
if flag_value is not None:
|
||||||
|
# Implicitly a flag because flag_value was set.
|
||||||
is_flag = True
|
is_flag = True
|
||||||
|
elif self._flag_needs_value:
|
||||||
|
# Not a flag, but when used as a flag it shows a prompt.
|
||||||
|
is_flag = False
|
||||||
else:
|
else:
|
||||||
|
# Implicitly a flag because flag options were given.
|
||||||
is_flag = bool(self.secondary_opts)
|
is_flag = bool(self.secondary_opts)
|
||||||
|
elif is_flag is False and not self._flag_needs_value:
|
||||||
|
# Not a flag, and prompt is not enabled, can be used as a
|
||||||
|
# flag if flag_value is set.
|
||||||
|
self._flag_needs_value = flag_value is not None
|
||||||
|
|
||||||
if is_flag and default_is_missing:
|
if is_flag and default_is_missing:
|
||||||
self.default = False
|
self.default: t.Union[t.Any, t.Callable[[], t.Any]] = False
|
||||||
|
|
||||||
if flag_value is None:
|
if flag_value is None:
|
||||||
flag_value = not self.default
|
flag_value = not self.default
|
||||||
self.is_flag = is_flag
|
|
||||||
self.flag_value = flag_value
|
if is_flag and type is None:
|
||||||
if self.is_flag and isinstance(self.flag_value, bool) and type in [None, bool]:
|
# Re-guess the type from the flag value instead of the
|
||||||
self.type = BOOL
|
# default.
|
||||||
self.is_bool_flag = True
|
self.type = types.convert_type(None, flag_value)
|
||||||
else:
|
|
||||||
self.is_bool_flag = False
|
self.is_flag: bool = is_flag
|
||||||
|
self.is_bool_flag = is_flag and isinstance(self.type, types.BoolParamType)
|
||||||
|
self.flag_value: t.Any = flag_value
|
||||||
|
|
||||||
# Counting
|
# Counting
|
||||||
self.count = count
|
self.count = count
|
||||||
if count:
|
if count:
|
||||||
if type is None:
|
if type is None:
|
||||||
self.type = IntRange(min=0)
|
self.type = types.IntRange(min=0)
|
||||||
if default_is_missing:
|
if default_is_missing:
|
||||||
self.default = 0
|
self.default = 0
|
||||||
|
|
||||||
self.multiple = multiple
|
|
||||||
self.allow_from_autoenv = allow_from_autoenv
|
self.allow_from_autoenv = allow_from_autoenv
|
||||||
self.help = help
|
self.help = help
|
||||||
self.show_default = show_default
|
self.show_default = show_default
|
||||||
self.show_choices = show_choices
|
self.show_choices = show_choices
|
||||||
self.show_envvar = show_envvar
|
self.show_envvar = show_envvar
|
||||||
|
|
||||||
# Sanity check for stuff we don't support
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
if self.nargs < 0:
|
if self.nargs == -1:
|
||||||
raise TypeError("Options cannot have nargs < 0")
|
raise TypeError("nargs=-1 is not supported for options.")
|
||||||
|
|
||||||
if self.prompt and self.is_flag and not self.is_bool_flag:
|
if self.prompt and self.is_flag and not self.is_bool_flag:
|
||||||
raise TypeError("Cannot prompt for flags that are not bools.")
|
raise TypeError("'prompt' is not valid for non-boolean flag.")
|
||||||
|
|
||||||
if not self.is_bool_flag and self.secondary_opts:
|
if not self.is_bool_flag and self.secondary_opts:
|
||||||
raise TypeError("Got secondary option for non boolean flag.")
|
raise TypeError("Secondary flag is not valid for non-boolean flag.")
|
||||||
|
|
||||||
if self.is_bool_flag and self.hide_input and self.prompt is not None:
|
if self.is_bool_flag and self.hide_input and self.prompt is not None:
|
||||||
raise TypeError("Hidden input does not work with boolean flag prompts.")
|
raise TypeError(
|
||||||
|
"'prompt' with 'hide_input' is not valid for boolean flag."
|
||||||
|
)
|
||||||
|
|
||||||
if self.count:
|
if self.count:
|
||||||
if self.multiple:
|
if self.multiple:
|
||||||
raise TypeError(
|
raise TypeError("'count' is not valid with 'multiple'.")
|
||||||
"Options cannot be multiple and count at the same time."
|
|
||||||
)
|
|
||||||
elif self.is_flag:
|
|
||||||
raise TypeError(
|
|
||||||
"Options cannot be count and flags at the same time."
|
|
||||||
)
|
|
||||||
|
|
||||||
def _parse_decls(self, decls, expose_value):
|
if self.is_flag:
|
||||||
|
raise TypeError("'count' is not valid with 'is_flag'.")
|
||||||
|
|
||||||
|
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||||
|
info_dict = super().to_info_dict()
|
||||||
|
info_dict.update(
|
||||||
|
help=self.help,
|
||||||
|
prompt=self.prompt,
|
||||||
|
is_flag=self.is_flag,
|
||||||
|
flag_value=self.flag_value,
|
||||||
|
count=self.count,
|
||||||
|
hidden=self.hidden,
|
||||||
|
)
|
||||||
|
return info_dict
|
||||||
|
|
||||||
|
def _parse_decls(
|
||||||
|
self, decls: t.Sequence[str], expose_value: bool
|
||||||
|
) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]:
|
||||||
opts = []
|
opts = []
|
||||||
secondary_opts = []
|
secondary_opts = []
|
||||||
name = None
|
name = None
|
||||||
possible_names = []
|
possible_names = []
|
||||||
|
|
||||||
for decl in decls:
|
for decl in decls:
|
||||||
if isidentifier(decl):
|
if decl.isidentifier():
|
||||||
if name is not None:
|
if name is not None:
|
||||||
raise TypeError("Name defined twice")
|
raise TypeError(f"Name '{name}' defined twice")
|
||||||
name = decl
|
name = decl
|
||||||
else:
|
else:
|
||||||
split_char = ";" if decl[:1] == "/" else "/"
|
split_char = ";" if decl[:1] == "/" else "/"
|
||||||
|
@ -1799,6 +2584,11 @@ class Option(Parameter):
|
||||||
second = second.lstrip()
|
second = second.lstrip()
|
||||||
if second:
|
if second:
|
||||||
secondary_opts.append(second.lstrip())
|
secondary_opts.append(second.lstrip())
|
||||||
|
if first == second:
|
||||||
|
raise ValueError(
|
||||||
|
f"Boolean option {decl!r} cannot use the"
|
||||||
|
" same flag for true/false."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
possible_names.append(split_opt(decl))
|
possible_names.append(split_opt(decl))
|
||||||
opts.append(decl)
|
opts.append(decl)
|
||||||
|
@ -1806,7 +2596,7 @@ class Option(Parameter):
|
||||||
if name is None and possible_names:
|
if name is None and possible_names:
|
||||||
possible_names.sort(key=lambda x: -len(x[0])) # group long options first
|
possible_names.sort(key=lambda x: -len(x[0])) # group long options first
|
||||||
name = possible_names[0][1].replace("-", "_").lower()
|
name = possible_names[0][1].replace("-", "_").lower()
|
||||||
if not isidentifier(name):
|
if not name.isidentifier():
|
||||||
name = None
|
name = None
|
||||||
|
|
||||||
if name is None:
|
if name is None:
|
||||||
|
@ -1816,19 +2606,14 @@ class Option(Parameter):
|
||||||
|
|
||||||
if not opts and not secondary_opts:
|
if not opts and not secondary_opts:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"No options defined but a name was passed ({}). Did you"
|
f"No options defined but a name was passed ({name})."
|
||||||
" mean to declare an argument instead of an option?".format(name)
|
" Did you mean to declare an argument instead? Did"
|
||||||
|
f" you mean to pass '--{name}'?"
|
||||||
)
|
)
|
||||||
|
|
||||||
return name, opts, secondary_opts
|
return name, opts, secondary_opts
|
||||||
|
|
||||||
def add_to_parser(self, parser, ctx):
|
def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
|
||||||
kwargs = {
|
|
||||||
"dest": self.name,
|
|
||||||
"nargs": self.nargs,
|
|
||||||
"obj": self,
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.multiple:
|
if self.multiple:
|
||||||
action = "append"
|
action = "append"
|
||||||
elif self.count:
|
elif self.count:
|
||||||
|
@ -1837,74 +2622,150 @@ class Option(Parameter):
|
||||||
action = "store"
|
action = "store"
|
||||||
|
|
||||||
if self.is_flag:
|
if self.is_flag:
|
||||||
kwargs.pop("nargs", None)
|
action = f"{action}_const"
|
||||||
action_const = "{}_const".format(action)
|
|
||||||
if self.is_bool_flag and self.secondary_opts:
|
if self.is_bool_flag and self.secondary_opts:
|
||||||
parser.add_option(self.opts, action=action_const, const=True, **kwargs)
|
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
self.secondary_opts, action=action_const, const=False, **kwargs
|
obj=self, opts=self.opts, dest=self.name, action=action, const=True
|
||||||
|
)
|
||||||
|
parser.add_option(
|
||||||
|
obj=self,
|
||||||
|
opts=self.secondary_opts,
|
||||||
|
dest=self.name,
|
||||||
|
action=action,
|
||||||
|
const=False,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
self.opts, action=action_const, const=self.flag_value, **kwargs
|
obj=self,
|
||||||
|
opts=self.opts,
|
||||||
|
dest=self.name,
|
||||||
|
action=action,
|
||||||
|
const=self.flag_value,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
kwargs["action"] = action
|
parser.add_option(
|
||||||
parser.add_option(self.opts, **kwargs)
|
obj=self,
|
||||||
|
opts=self.opts,
|
||||||
|
dest=self.name,
|
||||||
|
action=action,
|
||||||
|
nargs=self.nargs,
|
||||||
|
)
|
||||||
|
|
||||||
def get_help_record(self, ctx):
|
def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]:
|
||||||
if self.hidden:
|
if self.hidden:
|
||||||
return
|
return None
|
||||||
any_prefix_is_slash = []
|
|
||||||
|
any_prefix_is_slash = False
|
||||||
|
|
||||||
|
def _write_opts(opts: t.Sequence[str]) -> str:
|
||||||
|
nonlocal any_prefix_is_slash
|
||||||
|
|
||||||
def _write_opts(opts):
|
|
||||||
rv, any_slashes = join_options(opts)
|
rv, any_slashes = join_options(opts)
|
||||||
|
|
||||||
if any_slashes:
|
if any_slashes:
|
||||||
any_prefix_is_slash[:] = [True]
|
any_prefix_is_slash = True
|
||||||
|
|
||||||
if not self.is_flag and not self.count:
|
if not self.is_flag and not self.count:
|
||||||
rv += " {}".format(self.make_metavar())
|
rv += f" {self.make_metavar()}"
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
rv = [_write_opts(self.opts)]
|
rv = [_write_opts(self.opts)]
|
||||||
|
|
||||||
if self.secondary_opts:
|
if self.secondary_opts:
|
||||||
rv.append(_write_opts(self.secondary_opts))
|
rv.append(_write_opts(self.secondary_opts))
|
||||||
|
|
||||||
help = self.help or ""
|
help = self.help or ""
|
||||||
extra = []
|
extra = []
|
||||||
|
|
||||||
if self.show_envvar:
|
if self.show_envvar:
|
||||||
envvar = self.envvar
|
envvar = self.envvar
|
||||||
|
|
||||||
if envvar is None:
|
if envvar is None:
|
||||||
if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None:
|
if (
|
||||||
envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper())
|
self.allow_from_autoenv
|
||||||
|
and ctx.auto_envvar_prefix is not None
|
||||||
|
and self.name is not None
|
||||||
|
):
|
||||||
|
envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"
|
||||||
|
|
||||||
if envvar is not None:
|
if envvar is not None:
|
||||||
extra.append(
|
var_str = (
|
||||||
"env var: {}".format(
|
envvar
|
||||||
", ".join(str(d) for d in envvar)
|
if isinstance(envvar, str)
|
||||||
if isinstance(envvar, (list, tuple))
|
else ", ".join(str(d) for d in envvar)
|
||||||
else envvar
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if self.default is not None and (self.show_default or ctx.show_default):
|
extra.append(_("env var: {var}").format(var=var_str))
|
||||||
if isinstance(self.show_default, string_types):
|
|
||||||
default_string = "({})".format(self.show_default)
|
# Temporarily enable resilient parsing to avoid type casting
|
||||||
elif isinstance(self.default, (list, tuple)):
|
# failing for the default. Might be possible to extend this to
|
||||||
default_string = ", ".join(str(d) for d in self.default)
|
# help formatting in general.
|
||||||
elif inspect.isfunction(self.default):
|
resilient = ctx.resilient_parsing
|
||||||
default_string = "(dynamic)"
|
ctx.resilient_parsing = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
default_value = self.get_default(ctx, call=False)
|
||||||
|
finally:
|
||||||
|
ctx.resilient_parsing = resilient
|
||||||
|
|
||||||
|
show_default_is_str = isinstance(self.show_default, str)
|
||||||
|
|
||||||
|
if show_default_is_str or (
|
||||||
|
default_value is not None and (self.show_default or ctx.show_default)
|
||||||
|
):
|
||||||
|
if show_default_is_str:
|
||||||
|
default_string = f"({self.show_default})"
|
||||||
|
elif isinstance(default_value, (list, tuple)):
|
||||||
|
default_string = ", ".join(str(d) for d in default_value)
|
||||||
|
elif callable(default_value):
|
||||||
|
default_string = _("(dynamic)")
|
||||||
|
elif self.is_bool_flag and self.secondary_opts:
|
||||||
|
# For boolean flags that have distinct True/False opts,
|
||||||
|
# use the opt without prefix instead of the value.
|
||||||
|
default_string = split_opt(
|
||||||
|
(self.opts if self.default else self.secondary_opts)[0]
|
||||||
|
)[1]
|
||||||
else:
|
else:
|
||||||
default_string = self.default
|
default_string = str(default_value)
|
||||||
extra.append("default: {}".format(default_string))
|
|
||||||
|
if default_string:
|
||||||
|
extra.append(_("default: {default}").format(default=default_string))
|
||||||
|
|
||||||
|
if (
|
||||||
|
isinstance(self.type, types._NumberRangeBase)
|
||||||
|
# skip count with default range type
|
||||||
|
and not (self.count and self.type.min == 0 and self.type.max is None)
|
||||||
|
):
|
||||||
|
range_str = self.type._describe_range()
|
||||||
|
|
||||||
|
if range_str:
|
||||||
|
extra.append(range_str)
|
||||||
|
|
||||||
if self.required:
|
if self.required:
|
||||||
extra.append("required")
|
extra.append(_("required"))
|
||||||
|
|
||||||
if extra:
|
if extra:
|
||||||
help = "{}[{}]".format(
|
extra_str = "; ".join(extra)
|
||||||
"{} ".format(help) if help else "", "; ".join(extra)
|
help = f"{help} [{extra_str}]" if help else f"[{extra_str}]"
|
||||||
)
|
|
||||||
|
|
||||||
return ("; " if any_prefix_is_slash else " / ").join(rv), help
|
return ("; " if any_prefix_is_slash else " / ").join(rv), help
|
||||||
|
|
||||||
def get_default(self, ctx):
|
@typing.overload
|
||||||
|
def get_default(
|
||||||
|
self, ctx: Context, call: "te.Literal[True]" = True
|
||||||
|
) -> t.Optional[t.Any]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@typing.overload
|
||||||
|
def get_default(
|
||||||
|
self, ctx: Context, call: bool = ...
|
||||||
|
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def get_default(
|
||||||
|
self, ctx: Context, call: bool = True
|
||||||
|
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
|
||||||
# If we're a non boolean flag our default is more complex because
|
# If we're a non boolean flag our default is more complex because
|
||||||
# we need to look at all flags in the same group to figure out
|
# we need to look at all flags in the same group to figure out
|
||||||
# if we're the the default one in which case we return the flag
|
# if we're the the default one in which case we return the flag
|
||||||
|
@ -1912,16 +2773,20 @@ class Option(Parameter):
|
||||||
if self.is_flag and not self.is_bool_flag:
|
if self.is_flag and not self.is_bool_flag:
|
||||||
for param in ctx.command.params:
|
for param in ctx.command.params:
|
||||||
if param.name == self.name and param.default:
|
if param.name == self.name and param.default:
|
||||||
return param.flag_value
|
return param.flag_value # type: ignore
|
||||||
return None
|
|
||||||
return Parameter.get_default(self, ctx)
|
|
||||||
|
|
||||||
def prompt_for_value(self, ctx):
|
return None
|
||||||
|
|
||||||
|
return super().get_default(ctx, call=call)
|
||||||
|
|
||||||
|
def prompt_for_value(self, ctx: Context) -> t.Any:
|
||||||
"""This is an alternative flow that can be activated in the full
|
"""This is an alternative flow that can be activated in the full
|
||||||
value processing if a value does not exist. It will prompt the
|
value processing if a value does not exist. It will prompt the
|
||||||
user until a valid value exists and then returns the processed
|
user until a valid value exists and then returns the processed
|
||||||
value as result.
|
value as result.
|
||||||
"""
|
"""
|
||||||
|
assert self.prompt is not None
|
||||||
|
|
||||||
# Calculate the default before prompting anything to be stable.
|
# Calculate the default before prompting anything to be stable.
|
||||||
default = self.get_default(ctx)
|
default = self.get_default(ctx)
|
||||||
|
|
||||||
|
@ -1940,29 +2805,74 @@ class Option(Parameter):
|
||||||
value_proc=lambda x: self.process_value(ctx, x),
|
value_proc=lambda x: self.process_value(ctx, x),
|
||||||
)
|
)
|
||||||
|
|
||||||
def resolve_envvar_value(self, ctx):
|
def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]:
|
||||||
rv = Parameter.resolve_envvar_value(self, ctx)
|
rv = super().resolve_envvar_value(ctx)
|
||||||
|
|
||||||
if rv is not None:
|
if rv is not None:
|
||||||
return rv
|
return rv
|
||||||
if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None:
|
|
||||||
envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper())
|
|
||||||
return os.environ.get(envvar)
|
|
||||||
|
|
||||||
def value_from_envvar(self, ctx):
|
if (
|
||||||
rv = self.resolve_envvar_value(ctx)
|
self.allow_from_autoenv
|
||||||
if rv is None:
|
and ctx.auto_envvar_prefix is not None
|
||||||
return None
|
and self.name is not None
|
||||||
value_depth = (self.nargs != 1) + bool(self.multiple)
|
):
|
||||||
if value_depth > 0 and rv is not None:
|
envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"
|
||||||
rv = self.type.split_envvar_value(rv)
|
rv = os.environ.get(envvar)
|
||||||
if self.multiple and self.nargs != 1:
|
|
||||||
rv = batch(rv, self.nargs)
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def full_process_value(self, ctx, value):
|
def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]:
|
||||||
if value is None and self.prompt is not None and not ctx.resilient_parsing:
|
rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx)
|
||||||
return self.prompt_for_value(ctx)
|
|
||||||
return Parameter.full_process_value(self, ctx, value)
|
if rv is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
value_depth = (self.nargs != 1) + bool(self.multiple)
|
||||||
|
|
||||||
|
if value_depth > 0:
|
||||||
|
rv = self.type.split_envvar_value(rv)
|
||||||
|
|
||||||
|
if self.multiple and self.nargs != 1:
|
||||||
|
rv = batch(rv, self.nargs)
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def consume_value(
|
||||||
|
self, ctx: Context, opts: t.Mapping[str, "Parameter"]
|
||||||
|
) -> t.Tuple[t.Any, ParameterSource]:
|
||||||
|
value, source = super().consume_value(ctx, opts)
|
||||||
|
|
||||||
|
# The parser will emit a sentinel value if the option can be
|
||||||
|
# given as a flag without a value. This is different from None
|
||||||
|
# to distinguish from the flag not being given at all.
|
||||||
|
if value is _flag_needs_value:
|
||||||
|
if self.prompt is not None and not ctx.resilient_parsing:
|
||||||
|
value = self.prompt_for_value(ctx)
|
||||||
|
source = ParameterSource.PROMPT
|
||||||
|
else:
|
||||||
|
value = self.flag_value
|
||||||
|
source = ParameterSource.COMMANDLINE
|
||||||
|
|
||||||
|
elif (
|
||||||
|
self.multiple
|
||||||
|
and value is not None
|
||||||
|
and any(v is _flag_needs_value for v in value)
|
||||||
|
):
|
||||||
|
value = [self.flag_value if v is _flag_needs_value else v for v in value]
|
||||||
|
source = ParameterSource.COMMANDLINE
|
||||||
|
|
||||||
|
# The value wasn't set, or used the param's default, prompt if
|
||||||
|
# prompting is enabled.
|
||||||
|
elif (
|
||||||
|
source in {None, ParameterSource.DEFAULT}
|
||||||
|
and self.prompt is not None
|
||||||
|
and (self.required or self.prompt_required)
|
||||||
|
and not ctx.resilient_parsing
|
||||||
|
):
|
||||||
|
value = self.prompt_for_value(ctx)
|
||||||
|
source = ParameterSource.PROMPT
|
||||||
|
|
||||||
|
return value, source
|
||||||
|
|
||||||
|
|
||||||
class Argument(Parameter):
|
class Argument(Parameter):
|
||||||
|
@ -1975,37 +2885,48 @@ class Argument(Parameter):
|
||||||
|
|
||||||
param_type_name = "argument"
|
param_type_name = "argument"
|
||||||
|
|
||||||
def __init__(self, param_decls, required=None, **attrs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
param_decls: t.Sequence[str],
|
||||||
|
required: t.Optional[bool] = None,
|
||||||
|
**attrs: t.Any,
|
||||||
|
) -> None:
|
||||||
if required is None:
|
if required is None:
|
||||||
if attrs.get("default") is not None:
|
if attrs.get("default") is not None:
|
||||||
required = False
|
required = False
|
||||||
else:
|
else:
|
||||||
required = attrs.get("nargs", 1) > 0
|
required = attrs.get("nargs", 1) > 0
|
||||||
Parameter.__init__(self, param_decls, required=required, **attrs)
|
|
||||||
if self.default is not None and self.nargs < 0:
|
if "multiple" in attrs:
|
||||||
raise TypeError(
|
raise TypeError("__init__() got an unexpected keyword argument 'multiple'.")
|
||||||
"nargs=-1 in combination with a default value is not supported."
|
|
||||||
)
|
super().__init__(param_decls, required=required, **attrs)
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
if self.default is not None and self.nargs == -1:
|
||||||
|
raise TypeError("'default' is not supported for nargs=-1.")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def human_readable_name(self):
|
def human_readable_name(self) -> str:
|
||||||
if self.metavar is not None:
|
if self.metavar is not None:
|
||||||
return self.metavar
|
return self.metavar
|
||||||
return self.name.upper()
|
return self.name.upper() # type: ignore
|
||||||
|
|
||||||
def make_metavar(self):
|
def make_metavar(self) -> str:
|
||||||
if self.metavar is not None:
|
if self.metavar is not None:
|
||||||
return self.metavar
|
return self.metavar
|
||||||
var = self.type.get_metavar(self)
|
var = self.type.get_metavar(self)
|
||||||
if not var:
|
if not var:
|
||||||
var = self.name.upper()
|
var = self.name.upper() # type: ignore
|
||||||
if not self.required:
|
if not self.required:
|
||||||
var = "[{}]".format(var)
|
var = f"[{var}]"
|
||||||
if self.nargs != 1:
|
if self.nargs != 1:
|
||||||
var += "..."
|
var += "..."
|
||||||
return var
|
return var
|
||||||
|
|
||||||
def _parse_decls(self, decls, expose_value):
|
def _parse_decls(
|
||||||
|
self, decls: t.Sequence[str], expose_value: bool
|
||||||
|
) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]:
|
||||||
if not decls:
|
if not decls:
|
||||||
if not expose_value:
|
if not expose_value:
|
||||||
return None, [], []
|
return None, [], []
|
||||||
|
@ -2016,15 +2937,15 @@ class Argument(Parameter):
|
||||||
else:
|
else:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"Arguments take exactly one parameter declaration, got"
|
"Arguments take exactly one parameter declaration, got"
|
||||||
" {}".format(len(decls))
|
f" {len(decls)}."
|
||||||
)
|
)
|
||||||
return name, [arg], []
|
return name, [arg], []
|
||||||
|
|
||||||
def get_usage_pieces(self, ctx):
|
def get_usage_pieces(self, ctx: Context) -> t.List[str]:
|
||||||
return [self.make_metavar()]
|
return [self.make_metavar()]
|
||||||
|
|
||||||
def get_error_hint(self, ctx):
|
def get_error_hint(self, ctx: Context) -> str:
|
||||||
return repr(self.make_metavar())
|
return f"'{self.make_metavar()}'"
|
||||||
|
|
||||||
def add_to_parser(self, parser, ctx):
|
def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
|
||||||
parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)
|
parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)
|
||||||
|
|
|
@ -1,41 +1,48 @@
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import types
|
||||||
|
import typing as t
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
from ._compat import iteritems
|
|
||||||
from ._unicodefun import _check_for_unicode_literals
|
|
||||||
from .core import Argument
|
from .core import Argument
|
||||||
from .core import Command
|
from .core import Command
|
||||||
|
from .core import Context
|
||||||
from .core import Group
|
from .core import Group
|
||||||
from .core import Option
|
from .core import Option
|
||||||
|
from .core import Parameter
|
||||||
from .globals import get_current_context
|
from .globals import get_current_context
|
||||||
from .utils import echo
|
from .utils import echo
|
||||||
|
|
||||||
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
FC = t.TypeVar("FC", t.Callable[..., t.Any], Command)
|
||||||
|
|
||||||
def pass_context(f):
|
|
||||||
|
def pass_context(f: F) -> F:
|
||||||
"""Marks a callback as wanting to receive the current context
|
"""Marks a callback as wanting to receive the current context
|
||||||
object as first argument.
|
object as first argument.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def new_func(*args, **kwargs):
|
def new_func(*args, **kwargs): # type: ignore
|
||||||
return f(get_current_context(), *args, **kwargs)
|
return f(get_current_context(), *args, **kwargs)
|
||||||
|
|
||||||
return update_wrapper(new_func, f)
|
return update_wrapper(t.cast(F, new_func), f)
|
||||||
|
|
||||||
|
|
||||||
def pass_obj(f):
|
def pass_obj(f: F) -> F:
|
||||||
"""Similar to :func:`pass_context`, but only pass the object on the
|
"""Similar to :func:`pass_context`, but only pass the object on the
|
||||||
context onwards (:attr:`Context.obj`). This is useful if that object
|
context onwards (:attr:`Context.obj`). This is useful if that object
|
||||||
represents the state of a nested system.
|
represents the state of a nested system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def new_func(*args, **kwargs):
|
def new_func(*args, **kwargs): # type: ignore
|
||||||
return f(get_current_context().obj, *args, **kwargs)
|
return f(get_current_context().obj, *args, **kwargs)
|
||||||
|
|
||||||
return update_wrapper(new_func, f)
|
return update_wrapper(t.cast(F, new_func), f)
|
||||||
|
|
||||||
|
|
||||||
def make_pass_decorator(object_type, ensure=False):
|
def make_pass_decorator(
|
||||||
|
object_type: t.Type, ensure: bool = False
|
||||||
|
) -> "t.Callable[[F], F]":
|
||||||
"""Given an object type this creates a decorator that will work
|
"""Given an object type this creates a decorator that will work
|
||||||
similar to :func:`pass_obj` but instead of passing the object of the
|
similar to :func:`pass_obj` but instead of passing the object of the
|
||||||
current context, it will find the innermost context of type
|
current context, it will find the innermost context of type
|
||||||
|
@ -58,52 +65,99 @@ def make_pass_decorator(object_type, ensure=False):
|
||||||
remembered on the context if it's not there yet.
|
remembered on the context if it's not there yet.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f: F) -> F:
|
||||||
def new_func(*args, **kwargs):
|
def new_func(*args, **kwargs): # type: ignore
|
||||||
ctx = get_current_context()
|
ctx = get_current_context()
|
||||||
|
|
||||||
if ensure:
|
if ensure:
|
||||||
obj = ctx.ensure_object(object_type)
|
obj = ctx.ensure_object(object_type)
|
||||||
else:
|
else:
|
||||||
obj = ctx.find_object(object_type)
|
obj = ctx.find_object(object_type)
|
||||||
|
|
||||||
if obj is None:
|
if obj is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Managed to invoke callback without a context"
|
"Managed to invoke callback without a context"
|
||||||
" object of type '{}' existing".format(object_type.__name__)
|
f" object of type {object_type.__name__!r}"
|
||||||
|
" existing."
|
||||||
)
|
)
|
||||||
|
|
||||||
return ctx.invoke(f, obj, *args, **kwargs)
|
return ctx.invoke(f, obj, *args, **kwargs)
|
||||||
|
|
||||||
return update_wrapper(new_func, f)
|
return update_wrapper(t.cast(F, new_func), f)
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def _make_command(f, name, attrs, cls):
|
def pass_meta_key(
|
||||||
|
key: str, *, doc_description: t.Optional[str] = None
|
||||||
|
) -> "t.Callable[[F], F]":
|
||||||
|
"""Create a decorator that passes a key from
|
||||||
|
:attr:`click.Context.meta` as the first argument to the decorated
|
||||||
|
function.
|
||||||
|
|
||||||
|
:param key: Key in ``Context.meta`` to pass.
|
||||||
|
:param doc_description: Description of the object being passed,
|
||||||
|
inserted into the decorator's docstring. Defaults to "the 'key'
|
||||||
|
key from Context.meta".
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f: F) -> F:
|
||||||
|
def new_func(*args, **kwargs): # type: ignore
|
||||||
|
ctx = get_current_context()
|
||||||
|
obj = ctx.meta[key]
|
||||||
|
return ctx.invoke(f, obj, *args, **kwargs)
|
||||||
|
|
||||||
|
return update_wrapper(t.cast(F, new_func), f)
|
||||||
|
|
||||||
|
if doc_description is None:
|
||||||
|
doc_description = f"the {key!r} key from :attr:`click.Context.meta`"
|
||||||
|
|
||||||
|
decorator.__doc__ = (
|
||||||
|
f"Decorator that passes {doc_description} as the first argument"
|
||||||
|
" to the decorated function."
|
||||||
|
)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _make_command(
|
||||||
|
f: F,
|
||||||
|
name: t.Optional[str],
|
||||||
|
attrs: t.MutableMapping[str, t.Any],
|
||||||
|
cls: t.Type[Command],
|
||||||
|
) -> Command:
|
||||||
if isinstance(f, Command):
|
if isinstance(f, Command):
|
||||||
raise TypeError("Attempted to convert a callback into a command twice.")
|
raise TypeError("Attempted to convert a callback into a command twice.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
params = f.__click_params__
|
params = f.__click_params__ # type: ignore
|
||||||
params.reverse()
|
params.reverse()
|
||||||
del f.__click_params__
|
del f.__click_params__ # type: ignore
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
help = attrs.get("help")
|
help = attrs.get("help")
|
||||||
|
|
||||||
if help is None:
|
if help is None:
|
||||||
help = inspect.getdoc(f)
|
help = inspect.getdoc(f)
|
||||||
if isinstance(help, bytes):
|
|
||||||
help = help.decode("utf-8")
|
|
||||||
else:
|
else:
|
||||||
help = inspect.cleandoc(help)
|
help = inspect.cleandoc(help)
|
||||||
|
|
||||||
attrs["help"] = help
|
attrs["help"] = help
|
||||||
_check_for_unicode_literals()
|
|
||||||
return cls(
|
return cls(
|
||||||
name=name or f.__name__.lower().replace("_", "-"),
|
name=name or f.__name__.lower().replace("_", "-"),
|
||||||
callback=f,
|
callback=f,
|
||||||
params=params,
|
params=params,
|
||||||
**attrs
|
**attrs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def command(name=None, cls=None, **attrs):
|
def command(
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
cls: t.Optional[t.Type[Command]] = None,
|
||||||
|
**attrs: t.Any,
|
||||||
|
) -> t.Callable[[F], Command]:
|
||||||
r"""Creates a new :class:`Command` and uses the decorated function as
|
r"""Creates a new :class:`Command` and uses the decorated function as
|
||||||
callback. This will also automatically attach all decorated
|
callback. This will also automatically attach all decorated
|
||||||
:func:`option`\s and :func:`argument`\s as parameters to the command.
|
:func:`option`\s and :func:`argument`\s as parameters to the command.
|
||||||
|
@ -126,33 +180,34 @@ def command(name=None, cls=None, **attrs):
|
||||||
if cls is None:
|
if cls is None:
|
||||||
cls = Command
|
cls = Command
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f: t.Callable[..., t.Any]) -> Command:
|
||||||
cmd = _make_command(f, name, attrs, cls)
|
cmd = _make_command(f, name, attrs, cls) # type: ignore
|
||||||
cmd.__doc__ = f.__doc__
|
cmd.__doc__ = f.__doc__
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def group(name=None, **attrs):
|
def group(name: t.Optional[str] = None, **attrs: t.Any) -> t.Callable[[F], Group]:
|
||||||
"""Creates a new :class:`Group` with a function as callback. This
|
"""Creates a new :class:`Group` with a function as callback. This
|
||||||
works otherwise the same as :func:`command` just that the `cls`
|
works otherwise the same as :func:`command` just that the `cls`
|
||||||
parameter is set to :class:`Group`.
|
parameter is set to :class:`Group`.
|
||||||
"""
|
"""
|
||||||
attrs.setdefault("cls", Group)
|
attrs.setdefault("cls", Group)
|
||||||
return command(name, **attrs)
|
return t.cast(Group, command(name, **attrs))
|
||||||
|
|
||||||
|
|
||||||
def _param_memo(f, param):
|
def _param_memo(f: FC, param: Parameter) -> None:
|
||||||
if isinstance(f, Command):
|
if isinstance(f, Command):
|
||||||
f.params.append(param)
|
f.params.append(param)
|
||||||
else:
|
else:
|
||||||
if not hasattr(f, "__click_params__"):
|
if not hasattr(f, "__click_params__"):
|
||||||
f.__click_params__ = []
|
f.__click_params__ = [] # type: ignore
|
||||||
f.__click_params__.append(param)
|
|
||||||
|
f.__click_params__.append(param) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def argument(*param_decls, **attrs):
|
def argument(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
|
||||||
"""Attaches an argument to the command. All positional arguments are
|
"""Attaches an argument to the command. All positional arguments are
|
||||||
passed as parameter declarations to :class:`Argument`; all keyword
|
passed as parameter declarations to :class:`Argument`; all keyword
|
||||||
arguments are forwarded unchanged (except ``cls``).
|
arguments are forwarded unchanged (except ``cls``).
|
||||||
|
@ -163,7 +218,7 @@ def argument(*param_decls, **attrs):
|
||||||
:class:`Argument`.
|
:class:`Argument`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f: FC) -> FC:
|
||||||
ArgumentClass = attrs.pop("cls", Argument)
|
ArgumentClass = attrs.pop("cls", Argument)
|
||||||
_param_memo(f, ArgumentClass(param_decls, **attrs))
|
_param_memo(f, ArgumentClass(param_decls, **attrs))
|
||||||
return f
|
return f
|
||||||
|
@ -171,7 +226,7 @@ def argument(*param_decls, **attrs):
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def option(*param_decls, **attrs):
|
def option(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
|
||||||
"""Attaches an option to the command. All positional arguments are
|
"""Attaches an option to the command. All positional arguments are
|
||||||
passed as parameter declarations to :class:`Option`; all keyword
|
passed as parameter declarations to :class:`Option`; all keyword
|
||||||
arguments are forwarded unchanged (except ``cls``).
|
arguments are forwarded unchanged (except ``cls``).
|
||||||
|
@ -182,7 +237,7 @@ def option(*param_decls, **attrs):
|
||||||
:class:`Option`.
|
:class:`Option`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f: FC) -> FC:
|
||||||
# Issue 926, copy attrs, so pre-defined options can re-use the same cls=
|
# Issue 926, copy attrs, so pre-defined options can re-use the same cls=
|
||||||
option_attrs = attrs.copy()
|
option_attrs = attrs.copy()
|
||||||
|
|
||||||
|
@ -195,139 +250,187 @@ def option(*param_decls, **attrs):
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def confirmation_option(*param_decls, **attrs):
|
def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
|
||||||
"""Shortcut for confirmation prompts that can be ignored by passing
|
"""Add a ``--yes`` option which shows a prompt before continuing if
|
||||||
``--yes`` as parameter.
|
not passed. If the prompt is declined, the program will exit.
|
||||||
|
|
||||||
This is equivalent to decorating a function with :func:`option` with
|
:param param_decls: One or more option names. Defaults to the single
|
||||||
the following parameters::
|
value ``"--yes"``.
|
||||||
|
:param kwargs: Extra arguments are passed to :func:`option`.
|
||||||
def callback(ctx, param, value):
|
|
||||||
if not value:
|
|
||||||
ctx.abort()
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option('--yes', is_flag=True, callback=callback,
|
|
||||||
expose_value=False, prompt='Do you want to continue?')
|
|
||||||
def dropdb():
|
|
||||||
pass
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f):
|
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||||
def callback(ctx, param, value):
|
if not value:
|
||||||
if not value:
|
ctx.abort()
|
||||||
ctx.abort()
|
|
||||||
|
|
||||||
attrs.setdefault("is_flag", True)
|
if not param_decls:
|
||||||
attrs.setdefault("callback", callback)
|
param_decls = ("--yes",)
|
||||||
attrs.setdefault("expose_value", False)
|
|
||||||
attrs.setdefault("prompt", "Do you want to continue?")
|
|
||||||
attrs.setdefault("help", "Confirm the action without prompting.")
|
|
||||||
return option(*(param_decls or ("--yes",)), **attrs)(f)
|
|
||||||
|
|
||||||
return decorator
|
kwargs.setdefault("is_flag", True)
|
||||||
|
kwargs.setdefault("callback", callback)
|
||||||
|
kwargs.setdefault("expose_value", False)
|
||||||
|
kwargs.setdefault("prompt", "Do you want to continue?")
|
||||||
|
kwargs.setdefault("help", "Confirm the action without prompting.")
|
||||||
|
return option(*param_decls, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def password_option(*param_decls, **attrs):
|
def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
|
||||||
"""Shortcut for password prompts.
|
"""Add a ``--password`` option which prompts for a password, hiding
|
||||||
|
input and asking to enter the value again for confirmation.
|
||||||
|
|
||||||
This is equivalent to decorating a function with :func:`option` with
|
:param param_decls: One or more option names. Defaults to the single
|
||||||
the following parameters::
|
value ``"--password"``.
|
||||||
|
:param kwargs: Extra arguments are passed to :func:`option`.
|
||||||
|
"""
|
||||||
|
if not param_decls:
|
||||||
|
param_decls = ("--password",)
|
||||||
|
|
||||||
@click.command()
|
kwargs.setdefault("prompt", True)
|
||||||
@click.option('--password', prompt=True, confirmation_prompt=True,
|
kwargs.setdefault("confirmation_prompt", True)
|
||||||
hide_input=True)
|
kwargs.setdefault("hide_input", True)
|
||||||
def changeadmin(password):
|
return option(*param_decls, **kwargs)
|
||||||
pass
|
|
||||||
|
|
||||||
|
def version_option(
|
||||||
|
version: t.Optional[str] = None,
|
||||||
|
*param_decls: str,
|
||||||
|
package_name: t.Optional[str] = None,
|
||||||
|
prog_name: t.Optional[str] = None,
|
||||||
|
message: t.Optional[str] = None,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> t.Callable[[FC], FC]:
|
||||||
|
"""Add a ``--version`` option which immediately prints the version
|
||||||
|
number and exits the program.
|
||||||
|
|
||||||
|
If ``version`` is not provided, Click will try to detect it using
|
||||||
|
:func:`importlib.metadata.version` to get the version for the
|
||||||
|
``package_name``. On Python < 3.8, the ``importlib_metadata``
|
||||||
|
backport must be installed.
|
||||||
|
|
||||||
|
If ``package_name`` is not provided, Click will try to detect it by
|
||||||
|
inspecting the stack frames. This will be used to detect the
|
||||||
|
version, so it must match the name of the installed package.
|
||||||
|
|
||||||
|
:param version: The version number to show. If not provided, Click
|
||||||
|
will try to detect it.
|
||||||
|
:param param_decls: One or more option names. Defaults to the single
|
||||||
|
value ``"--version"``.
|
||||||
|
:param package_name: The package name to detect the version from. If
|
||||||
|
not provided, Click will try to detect it.
|
||||||
|
:param prog_name: The name of the CLI to show in the message. If not
|
||||||
|
provided, it will be detected from the command.
|
||||||
|
:param message: The message to show. The values ``%(prog)s``,
|
||||||
|
``%(package)s``, and ``%(version)s`` are available. Defaults to
|
||||||
|
``"%(prog)s, version %(version)s"``.
|
||||||
|
:param kwargs: Extra arguments are passed to :func:`option`.
|
||||||
|
:raise RuntimeError: ``version`` could not be detected.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Add the ``package_name`` parameter, and the ``%(package)s``
|
||||||
|
value for messages.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Use :mod:`importlib.metadata` instead of ``pkg_resources``. The
|
||||||
|
version is detected based on the package name, not the entry
|
||||||
|
point name. The Python package name must match the installed
|
||||||
|
package name, or be passed with ``package_name=``.
|
||||||
|
"""
|
||||||
|
if message is None:
|
||||||
|
message = _("%(prog)s, version %(version)s")
|
||||||
|
|
||||||
|
if version is None and package_name is None:
|
||||||
|
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 = f_globals.get("__name__")
|
||||||
|
|
||||||
|
if package_name == "__main__":
|
||||||
|
package_name = f_globals.get("__package__")
|
||||||
|
|
||||||
|
if package_name:
|
||||||
|
package_name = package_name.partition(".")[0]
|
||||||
|
|
||||||
|
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
|
||||||
|
nonlocal prog_name
|
||||||
|
nonlocal version
|
||||||
|
|
||||||
|
if prog_name is None:
|
||||||
|
prog_name = ctx.find_root().info_name
|
||||||
|
|
||||||
|
if version is None and package_name is not None:
|
||||||
|
metadata: t.Optional[types.ModuleType]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from importlib import metadata # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
# Python < 3.8
|
||||||
|
import importlib_metadata as metadata # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = metadata.version(package_name) # type: ignore
|
||||||
|
except metadata.PackageNotFoundError: # type: ignore
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{package_name!r} is not installed. Try passing"
|
||||||
|
" 'package_name' instead."
|
||||||
|
) from None
|
||||||
|
|
||||||
|
if version is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Could not determine the version for {package_name!r} automatically."
|
||||||
|
)
|
||||||
|
|
||||||
|
echo(
|
||||||
|
t.cast(str, message)
|
||||||
|
% {"prog": prog_name, "package": package_name, "version": version},
|
||||||
|
color=ctx.color,
|
||||||
|
)
|
||||||
|
ctx.exit()
|
||||||
|
|
||||||
|
if not param_decls:
|
||||||
|
param_decls = ("--version",)
|
||||||
|
|
||||||
|
kwargs.setdefault("is_flag", True)
|
||||||
|
kwargs.setdefault("expose_value", False)
|
||||||
|
kwargs.setdefault("is_eager", True)
|
||||||
|
kwargs.setdefault("help", _("Show the version and exit."))
|
||||||
|
kwargs["callback"] = callback
|
||||||
|
return option(*param_decls, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
|
||||||
|
"""Add a ``--help`` option which immediately prints the help page
|
||||||
|
and exits the program.
|
||||||
|
|
||||||
|
This is usually unnecessary, as the ``--help`` option is added to
|
||||||
|
each command automatically unless ``add_help_option=False`` is
|
||||||
|
passed.
|
||||||
|
|
||||||
|
:param param_decls: One or more option names. Defaults to the single
|
||||||
|
value ``"--help"``.
|
||||||
|
:param kwargs: Extra arguments are passed to :func:`option`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f):
|
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||||
attrs.setdefault("prompt", True)
|
if not value or ctx.resilient_parsing:
|
||||||
attrs.setdefault("confirmation_prompt", True)
|
return
|
||||||
attrs.setdefault("hide_input", True)
|
|
||||||
return option(*(param_decls or ("--password",)), **attrs)(f)
|
|
||||||
|
|
||||||
return decorator
|
echo(ctx.get_help(), color=ctx.color)
|
||||||
|
ctx.exit()
|
||||||
|
|
||||||
|
if not param_decls:
|
||||||
|
param_decls = ("--help",)
|
||||||
|
|
||||||
def version_option(version=None, *param_decls, **attrs):
|
kwargs.setdefault("is_flag", True)
|
||||||
"""Adds a ``--version`` option which immediately ends the program
|
kwargs.setdefault("expose_value", False)
|
||||||
printing out the version number. This is implemented as an eager
|
kwargs.setdefault("is_eager", True)
|
||||||
option that prints the version and exits the program in the callback.
|
kwargs.setdefault("help", _("Show this message and exit."))
|
||||||
|
kwargs["callback"] = callback
|
||||||
:param version: the version number to show. If not provided Click
|
return option(*param_decls, **kwargs)
|
||||||
attempts an auto discovery via setuptools.
|
|
||||||
:param prog_name: the name of the program (defaults to autodetection)
|
|
||||||
:param message: custom message to show instead of the default
|
|
||||||
(``'%(prog)s, version %(version)s'``)
|
|
||||||
:param others: everything else is forwarded to :func:`option`.
|
|
||||||
"""
|
|
||||||
if version is None:
|
|
||||||
if hasattr(sys, "_getframe"):
|
|
||||||
module = sys._getframe(1).f_globals.get("__name__")
|
|
||||||
else:
|
|
||||||
module = ""
|
|
||||||
|
|
||||||
def decorator(f):
|
|
||||||
prog_name = attrs.pop("prog_name", None)
|
|
||||||
message = attrs.pop("message", "%(prog)s, version %(version)s")
|
|
||||||
|
|
||||||
def callback(ctx, param, value):
|
|
||||||
if not value or ctx.resilient_parsing:
|
|
||||||
return
|
|
||||||
prog = prog_name
|
|
||||||
if prog is None:
|
|
||||||
prog = ctx.find_root().info_name
|
|
||||||
ver = version
|
|
||||||
if ver is None:
|
|
||||||
try:
|
|
||||||
import pkg_resources
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
for dist in pkg_resources.working_set:
|
|
||||||
scripts = dist.get_entry_map().get("console_scripts") or {}
|
|
||||||
for _, entry_point in iteritems(scripts):
|
|
||||||
if entry_point.module_name == module:
|
|
||||||
ver = dist.version
|
|
||||||
break
|
|
||||||
if ver is None:
|
|
||||||
raise RuntimeError("Could not determine version")
|
|
||||||
echo(message % {"prog": prog, "version": ver}, color=ctx.color)
|
|
||||||
ctx.exit()
|
|
||||||
|
|
||||||
attrs.setdefault("is_flag", True)
|
|
||||||
attrs.setdefault("expose_value", False)
|
|
||||||
attrs.setdefault("is_eager", True)
|
|
||||||
attrs.setdefault("help", "Show the version and exit.")
|
|
||||||
attrs["callback"] = callback
|
|
||||||
return option(*(param_decls or ("--version",)), **attrs)(f)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def help_option(*param_decls, **attrs):
|
|
||||||
"""Adds a ``--help`` option which immediately ends the program
|
|
||||||
printing out the help page. This is usually unnecessary to add as
|
|
||||||
this is added by default to all commands unless suppressed.
|
|
||||||
|
|
||||||
Like :func:`version_option`, this is implemented as eager option that
|
|
||||||
prints in the callback and exits.
|
|
||||||
|
|
||||||
All arguments are forwarded to :func:`option`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(f):
|
|
||||||
def callback(ctx, param, value):
|
|
||||||
if value and not ctx.resilient_parsing:
|
|
||||||
echo(ctx.get_help(), color=ctx.color)
|
|
||||||
ctx.exit()
|
|
||||||
|
|
||||||
attrs.setdefault("is_flag", True)
|
|
||||||
attrs.setdefault("expose_value", False)
|
|
||||||
attrs.setdefault("help", "Show this message and exit.")
|
|
||||||
attrs.setdefault("is_eager", True)
|
|
||||||
attrs["callback"] = callback
|
|
||||||
return option(*(param_decls or ("--help",)), **attrs)(f)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
|
@ -1,45 +1,46 @@
|
||||||
from ._compat import filename_to_ui
|
import os
|
||||||
|
import typing as t
|
||||||
|
from gettext import gettext as _
|
||||||
|
from gettext import ngettext
|
||||||
|
|
||||||
from ._compat import get_text_stderr
|
from ._compat import get_text_stderr
|
||||||
from ._compat import PY2
|
|
||||||
from .utils import echo
|
from .utils import echo
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .core import Context
|
||||||
|
from .core import Parameter
|
||||||
|
|
||||||
def _join_param_hints(param_hint):
|
|
||||||
if isinstance(param_hint, (tuple, list)):
|
def _join_param_hints(
|
||||||
|
param_hint: t.Optional[t.Union[t.Sequence[str], str]]
|
||||||
|
) -> t.Optional[str]:
|
||||||
|
if param_hint is not None and not isinstance(param_hint, str):
|
||||||
return " / ".join(repr(x) for x in param_hint)
|
return " / ".join(repr(x) for x in param_hint)
|
||||||
|
|
||||||
return param_hint
|
return param_hint
|
||||||
|
|
||||||
|
|
||||||
class ClickException(Exception):
|
class ClickException(Exception):
|
||||||
"""An exception that Click can handle and show to the user."""
|
"""An exception that Click can handle and show to the user."""
|
||||||
|
|
||||||
#: The exit code for this exception
|
#: The exit code for this exception.
|
||||||
exit_code = 1
|
exit_code = 1
|
||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message: str) -> None:
|
||||||
ctor_msg = message
|
super().__init__(message)
|
||||||
if PY2:
|
|
||||||
if ctor_msg is not None:
|
|
||||||
ctor_msg = ctor_msg.encode("utf-8")
|
|
||||||
Exception.__init__(self, ctor_msg)
|
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
def format_message(self):
|
def format_message(self) -> str:
|
||||||
return self.message
|
return self.message
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.message
|
return self.message
|
||||||
|
|
||||||
if PY2:
|
def show(self, file: t.Optional[t.IO] = None) -> None:
|
||||||
__unicode__ = __str__
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.message.encode("utf-8")
|
|
||||||
|
|
||||||
def show(self, file=None):
|
|
||||||
if file is None:
|
if file is None:
|
||||||
file = get_text_stderr()
|
file = get_text_stderr()
|
||||||
echo("Error: {}".format(self.format_message()), file=file)
|
|
||||||
|
echo(_("Error: {message}").format(message=self.format_message()), file=file)
|
||||||
|
|
||||||
|
|
||||||
class UsageError(ClickException):
|
class UsageError(ClickException):
|
||||||
|
@ -53,24 +54,32 @@ class UsageError(ClickException):
|
||||||
|
|
||||||
exit_code = 2
|
exit_code = 2
|
||||||
|
|
||||||
def __init__(self, message, ctx=None):
|
def __init__(self, message: str, ctx: t.Optional["Context"] = None) -> None:
|
||||||
ClickException.__init__(self, message)
|
super().__init__(message)
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.cmd = self.ctx.command if self.ctx else None
|
self.cmd = self.ctx.command if self.ctx else None
|
||||||
|
|
||||||
def show(self, file=None):
|
def show(self, file: t.Optional[t.IO] = None) -> None:
|
||||||
if file is None:
|
if file is None:
|
||||||
file = get_text_stderr()
|
file = get_text_stderr()
|
||||||
color = None
|
color = None
|
||||||
hint = ""
|
hint = ""
|
||||||
if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None:
|
if (
|
||||||
hint = "Try '{} {}' for help.\n".format(
|
self.ctx is not None
|
||||||
self.ctx.command_path, self.ctx.help_option_names[0]
|
and self.ctx.command.get_help_option(self.ctx) is not None
|
||||||
|
):
|
||||||
|
hint = _("Try '{command} {option}' for help.").format(
|
||||||
|
command=self.ctx.command_path, option=self.ctx.help_option_names[0]
|
||||||
)
|
)
|
||||||
|
hint = f"{hint}\n"
|
||||||
if self.ctx is not None:
|
if self.ctx is not None:
|
||||||
color = self.ctx.color
|
color = self.ctx.color
|
||||||
echo("{}\n{}".format(self.ctx.get_usage(), hint), file=file, color=color)
|
echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color)
|
||||||
echo("Error: {}".format(self.format_message()), file=file, color=color)
|
echo(
|
||||||
|
_("Error: {message}").format(message=self.format_message()),
|
||||||
|
file=file,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BadParameter(UsageError):
|
class BadParameter(UsageError):
|
||||||
|
@ -91,21 +100,28 @@ class BadParameter(UsageError):
|
||||||
each item is quoted and separated.
|
each item is quoted and separated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, message, ctx=None, param=None, param_hint=None):
|
def __init__(
|
||||||
UsageError.__init__(self, message, ctx)
|
self,
|
||||||
|
message: str,
|
||||||
|
ctx: t.Optional["Context"] = None,
|
||||||
|
param: t.Optional["Parameter"] = None,
|
||||||
|
param_hint: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(message, ctx)
|
||||||
self.param = param
|
self.param = param
|
||||||
self.param_hint = param_hint
|
self.param_hint = param_hint
|
||||||
|
|
||||||
def format_message(self):
|
def format_message(self) -> str:
|
||||||
if self.param_hint is not None:
|
if self.param_hint is not None:
|
||||||
param_hint = self.param_hint
|
param_hint = self.param_hint
|
||||||
elif self.param is not None:
|
elif self.param is not None:
|
||||||
param_hint = self.param.get_error_hint(self.ctx)
|
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
|
||||||
else:
|
else:
|
||||||
return "Invalid value: {}".format(self.message)
|
return _("Invalid value: {message}").format(message=self.message)
|
||||||
param_hint = _join_param_hints(param_hint)
|
|
||||||
|
|
||||||
return "Invalid value for {}: {}".format(param_hint, self.message)
|
return _("Invalid value for {param_hint}: {message}").format(
|
||||||
|
param_hint=_join_param_hints(param_hint), message=self.message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MissingParameter(BadParameter):
|
class MissingParameter(BadParameter):
|
||||||
|
@ -121,19 +137,26 @@ class MissingParameter(BadParameter):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, message=None, ctx=None, param=None, param_hint=None, param_type=None
|
self,
|
||||||
):
|
message: t.Optional[str] = None,
|
||||||
BadParameter.__init__(self, message, ctx, param, param_hint)
|
ctx: t.Optional["Context"] = None,
|
||||||
|
param: t.Optional["Parameter"] = None,
|
||||||
|
param_hint: t.Optional[str] = None,
|
||||||
|
param_type: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(message or "", ctx, param, param_hint)
|
||||||
self.param_type = param_type
|
self.param_type = param_type
|
||||||
|
|
||||||
def format_message(self):
|
def format_message(self) -> str:
|
||||||
if self.param_hint is not None:
|
if self.param_hint is not None:
|
||||||
param_hint = self.param_hint
|
param_hint: t.Optional[str] = self.param_hint
|
||||||
elif self.param is not None:
|
elif self.param is not None:
|
||||||
param_hint = self.param.get_error_hint(self.ctx)
|
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
|
||||||
else:
|
else:
|
||||||
param_hint = None
|
param_hint = None
|
||||||
|
|
||||||
param_hint = _join_param_hints(param_hint)
|
param_hint = _join_param_hints(param_hint)
|
||||||
|
param_hint = f" {param_hint}" if param_hint else ""
|
||||||
|
|
||||||
param_type = self.param_type
|
param_type = self.param_type
|
||||||
if param_type is None and self.param is not None:
|
if param_type is None and self.param is not None:
|
||||||
|
@ -144,30 +167,31 @@ class MissingParameter(BadParameter):
|
||||||
msg_extra = self.param.type.get_missing_message(self.param)
|
msg_extra = self.param.type.get_missing_message(self.param)
|
||||||
if msg_extra:
|
if msg_extra:
|
||||||
if msg:
|
if msg:
|
||||||
msg += ". {}".format(msg_extra)
|
msg += f". {msg_extra}"
|
||||||
else:
|
else:
|
||||||
msg = msg_extra
|
msg = msg_extra
|
||||||
|
|
||||||
return "Missing {}{}{}{}".format(
|
msg = f" {msg}" if msg else ""
|
||||||
param_type,
|
|
||||||
" {}".format(param_hint) if param_hint else "",
|
|
||||||
". " if msg else ".",
|
|
||||||
msg or "",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
# Translate param_type for known types.
|
||||||
if self.message is None:
|
if param_type == "argument":
|
||||||
|
missing = _("Missing argument")
|
||||||
|
elif param_type == "option":
|
||||||
|
missing = _("Missing option")
|
||||||
|
elif param_type == "parameter":
|
||||||
|
missing = _("Missing parameter")
|
||||||
|
else:
|
||||||
|
missing = _("Missing {param_type}").format(param_type=param_type)
|
||||||
|
|
||||||
|
return f"{missing}{param_hint}.{msg}"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if not self.message:
|
||||||
param_name = self.param.name if self.param else None
|
param_name = self.param.name if self.param else None
|
||||||
return "missing parameter: {}".format(param_name)
|
return _("Missing parameter: {param_name}").format(param_name=param_name)
|
||||||
else:
|
else:
|
||||||
return self.message
|
return self.message
|
||||||
|
|
||||||
if PY2:
|
|
||||||
__unicode__ = __str__
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.__unicode__().encode("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
class NoSuchOption(UsageError):
|
class NoSuchOption(UsageError):
|
||||||
"""Raised if click attempted to handle an option that does not
|
"""Raised if click attempted to handle an option that does not
|
||||||
|
@ -176,22 +200,31 @@ class NoSuchOption(UsageError):
|
||||||
.. versionadded:: 4.0
|
.. versionadded:: 4.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, option_name, message=None, possibilities=None, ctx=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
option_name: str,
|
||||||
|
message: t.Optional[str] = None,
|
||||||
|
possibilities: t.Optional[t.Sequence[str]] = None,
|
||||||
|
ctx: t.Optional["Context"] = None,
|
||||||
|
) -> None:
|
||||||
if message is None:
|
if message is None:
|
||||||
message = "no such option: {}".format(option_name)
|
message = _("No such option: {name}").format(name=option_name)
|
||||||
UsageError.__init__(self, message, ctx)
|
|
||||||
|
super().__init__(message, ctx)
|
||||||
self.option_name = option_name
|
self.option_name = option_name
|
||||||
self.possibilities = possibilities
|
self.possibilities = possibilities
|
||||||
|
|
||||||
def format_message(self):
|
def format_message(self) -> str:
|
||||||
bits = [self.message]
|
if not self.possibilities:
|
||||||
if self.possibilities:
|
return self.message
|
||||||
if len(self.possibilities) == 1:
|
|
||||||
bits.append("Did you mean {}?".format(self.possibilities[0]))
|
possibility_str = ", ".join(sorted(self.possibilities))
|
||||||
else:
|
suggest = ngettext(
|
||||||
possibilities = sorted(self.possibilities)
|
"Did you mean {possibility}?",
|
||||||
bits.append("(Possible options: {})".format(", ".join(possibilities)))
|
"(Possible options: {possibilities})",
|
||||||
return " ".join(bits)
|
len(self.possibilities),
|
||||||
|
).format(possibility=possibility_str, possibilities=possibility_str)
|
||||||
|
return f"{self.message} {suggest}"
|
||||||
|
|
||||||
|
|
||||||
class BadOptionUsage(UsageError):
|
class BadOptionUsage(UsageError):
|
||||||
|
@ -204,8 +237,10 @@ class BadOptionUsage(UsageError):
|
||||||
:param option_name: the name of the option being used incorrectly.
|
:param option_name: the name of the option being used incorrectly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, option_name, message, ctx=None):
|
def __init__(
|
||||||
UsageError.__init__(self, message, ctx)
|
self, option_name: str, message: str, ctx: t.Optional["Context"] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(message, ctx)
|
||||||
self.option_name = option_name
|
self.option_name = option_name
|
||||||
|
|
||||||
|
|
||||||
|
@ -217,23 +252,22 @@ class BadArgumentUsage(UsageError):
|
||||||
.. versionadded:: 6.0
|
.. versionadded:: 6.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, message, ctx=None):
|
|
||||||
UsageError.__init__(self, message, ctx)
|
|
||||||
|
|
||||||
|
|
||||||
class FileError(ClickException):
|
class FileError(ClickException):
|
||||||
"""Raised if a file cannot be opened."""
|
"""Raised if a file cannot be opened."""
|
||||||
|
|
||||||
def __init__(self, filename, hint=None):
|
def __init__(self, filename: str, hint: t.Optional[str] = None) -> None:
|
||||||
ui_filename = filename_to_ui(filename)
|
|
||||||
if hint is None:
|
if hint is None:
|
||||||
hint = "unknown error"
|
hint = _("unknown error")
|
||||||
ClickException.__init__(self, hint)
|
|
||||||
self.ui_filename = ui_filename
|
super().__init__(hint)
|
||||||
|
self.ui_filename = os.fsdecode(filename)
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
|
|
||||||
def format_message(self):
|
def format_message(self) -> str:
|
||||||
return "Could not open file {}: {}".format(self.ui_filename, self.message)
|
return _("Could not open file {filename!r}: {message}").format(
|
||||||
|
filename=self.ui_filename, message=self.message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Abort(RuntimeError):
|
class Abort(RuntimeError):
|
||||||
|
@ -249,5 +283,5 @@ class Exit(RuntimeError):
|
||||||
|
|
||||||
__slots__ = ("exit_code",)
|
__slots__ = ("exit_code",)
|
||||||
|
|
||||||
def __init__(self, code=0):
|
def __init__(self, code: int = 0) -> None:
|
||||||
self.exit_code = code
|
self.exit_code = code
|
||||||
|
|
|
@ -1,30 +1,38 @@
|
||||||
|
import typing as t
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
from ._compat import term_len
|
from ._compat import term_len
|
||||||
from .parser import split_opt
|
from .parser import split_opt
|
||||||
from .termui import get_terminal_size
|
|
||||||
|
|
||||||
# Can force a width. This is used by the test system
|
# Can force a width. This is used by the test system
|
||||||
FORCED_WIDTH = None
|
FORCED_WIDTH: t.Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
def measure_table(rows):
|
def measure_table(rows: t.Iterable[t.Tuple[str, str]]) -> t.Tuple[int, ...]:
|
||||||
widths = {}
|
widths: t.Dict[int, int] = {}
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
for idx, col in enumerate(row):
|
for idx, col in enumerate(row):
|
||||||
widths[idx] = max(widths.get(idx, 0), term_len(col))
|
widths[idx] = max(widths.get(idx, 0), term_len(col))
|
||||||
|
|
||||||
return tuple(y for x, y in sorted(widths.items()))
|
return tuple(y for x, y in sorted(widths.items()))
|
||||||
|
|
||||||
|
|
||||||
def iter_rows(rows, col_count):
|
def iter_rows(
|
||||||
|
rows: t.Iterable[t.Tuple[str, str]], col_count: int
|
||||||
|
) -> t.Iterator[t.Tuple[str, ...]]:
|
||||||
for row in rows:
|
for row in rows:
|
||||||
row = tuple(row)
|
|
||||||
yield row + ("",) * (col_count - len(row))
|
yield row + ("",) * (col_count - len(row))
|
||||||
|
|
||||||
|
|
||||||
def wrap_text(
|
def wrap_text(
|
||||||
text, width=78, initial_indent="", subsequent_indent="", preserve_paragraphs=False
|
text: str,
|
||||||
):
|
width: int = 78,
|
||||||
|
initial_indent: str = "",
|
||||||
|
subsequent_indent: str = "",
|
||||||
|
preserve_paragraphs: bool = False,
|
||||||
|
) -> str:
|
||||||
"""A helper function that intelligently wraps text. By default, it
|
"""A helper function that intelligently wraps text. By default, it
|
||||||
assumes that it operates on a single paragraph of text but if the
|
assumes that it operates on a single paragraph of text but if the
|
||||||
`preserve_paragraphs` parameter is provided it will intelligently
|
`preserve_paragraphs` parameter is provided it will intelligently
|
||||||
|
@ -55,11 +63,11 @@ def wrap_text(
|
||||||
if not preserve_paragraphs:
|
if not preserve_paragraphs:
|
||||||
return wrapper.fill(text)
|
return wrapper.fill(text)
|
||||||
|
|
||||||
p = []
|
p: t.List[t.Tuple[int, bool, str]] = []
|
||||||
buf = []
|
buf: t.List[str] = []
|
||||||
indent = None
|
indent = None
|
||||||
|
|
||||||
def _flush_par():
|
def _flush_par() -> None:
|
||||||
if not buf:
|
if not buf:
|
||||||
return
|
return
|
||||||
if buf[0].strip() == "\b":
|
if buf[0].strip() == "\b":
|
||||||
|
@ -91,7 +99,7 @@ def wrap_text(
|
||||||
return "\n\n".join(rv)
|
return "\n\n".join(rv)
|
||||||
|
|
||||||
|
|
||||||
class HelpFormatter(object):
|
class HelpFormatter:
|
||||||
"""This class helps with formatting text-based help pages. It's
|
"""This class helps with formatting text-based help pages. It's
|
||||||
usually just needed for very special internal cases, but it's also
|
usually just needed for very special internal cases, but it's also
|
||||||
exposed so that developers can write their own fancy outputs.
|
exposed so that developers can write their own fancy outputs.
|
||||||
|
@ -103,38 +111,51 @@ class HelpFormatter(object):
|
||||||
width clamped to a maximum of 78.
|
width clamped to a maximum of 78.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, indent_increment=2, width=None, max_width=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
indent_increment: int = 2,
|
||||||
|
width: t.Optional[int] = None,
|
||||||
|
max_width: t.Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
import shutil
|
||||||
|
|
||||||
self.indent_increment = indent_increment
|
self.indent_increment = indent_increment
|
||||||
if max_width is None:
|
if max_width is None:
|
||||||
max_width = 80
|
max_width = 80
|
||||||
if width is None:
|
if width is None:
|
||||||
width = FORCED_WIDTH
|
width = FORCED_WIDTH
|
||||||
if width is None:
|
if width is None:
|
||||||
width = max(min(get_terminal_size()[0], max_width) - 2, 50)
|
width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50)
|
||||||
self.width = width
|
self.width = width
|
||||||
self.current_indent = 0
|
self.current_indent = 0
|
||||||
self.buffer = []
|
self.buffer: t.List[str] = []
|
||||||
|
|
||||||
def write(self, string):
|
def write(self, string: str) -> None:
|
||||||
"""Writes a unicode string into the internal buffer."""
|
"""Writes a unicode string into the internal buffer."""
|
||||||
self.buffer.append(string)
|
self.buffer.append(string)
|
||||||
|
|
||||||
def indent(self):
|
def indent(self) -> None:
|
||||||
"""Increases the indentation."""
|
"""Increases the indentation."""
|
||||||
self.current_indent += self.indent_increment
|
self.current_indent += self.indent_increment
|
||||||
|
|
||||||
def dedent(self):
|
def dedent(self) -> None:
|
||||||
"""Decreases the indentation."""
|
"""Decreases the indentation."""
|
||||||
self.current_indent -= self.indent_increment
|
self.current_indent -= self.indent_increment
|
||||||
|
|
||||||
def write_usage(self, prog, args="", prefix="Usage: "):
|
def write_usage(
|
||||||
|
self, prog: str, args: str = "", prefix: t.Optional[str] = None
|
||||||
|
) -> None:
|
||||||
"""Writes a usage line into the buffer.
|
"""Writes a usage line into the buffer.
|
||||||
|
|
||||||
:param prog: the program name.
|
:param prog: the program name.
|
||||||
:param args: whitespace separated list of arguments.
|
:param args: whitespace separated list of arguments.
|
||||||
:param prefix: the prefix for the first line.
|
:param prefix: The prefix for the first line. Defaults to
|
||||||
|
``"Usage: "``.
|
||||||
"""
|
"""
|
||||||
usage_prefix = "{:>{w}}{} ".format(prefix, prog, w=self.current_indent)
|
if prefix is None:
|
||||||
|
prefix = f"{_('Usage:')} "
|
||||||
|
|
||||||
|
usage_prefix = f"{prefix:>{self.current_indent}}{prog} "
|
||||||
text_width = self.width - self.current_indent
|
text_width = self.width - self.current_indent
|
||||||
|
|
||||||
if text_width >= (term_len(usage_prefix) + 20):
|
if text_width >= (term_len(usage_prefix) + 20):
|
||||||
|
@ -161,25 +182,24 @@ class HelpFormatter(object):
|
||||||
|
|
||||||
self.write("\n")
|
self.write("\n")
|
||||||
|
|
||||||
def write_heading(self, heading):
|
def write_heading(self, heading: str) -> None:
|
||||||
"""Writes a heading into the buffer."""
|
"""Writes a heading into the buffer."""
|
||||||
self.write("{:>{w}}{}:\n".format("", heading, w=self.current_indent))
|
self.write(f"{'':>{self.current_indent}}{heading}:\n")
|
||||||
|
|
||||||
def write_paragraph(self):
|
def write_paragraph(self) -> None:
|
||||||
"""Writes a paragraph into the buffer."""
|
"""Writes a paragraph into the buffer."""
|
||||||
if self.buffer:
|
if self.buffer:
|
||||||
self.write("\n")
|
self.write("\n")
|
||||||
|
|
||||||
def write_text(self, text):
|
def write_text(self, text: str) -> None:
|
||||||
"""Writes re-indented text into the buffer. This rewraps and
|
"""Writes re-indented text into the buffer. This rewraps and
|
||||||
preserves paragraphs.
|
preserves paragraphs.
|
||||||
"""
|
"""
|
||||||
text_width = max(self.width - self.current_indent, 11)
|
|
||||||
indent = " " * self.current_indent
|
indent = " " * self.current_indent
|
||||||
self.write(
|
self.write(
|
||||||
wrap_text(
|
wrap_text(
|
||||||
text,
|
text,
|
||||||
text_width,
|
self.width,
|
||||||
initial_indent=indent,
|
initial_indent=indent,
|
||||||
subsequent_indent=indent,
|
subsequent_indent=indent,
|
||||||
preserve_paragraphs=True,
|
preserve_paragraphs=True,
|
||||||
|
@ -187,7 +207,12 @@ class HelpFormatter(object):
|
||||||
)
|
)
|
||||||
self.write("\n")
|
self.write("\n")
|
||||||
|
|
||||||
def write_dl(self, rows, col_max=30, col_spacing=2):
|
def write_dl(
|
||||||
|
self,
|
||||||
|
rows: t.Sequence[t.Tuple[str, str]],
|
||||||
|
col_max: int = 30,
|
||||||
|
col_spacing: int = 2,
|
||||||
|
) -> None:
|
||||||
"""Writes a definition list into the buffer. This is how options
|
"""Writes a definition list into the buffer. This is how options
|
||||||
and commands are usually formatted.
|
and commands are usually formatted.
|
||||||
|
|
||||||
|
@ -204,7 +229,7 @@ class HelpFormatter(object):
|
||||||
first_col = min(widths[0], col_max) + col_spacing
|
first_col = min(widths[0], col_max) + col_spacing
|
||||||
|
|
||||||
for first, second in iter_rows(rows, len(widths)):
|
for first, second in iter_rows(rows, len(widths)):
|
||||||
self.write("{:>{w}}{}".format("", first, w=self.current_indent))
|
self.write(f"{'':>{self.current_indent}}{first}")
|
||||||
if not second:
|
if not second:
|
||||||
self.write("\n")
|
self.write("\n")
|
||||||
continue
|
continue
|
||||||
|
@ -219,23 +244,15 @@ class HelpFormatter(object):
|
||||||
lines = wrapped_text.splitlines()
|
lines = wrapped_text.splitlines()
|
||||||
|
|
||||||
if lines:
|
if lines:
|
||||||
self.write("{}\n".format(lines[0]))
|
self.write(f"{lines[0]}\n")
|
||||||
|
|
||||||
for line in lines[1:]:
|
for line in lines[1:]:
|
||||||
self.write(
|
self.write(f"{'':>{first_col + self.current_indent}}{line}\n")
|
||||||
"{:>{w}}{}\n".format(
|
|
||||||
"", line, w=first_col + self.current_indent
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(lines) > 1:
|
|
||||||
# separate long help from next option
|
|
||||||
self.write("\n")
|
|
||||||
else:
|
else:
|
||||||
self.write("\n")
|
self.write("\n")
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def section(self, name):
|
def section(self, name: str) -> t.Iterator[None]:
|
||||||
"""Helpful context manager that writes a paragraph, a heading,
|
"""Helpful context manager that writes a paragraph, a heading,
|
||||||
and the indents.
|
and the indents.
|
||||||
|
|
||||||
|
@ -250,7 +267,7 @@ class HelpFormatter(object):
|
||||||
self.dedent()
|
self.dedent()
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def indentation(self):
|
def indentation(self) -> t.Iterator[None]:
|
||||||
"""A context manager that increases the indentation."""
|
"""A context manager that increases the indentation."""
|
||||||
self.indent()
|
self.indent()
|
||||||
try:
|
try:
|
||||||
|
@ -258,12 +275,12 @@ class HelpFormatter(object):
|
||||||
finally:
|
finally:
|
||||||
self.dedent()
|
self.dedent()
|
||||||
|
|
||||||
def getvalue(self):
|
def getvalue(self) -> str:
|
||||||
"""Returns the buffer contents."""
|
"""Returns the buffer contents."""
|
||||||
return "".join(self.buffer)
|
return "".join(self.buffer)
|
||||||
|
|
||||||
|
|
||||||
def join_options(options):
|
def join_options(options: t.Sequence[str]) -> t.Tuple[str, bool]:
|
||||||
"""Given a list of option strings this joins them in the most appropriate
|
"""Given a list of option strings this joins them in the most appropriate
|
||||||
way and returns them in the form ``(formatted_string,
|
way and returns them in the form ``(formatted_string,
|
||||||
any_prefix_is_slash)`` where the second item in the tuple is a flag that
|
any_prefix_is_slash)`` where the second item in the tuple is a flag that
|
||||||
|
@ -271,13 +288,14 @@ def join_options(options):
|
||||||
"""
|
"""
|
||||||
rv = []
|
rv = []
|
||||||
any_prefix_is_slash = False
|
any_prefix_is_slash = False
|
||||||
|
|
||||||
for opt in options:
|
for opt in options:
|
||||||
prefix = split_opt(opt)[0]
|
prefix = split_opt(opt)[0]
|
||||||
|
|
||||||
if prefix == "/":
|
if prefix == "/":
|
||||||
any_prefix_is_slash = True
|
any_prefix_is_slash = True
|
||||||
|
|
||||||
rv.append((len(prefix), opt))
|
rv.append((len(prefix), opt))
|
||||||
|
|
||||||
rv.sort(key=lambda x: x[0])
|
rv.sort(key=lambda x: x[0])
|
||||||
|
return ", ".join(x[1] for x in rv), any_prefix_is_slash
|
||||||
rv = ", ".join(x[1] for x in rv)
|
|
||||||
return rv, any_prefix_is_slash
|
|
||||||
|
|
|
@ -1,9 +1,25 @@
|
||||||
|
import typing
|
||||||
|
import typing as t
|
||||||
from threading import local
|
from threading import local
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .core import Context
|
||||||
|
|
||||||
_local = local()
|
_local = local()
|
||||||
|
|
||||||
|
|
||||||
def get_current_context(silent=False):
|
@typing.overload
|
||||||
|
def get_current_context(silent: "te.Literal[False]" = False) -> "Context":
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@typing.overload
|
||||||
|
def get_current_context(silent: bool = ...) -> t.Optional["Context"]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_context(silent: bool = False) -> t.Optional["Context"]:
|
||||||
"""Returns the current click context. This can be used as a way to
|
"""Returns the current click context. This can be used as a way to
|
||||||
access the current context object from anywhere. This is a more implicit
|
access the current context object from anywhere. This is a more implicit
|
||||||
alternative to the :func:`pass_context` decorator. This function is
|
alternative to the :func:`pass_context` decorator. This function is
|
||||||
|
@ -19,29 +35,35 @@ def get_current_context(silent=False):
|
||||||
:exc:`RuntimeError`.
|
:exc:`RuntimeError`.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return _local.stack[-1]
|
return t.cast("Context", _local.stack[-1])
|
||||||
except (AttributeError, IndexError):
|
except (AttributeError, IndexError) as e:
|
||||||
if not silent:
|
if not silent:
|
||||||
raise RuntimeError("There is no active click context.")
|
raise RuntimeError("There is no active click context.") from e
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def push_context(ctx):
|
def push_context(ctx: "Context") -> None:
|
||||||
"""Pushes a new context to the current stack."""
|
"""Pushes a new context to the current stack."""
|
||||||
_local.__dict__.setdefault("stack", []).append(ctx)
|
_local.__dict__.setdefault("stack", []).append(ctx)
|
||||||
|
|
||||||
|
|
||||||
def pop_context():
|
def pop_context() -> None:
|
||||||
"""Removes the top level from the stack."""
|
"""Removes the top level from the stack."""
|
||||||
_local.stack.pop()
|
_local.stack.pop()
|
||||||
|
|
||||||
|
|
||||||
def resolve_color_default(color=None):
|
def resolve_color_default(color: t.Optional[bool] = None) -> t.Optional[bool]:
|
||||||
""""Internal helper to get the default value of the color flag. If a
|
"""Internal helper to get the default value of the color flag. If a
|
||||||
value is passed it's returned unchanged, otherwise it's looked up from
|
value is passed it's returned unchanged, otherwise it's looked up from
|
||||||
the current context.
|
the current context.
|
||||||
"""
|
"""
|
||||||
if color is not None:
|
if color is not None:
|
||||||
return color
|
return color
|
||||||
|
|
||||||
ctx = get_current_context(silent=True)
|
ctx = get_current_context(silent=True)
|
||||||
|
|
||||||
if ctx is not None:
|
if ctx is not None:
|
||||||
return ctx.color
|
return ctx.color
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
"""
|
||||||
This module started out as largely a copy paste from the stdlib's
|
This module started out as largely a copy paste from the stdlib's
|
||||||
optparse module with the features removed that we do not need from
|
optparse module with the features removed that we do not need from
|
||||||
|
@ -18,16 +17,38 @@ by the Python Software Foundation. This is limited to code in parser.py.
|
||||||
Copyright 2001-2006 Gregory P. Ward. All rights reserved.
|
Copyright 2001-2006 Gregory P. Ward. All rights reserved.
|
||||||
Copyright 2002-2006 Python Software Foundation. All rights reserved.
|
Copyright 2002-2006 Python Software Foundation. All rights reserved.
|
||||||
"""
|
"""
|
||||||
import re
|
# This code uses parts of optparse written by Gregory P. Ward and
|
||||||
|
# maintained by the Python Software Foundation.
|
||||||
|
# Copyright 2001-2006 Gregory P. Ward
|
||||||
|
# Copyright 2002-2006 Python Software Foundation
|
||||||
|
import typing as t
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from gettext import gettext as _
|
||||||
|
from gettext import ngettext
|
||||||
|
|
||||||
from .exceptions import BadArgumentUsage
|
from .exceptions import BadArgumentUsage
|
||||||
from .exceptions import BadOptionUsage
|
from .exceptions import BadOptionUsage
|
||||||
from .exceptions import NoSuchOption
|
from .exceptions import NoSuchOption
|
||||||
from .exceptions import UsageError
|
from .exceptions import UsageError
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .core import Argument as CoreArgument
|
||||||
|
from .core import Context
|
||||||
|
from .core import Option as CoreOption
|
||||||
|
from .core import Parameter as CoreParameter
|
||||||
|
|
||||||
def _unpack_args(args, nargs_spec):
|
V = t.TypeVar("V")
|
||||||
|
|
||||||
|
# Sentinel value that indicates an option was passed as a flag without a
|
||||||
|
# value but is not a flag option. Option.consume_value uses this to
|
||||||
|
# prompt or use the flag_value.
|
||||||
|
_flag_needs_value = object()
|
||||||
|
|
||||||
|
|
||||||
|
def _unpack_args(
|
||||||
|
args: t.Sequence[str], nargs_spec: t.Sequence[int]
|
||||||
|
) -> t.Tuple[t.Sequence[t.Union[str, t.Sequence[t.Optional[str]], None]], t.List[str]]:
|
||||||
"""Given an iterable of arguments and an iterable of nargs specifications,
|
"""Given an iterable of arguments and an iterable of nargs specifications,
|
||||||
it returns a tuple with all the unpacked arguments at the first index
|
it returns a tuple with all the unpacked arguments at the first index
|
||||||
and all remaining arguments as the second.
|
and all remaining arguments as the second.
|
||||||
|
@ -39,10 +60,10 @@ def _unpack_args(args, nargs_spec):
|
||||||
"""
|
"""
|
||||||
args = deque(args)
|
args = deque(args)
|
||||||
nargs_spec = deque(nargs_spec)
|
nargs_spec = deque(nargs_spec)
|
||||||
rv = []
|
rv: t.List[t.Union[str, t.Tuple[t.Optional[str], ...], None]] = []
|
||||||
spos = None
|
spos: t.Optional[int] = None
|
||||||
|
|
||||||
def _fetch(c):
|
def _fetch(c: "te.Deque[V]") -> t.Optional[V]:
|
||||||
try:
|
try:
|
||||||
if spos is None:
|
if spos is None:
|
||||||
return c.popleft()
|
return c.popleft()
|
||||||
|
@ -53,18 +74,25 @@ def _unpack_args(args, nargs_spec):
|
||||||
|
|
||||||
while nargs_spec:
|
while nargs_spec:
|
||||||
nargs = _fetch(nargs_spec)
|
nargs = _fetch(nargs_spec)
|
||||||
|
|
||||||
|
if nargs is None:
|
||||||
|
continue
|
||||||
|
|
||||||
if nargs == 1:
|
if nargs == 1:
|
||||||
rv.append(_fetch(args))
|
rv.append(_fetch(args))
|
||||||
elif nargs > 1:
|
elif nargs > 1:
|
||||||
x = [_fetch(args) for _ in range(nargs)]
|
x = [_fetch(args) for _ in range(nargs)]
|
||||||
|
|
||||||
# If we're reversed, we're pulling in the arguments in reverse,
|
# If we're reversed, we're pulling in the arguments in reverse,
|
||||||
# so we need to turn them around.
|
# so we need to turn them around.
|
||||||
if spos is not None:
|
if spos is not None:
|
||||||
x.reverse()
|
x.reverse()
|
||||||
|
|
||||||
rv.append(tuple(x))
|
rv.append(tuple(x))
|
||||||
elif nargs < 0:
|
elif nargs < 0:
|
||||||
if spos is not None:
|
if spos is not None:
|
||||||
raise TypeError("Cannot have two nargs < 0")
|
raise TypeError("Cannot have two nargs < 0")
|
||||||
|
|
||||||
spos = len(rv)
|
spos = len(rv)
|
||||||
rv.append(None)
|
rv.append(None)
|
||||||
|
|
||||||
|
@ -78,13 +106,7 @@ def _unpack_args(args, nargs_spec):
|
||||||
return tuple(rv), list(args)
|
return tuple(rv), list(args)
|
||||||
|
|
||||||
|
|
||||||
def _error_opt_args(nargs, opt):
|
def split_opt(opt: str) -> t.Tuple[str, str]:
|
||||||
if nargs == 1:
|
|
||||||
raise BadOptionUsage(opt, "{} option requires an argument".format(opt))
|
|
||||||
raise BadOptionUsage(opt, "{} option requires {} arguments".format(opt, nargs))
|
|
||||||
|
|
||||||
|
|
||||||
def split_opt(opt):
|
|
||||||
first = opt[:1]
|
first = opt[:1]
|
||||||
if first.isalnum():
|
if first.isalnum():
|
||||||
return "", opt
|
return "", opt
|
||||||
|
@ -93,34 +115,57 @@ def split_opt(opt):
|
||||||
return first, opt[1:]
|
return first, opt[1:]
|
||||||
|
|
||||||
|
|
||||||
def normalize_opt(opt, ctx):
|
def normalize_opt(opt: str, ctx: t.Optional["Context"]) -> str:
|
||||||
if ctx is None or ctx.token_normalize_func is None:
|
if ctx is None or ctx.token_normalize_func is None:
|
||||||
return opt
|
return opt
|
||||||
prefix, opt = split_opt(opt)
|
prefix, opt = split_opt(opt)
|
||||||
return prefix + ctx.token_normalize_func(opt)
|
return f"{prefix}{ctx.token_normalize_func(opt)}"
|
||||||
|
|
||||||
|
|
||||||
def split_arg_string(string):
|
def split_arg_string(string: str) -> t.List[str]:
|
||||||
"""Given an argument string this attempts to split it into small parts."""
|
"""Split an argument string as with :func:`shlex.split`, but don't
|
||||||
rv = []
|
fail if the string is incomplete. Ignores a missing closing quote or
|
||||||
for match in re.finditer(
|
incomplete escape sequence and uses the partial token as-is.
|
||||||
r"('([^'\\]*(?:\\.[^'\\]*)*)'|\"([^\"\\]*(?:\\.[^\"\\]*)*)\"|\S+)\s*",
|
|
||||||
string,
|
.. code-block:: python
|
||||||
re.S,
|
|
||||||
|
split_arg_string("example 'my file")
|
||||||
|
["example", "my file"]
|
||||||
|
|
||||||
|
split_arg_string("example my\\")
|
||||||
|
["example", "my"]
|
||||||
|
|
||||||
|
:param string: String to split.
|
||||||
|
"""
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
lex = shlex.shlex(string, posix=True)
|
||||||
|
lex.whitespace_split = True
|
||||||
|
lex.commenters = ""
|
||||||
|
out = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for token in lex:
|
||||||
|
out.append(token)
|
||||||
|
except ValueError:
|
||||||
|
# Raised when end-of-string is reached in an invalid state. Use
|
||||||
|
# the partial token as-is. The quote or escape character is in
|
||||||
|
# lex.state, not lex.token.
|
||||||
|
out.append(lex.token)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class Option:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
obj: "CoreOption",
|
||||||
|
opts: t.Sequence[str],
|
||||||
|
dest: t.Optional[str],
|
||||||
|
action: t.Optional[str] = None,
|
||||||
|
nargs: int = 1,
|
||||||
|
const: t.Optional[t.Any] = None,
|
||||||
):
|
):
|
||||||
arg = match.group().strip()
|
|
||||||
if arg[:1] == arg[-1:] and arg[:1] in "\"'":
|
|
||||||
arg = arg[1:-1].encode("ascii", "backslashreplace").decode("unicode-escape")
|
|
||||||
try:
|
|
||||||
arg = type(string)(arg)
|
|
||||||
except UnicodeError:
|
|
||||||
pass
|
|
||||||
rv.append(arg)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
class Option(object):
|
|
||||||
def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None):
|
|
||||||
self._short_opts = []
|
self._short_opts = []
|
||||||
self._long_opts = []
|
self._long_opts = []
|
||||||
self.prefixes = set()
|
self.prefixes = set()
|
||||||
|
@ -128,7 +173,7 @@ class Option(object):
|
||||||
for opt in opts:
|
for opt in opts:
|
||||||
prefix, value = split_opt(opt)
|
prefix, value = split_opt(opt)
|
||||||
if not prefix:
|
if not prefix:
|
||||||
raise ValueError("Invalid start character for option ({})".format(opt))
|
raise ValueError(f"Invalid start character for option ({opt})")
|
||||||
self.prefixes.add(prefix[0])
|
self.prefixes.add(prefix[0])
|
||||||
if len(prefix) == 1 and len(value) == 1:
|
if len(prefix) == 1 and len(value) == 1:
|
||||||
self._short_opts.append(opt)
|
self._short_opts.append(opt)
|
||||||
|
@ -146,53 +191,66 @@ class Option(object):
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def takes_value(self):
|
def takes_value(self) -> bool:
|
||||||
return self.action in ("store", "append")
|
return self.action in ("store", "append")
|
||||||
|
|
||||||
def process(self, value, state):
|
def process(self, value: str, state: "ParsingState") -> None:
|
||||||
if self.action == "store":
|
if self.action == "store":
|
||||||
state.opts[self.dest] = value
|
state.opts[self.dest] = value # type: ignore
|
||||||
elif self.action == "store_const":
|
elif self.action == "store_const":
|
||||||
state.opts[self.dest] = self.const
|
state.opts[self.dest] = self.const # type: ignore
|
||||||
elif self.action == "append":
|
elif self.action == "append":
|
||||||
state.opts.setdefault(self.dest, []).append(value)
|
state.opts.setdefault(self.dest, []).append(value) # type: ignore
|
||||||
elif self.action == "append_const":
|
elif self.action == "append_const":
|
||||||
state.opts.setdefault(self.dest, []).append(self.const)
|
state.opts.setdefault(self.dest, []).append(self.const) # type: ignore
|
||||||
elif self.action == "count":
|
elif self.action == "count":
|
||||||
state.opts[self.dest] = state.opts.get(self.dest, 0) + 1
|
state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore
|
||||||
else:
|
else:
|
||||||
raise ValueError("unknown action '{}'".format(self.action))
|
raise ValueError(f"unknown action '{self.action}'")
|
||||||
state.order.append(self.obj)
|
state.order.append(self.obj)
|
||||||
|
|
||||||
|
|
||||||
class Argument(object):
|
class Argument:
|
||||||
def __init__(self, dest, nargs=1, obj=None):
|
def __init__(self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1):
|
||||||
self.dest = dest
|
self.dest = dest
|
||||||
self.nargs = nargs
|
self.nargs = nargs
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
|
|
||||||
def process(self, value, state):
|
def process(
|
||||||
|
self,
|
||||||
|
value: t.Union[t.Optional[str], t.Sequence[t.Optional[str]]],
|
||||||
|
state: "ParsingState",
|
||||||
|
) -> None:
|
||||||
if self.nargs > 1:
|
if self.nargs > 1:
|
||||||
|
assert value is not None
|
||||||
holes = sum(1 for x in value if x is None)
|
holes = sum(1 for x in value if x is None)
|
||||||
if holes == len(value):
|
if holes == len(value):
|
||||||
value = None
|
value = None
|
||||||
elif holes != 0:
|
elif holes != 0:
|
||||||
raise BadArgumentUsage(
|
raise BadArgumentUsage(
|
||||||
"argument {} takes {} values".format(self.dest, self.nargs)
|
_("Argument {name!r} takes {nargs} values.").format(
|
||||||
|
name=self.dest, nargs=self.nargs
|
||||||
|
)
|
||||||
)
|
)
|
||||||
state.opts[self.dest] = value
|
|
||||||
|
if self.nargs == -1 and self.obj.envvar is not None and value == ():
|
||||||
|
# Replace empty tuple with None so that a value from the
|
||||||
|
# environment may be tried.
|
||||||
|
value = None
|
||||||
|
|
||||||
|
state.opts[self.dest] = value # type: ignore
|
||||||
state.order.append(self.obj)
|
state.order.append(self.obj)
|
||||||
|
|
||||||
|
|
||||||
class ParsingState(object):
|
class ParsingState:
|
||||||
def __init__(self, rargs):
|
def __init__(self, rargs: t.List[str]) -> None:
|
||||||
self.opts = {}
|
self.opts: t.Dict[str, t.Any] = {}
|
||||||
self.largs = []
|
self.largs: t.List[str] = []
|
||||||
self.rargs = rargs
|
self.rargs = rargs
|
||||||
self.order = []
|
self.order: t.List["CoreParameter"] = []
|
||||||
|
|
||||||
|
|
||||||
class OptionParser(object):
|
class OptionParser:
|
||||||
"""The option parser is an internal class that is ultimately used to
|
"""The option parser is an internal class that is ultimately used to
|
||||||
parse options and arguments. It's modelled after optparse and brings
|
parse options and arguments. It's modelled after optparse and brings
|
||||||
a similar but vastly simplified API. It should generally not be used
|
a similar but vastly simplified API. It should generally not be used
|
||||||
|
@ -206,7 +264,7 @@ class OptionParser(object):
|
||||||
should go with.
|
should go with.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ctx=None):
|
def __init__(self, ctx: t.Optional["Context"] = None) -> None:
|
||||||
#: The :class:`~click.Context` for this parser. This might be
|
#: The :class:`~click.Context` for this parser. This might be
|
||||||
#: `None` for some advanced use cases.
|
#: `None` for some advanced use cases.
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
|
@ -220,44 +278,54 @@ class OptionParser(object):
|
||||||
#: second mode where it will ignore it and continue processing
|
#: second mode where it will ignore it and continue processing
|
||||||
#: after shifting all the unknown options into the resulting args.
|
#: after shifting all the unknown options into the resulting args.
|
||||||
self.ignore_unknown_options = False
|
self.ignore_unknown_options = False
|
||||||
|
|
||||||
if ctx is not None:
|
if ctx is not None:
|
||||||
self.allow_interspersed_args = ctx.allow_interspersed_args
|
self.allow_interspersed_args = ctx.allow_interspersed_args
|
||||||
self.ignore_unknown_options = ctx.ignore_unknown_options
|
self.ignore_unknown_options = ctx.ignore_unknown_options
|
||||||
self._short_opt = {}
|
|
||||||
self._long_opt = {}
|
|
||||||
self._opt_prefixes = {"-", "--"}
|
|
||||||
self._args = []
|
|
||||||
|
|
||||||
def add_option(self, opts, dest, action=None, nargs=1, const=None, obj=None):
|
self._short_opt: t.Dict[str, Option] = {}
|
||||||
|
self._long_opt: t.Dict[str, Option] = {}
|
||||||
|
self._opt_prefixes = {"-", "--"}
|
||||||
|
self._args: t.List[Argument] = []
|
||||||
|
|
||||||
|
def add_option(
|
||||||
|
self,
|
||||||
|
obj: "CoreOption",
|
||||||
|
opts: t.Sequence[str],
|
||||||
|
dest: t.Optional[str],
|
||||||
|
action: t.Optional[str] = None,
|
||||||
|
nargs: int = 1,
|
||||||
|
const: t.Optional[t.Any] = None,
|
||||||
|
) -> None:
|
||||||
"""Adds a new option named `dest` to the parser. The destination
|
"""Adds a new option named `dest` to the parser. The destination
|
||||||
is not inferred (unlike with optparse) and needs to be explicitly
|
is not inferred (unlike with optparse) and needs to be explicitly
|
||||||
provided. Action can be any of ``store``, ``store_const``,
|
provided. Action can be any of ``store``, ``store_const``,
|
||||||
``append``, ``appnd_const`` or ``count``.
|
``append``, ``append_const`` or ``count``.
|
||||||
|
|
||||||
The `obj` can be used to identify the option in the order list
|
The `obj` can be used to identify the option in the order list
|
||||||
that is returned from the parser.
|
that is returned from the parser.
|
||||||
"""
|
"""
|
||||||
if obj is None:
|
|
||||||
obj = dest
|
|
||||||
opts = [normalize_opt(opt, self.ctx) for opt in opts]
|
opts = [normalize_opt(opt, self.ctx) for opt in opts]
|
||||||
option = Option(opts, dest, action=action, nargs=nargs, const=const, obj=obj)
|
option = Option(obj, opts, dest, action=action, nargs=nargs, const=const)
|
||||||
self._opt_prefixes.update(option.prefixes)
|
self._opt_prefixes.update(option.prefixes)
|
||||||
for opt in option._short_opts:
|
for opt in option._short_opts:
|
||||||
self._short_opt[opt] = option
|
self._short_opt[opt] = option
|
||||||
for opt in option._long_opts:
|
for opt in option._long_opts:
|
||||||
self._long_opt[opt] = option
|
self._long_opt[opt] = option
|
||||||
|
|
||||||
def add_argument(self, dest, nargs=1, obj=None):
|
def add_argument(
|
||||||
|
self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1
|
||||||
|
) -> None:
|
||||||
"""Adds a positional argument named `dest` to the parser.
|
"""Adds a positional argument named `dest` to the parser.
|
||||||
|
|
||||||
The `obj` can be used to identify the option in the order list
|
The `obj` can be used to identify the option in the order list
|
||||||
that is returned from the parser.
|
that is returned from the parser.
|
||||||
"""
|
"""
|
||||||
if obj is None:
|
self._args.append(Argument(obj, dest=dest, nargs=nargs))
|
||||||
obj = dest
|
|
||||||
self._args.append(Argument(dest=dest, nargs=nargs, obj=obj))
|
|
||||||
|
|
||||||
def parse_args(self, args):
|
def parse_args(
|
||||||
|
self, args: t.List[str]
|
||||||
|
) -> t.Tuple[t.Dict[str, t.Any], t.List[str], t.List["CoreParameter"]]:
|
||||||
"""Parses positional arguments and returns ``(values, args, order)``
|
"""Parses positional arguments and returns ``(values, args, order)``
|
||||||
for the parsed options and arguments as well as the leftover
|
for the parsed options and arguments as well as the leftover
|
||||||
arguments if there are any. The order is a list of objects as they
|
arguments if there are any. The order is a list of objects as they
|
||||||
|
@ -273,7 +341,7 @@ class OptionParser(object):
|
||||||
raise
|
raise
|
||||||
return state.opts, state.largs, state.order
|
return state.opts, state.largs, state.order
|
||||||
|
|
||||||
def _process_args_for_args(self, state):
|
def _process_args_for_args(self, state: ParsingState) -> None:
|
||||||
pargs, args = _unpack_args(
|
pargs, args = _unpack_args(
|
||||||
state.largs + state.rargs, [x.nargs for x in self._args]
|
state.largs + state.rargs, [x.nargs for x in self._args]
|
||||||
)
|
)
|
||||||
|
@ -284,7 +352,7 @@ class OptionParser(object):
|
||||||
state.largs = args
|
state.largs = args
|
||||||
state.rargs = []
|
state.rargs = []
|
||||||
|
|
||||||
def _process_args_for_options(self, state):
|
def _process_args_for_options(self, state: ParsingState) -> None:
|
||||||
while state.rargs:
|
while state.rargs:
|
||||||
arg = state.rargs.pop(0)
|
arg = state.rargs.pop(0)
|
||||||
arglen = len(arg)
|
arglen = len(arg)
|
||||||
|
@ -320,9 +388,13 @@ class OptionParser(object):
|
||||||
# *empty* -- still a subset of [arg0, ..., arg(i-1)], but
|
# *empty* -- still a subset of [arg0, ..., arg(i-1)], but
|
||||||
# not a very interesting subset!
|
# not a very interesting subset!
|
||||||
|
|
||||||
def _match_long_opt(self, opt, explicit_value, state):
|
def _match_long_opt(
|
||||||
|
self, opt: str, explicit_value: t.Optional[str], state: ParsingState
|
||||||
|
) -> None:
|
||||||
if opt not in self._long_opt:
|
if opt not in self._long_opt:
|
||||||
possibilities = [word for word in self._long_opt if word.startswith(opt)]
|
from difflib import get_close_matches
|
||||||
|
|
||||||
|
possibilities = get_close_matches(opt, self._long_opt)
|
||||||
raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
|
raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
|
||||||
|
|
||||||
option = self._long_opt[opt]
|
option = self._long_opt[opt]
|
||||||
|
@ -334,31 +406,26 @@ class OptionParser(object):
|
||||||
if explicit_value is not None:
|
if explicit_value is not None:
|
||||||
state.rargs.insert(0, explicit_value)
|
state.rargs.insert(0, explicit_value)
|
||||||
|
|
||||||
nargs = option.nargs
|
value = self._get_value_from_state(opt, option, state)
|
||||||
if len(state.rargs) < nargs:
|
|
||||||
_error_opt_args(nargs, opt)
|
|
||||||
elif nargs == 1:
|
|
||||||
value = state.rargs.pop(0)
|
|
||||||
else:
|
|
||||||
value = tuple(state.rargs[:nargs])
|
|
||||||
del state.rargs[:nargs]
|
|
||||||
|
|
||||||
elif explicit_value is not None:
|
elif explicit_value is not None:
|
||||||
raise BadOptionUsage(opt, "{} option does not take a value".format(opt))
|
raise BadOptionUsage(
|
||||||
|
opt, _("Option {name!r} does not take a value.").format(name=opt)
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
value = None
|
value = None
|
||||||
|
|
||||||
option.process(value, state)
|
option.process(value, state)
|
||||||
|
|
||||||
def _match_short_opt(self, arg, state):
|
def _match_short_opt(self, arg: str, state: ParsingState) -> None:
|
||||||
stop = False
|
stop = False
|
||||||
i = 1
|
i = 1
|
||||||
prefix = arg[0]
|
prefix = arg[0]
|
||||||
unknown_options = []
|
unknown_options = []
|
||||||
|
|
||||||
for ch in arg[1:]:
|
for ch in arg[1:]:
|
||||||
opt = normalize_opt(prefix + ch, self.ctx)
|
opt = normalize_opt(f"{prefix}{ch}", self.ctx)
|
||||||
option = self._short_opt.get(opt)
|
option = self._short_opt.get(opt)
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
|
@ -374,14 +441,7 @@ class OptionParser(object):
|
||||||
state.rargs.insert(0, arg[i:])
|
state.rargs.insert(0, arg[i:])
|
||||||
stop = True
|
stop = True
|
||||||
|
|
||||||
nargs = option.nargs
|
value = self._get_value_from_state(opt, option, state)
|
||||||
if len(state.rargs) < nargs:
|
|
||||||
_error_opt_args(nargs, opt)
|
|
||||||
elif nargs == 1:
|
|
||||||
value = state.rargs.pop(0)
|
|
||||||
else:
|
|
||||||
value = tuple(state.rargs[:nargs])
|
|
||||||
del state.rargs[:nargs]
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
value = None
|
value = None
|
||||||
|
@ -396,9 +456,47 @@ class OptionParser(object):
|
||||||
# to the state as new larg. This way there is basic combinatorics
|
# to the state as new larg. This way there is basic combinatorics
|
||||||
# that can be achieved while still ignoring unknown arguments.
|
# that can be achieved while still ignoring unknown arguments.
|
||||||
if self.ignore_unknown_options and unknown_options:
|
if self.ignore_unknown_options and unknown_options:
|
||||||
state.largs.append("{}{}".format(prefix, "".join(unknown_options)))
|
state.largs.append(f"{prefix}{''.join(unknown_options)}")
|
||||||
|
|
||||||
def _process_opts(self, arg, state):
|
def _get_value_from_state(
|
||||||
|
self, option_name: str, option: Option, state: ParsingState
|
||||||
|
) -> t.Any:
|
||||||
|
nargs = option.nargs
|
||||||
|
|
||||||
|
if len(state.rargs) < nargs:
|
||||||
|
if option.obj._flag_needs_value:
|
||||||
|
# Option allows omitting the value.
|
||||||
|
value = _flag_needs_value
|
||||||
|
else:
|
||||||
|
raise BadOptionUsage(
|
||||||
|
option_name,
|
||||||
|
ngettext(
|
||||||
|
"Option {name!r} requires an argument.",
|
||||||
|
"Option {name!r} requires {nargs} arguments.",
|
||||||
|
nargs,
|
||||||
|
).format(name=option_name, nargs=nargs),
|
||||||
|
)
|
||||||
|
elif nargs == 1:
|
||||||
|
next_rarg = state.rargs[0]
|
||||||
|
|
||||||
|
if (
|
||||||
|
option.obj._flag_needs_value
|
||||||
|
and isinstance(next_rarg, str)
|
||||||
|
and next_rarg[:1] in self._opt_prefixes
|
||||||
|
and len(next_rarg) > 1
|
||||||
|
):
|
||||||
|
# The next arg looks like the start of an option, don't
|
||||||
|
# use it as the value if omitting the value is allowed.
|
||||||
|
value = _flag_needs_value
|
||||||
|
else:
|
||||||
|
value = state.rargs.pop(0)
|
||||||
|
else:
|
||||||
|
value = tuple(state.rargs[:nargs])
|
||||||
|
del state.rargs[:nargs]
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _process_opts(self, arg: str, state: ParsingState) -> None:
|
||||||
explicit_value = None
|
explicit_value = None
|
||||||
# Long option handling happens in two parts. The first part is
|
# Long option handling happens in two parts. The first part is
|
||||||
# supporting explicitly attached values. In any case, we will try
|
# supporting explicitly attached values. In any case, we will try
|
||||||
|
@ -422,7 +520,10 @@ class OptionParser(object):
|
||||||
# short option code and will instead raise the no option
|
# short option code and will instead raise the no option
|
||||||
# error.
|
# error.
|
||||||
if arg[:2] not in self._opt_prefixes:
|
if arg[:2] not in self._opt_prefixes:
|
||||||
return self._match_short_opt(arg, state)
|
self._match_short_opt(arg, state)
|
||||||
|
return
|
||||||
|
|
||||||
if not self.ignore_unknown_options:
|
if not self.ignore_unknown_options:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
state.largs.append(arg)
|
state.largs.append(arg)
|
||||||
|
|
0
src/click/py.typed
Normal file
0
src/click/py.typed
Normal file
581
src/click/shell_completion.py
Normal file
581
src/click/shell_completion.py
Normal file
|
@ -0,0 +1,581 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import typing as t
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
|
from .core import Argument
|
||||||
|
from .core import BaseCommand
|
||||||
|
from .core import Context
|
||||||
|
from .core import MultiCommand
|
||||||
|
from .core import Option
|
||||||
|
from .core import Parameter
|
||||||
|
from .core import ParameterSource
|
||||||
|
from .parser import split_arg_string
|
||||||
|
from .utils import echo
|
||||||
|
|
||||||
|
|
||||||
|
def shell_complete(
|
||||||
|
cli: BaseCommand,
|
||||||
|
ctx_args: t.Dict[str, t.Any],
|
||||||
|
prog_name: str,
|
||||||
|
complete_var: str,
|
||||||
|
instruction: str,
|
||||||
|
) -> int:
|
||||||
|
"""Perform shell completion for the given CLI program.
|
||||||
|
|
||||||
|
:param cli: Command being called.
|
||||||
|
:param ctx_args: Extra arguments to pass to
|
||||||
|
``cli.make_context``.
|
||||||
|
:param prog_name: Name of the executable in the shell.
|
||||||
|
:param complete_var: Name of the environment variable that holds
|
||||||
|
the completion instruction.
|
||||||
|
:param instruction: Value of ``complete_var`` with the completion
|
||||||
|
instruction and shell, in the form ``instruction_shell``.
|
||||||
|
:return: Status code to exit with.
|
||||||
|
"""
|
||||||
|
shell, _, instruction = instruction.partition("_")
|
||||||
|
comp_cls = get_completion_class(shell)
|
||||||
|
|
||||||
|
if comp_cls is None:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
comp = comp_cls(cli, ctx_args, prog_name, complete_var)
|
||||||
|
|
||||||
|
if instruction == "source":
|
||||||
|
echo(comp.source())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if instruction == "complete":
|
||||||
|
echo(comp.complete())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionItem:
|
||||||
|
"""Represents a completion value and metadata about the value. The
|
||||||
|
default metadata is ``type`` to indicate special shell handling,
|
||||||
|
and ``help`` if a shell supports showing a help string next to the
|
||||||
|
value.
|
||||||
|
|
||||||
|
Arbitrary parameters can be passed when creating the object, and
|
||||||
|
accessed using ``item.attr``. If an attribute wasn't passed,
|
||||||
|
accessing it returns ``None``.
|
||||||
|
|
||||||
|
:param value: The completion suggestion.
|
||||||
|
:param type: Tells the shell script to provide special completion
|
||||||
|
support for the type. Click uses ``"dir"`` and ``"file"``.
|
||||||
|
:param help: String shown next to the value if supported.
|
||||||
|
:param kwargs: Arbitrary metadata. The built-in implementations
|
||||||
|
don't use this, but custom type completions paired with custom
|
||||||
|
shell support could use it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("value", "type", "help", "_info")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: t.Any,
|
||||||
|
type: str = "plain",
|
||||||
|
help: t.Optional[str] = None,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> None:
|
||||||
|
self.value = value
|
||||||
|
self.type = type
|
||||||
|
self.help = help
|
||||||
|
self._info = kwargs
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
|
return self._info.get(name)
|
||||||
|
|
||||||
|
|
||||||
|
# Only Bash >= 4.4 has the nosort option.
|
||||||
|
_SOURCE_BASH = """\
|
||||||
|
%(complete_func)s() {
|
||||||
|
local IFS=$'\\n'
|
||||||
|
local response
|
||||||
|
|
||||||
|
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \
|
||||||
|
%(complete_var)s=bash_complete $1)
|
||||||
|
|
||||||
|
for completion in $response; do
|
||||||
|
IFS=',' read type value <<< "$completion"
|
||||||
|
|
||||||
|
if [[ $type == 'dir' ]]; then
|
||||||
|
COMREPLY=()
|
||||||
|
compopt -o dirnames
|
||||||
|
elif [[ $type == 'file' ]]; then
|
||||||
|
COMREPLY=()
|
||||||
|
compopt -o default
|
||||||
|
elif [[ $type == 'plain' ]]; then
|
||||||
|
COMPREPLY+=($value)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
%(complete_func)s_setup() {
|
||||||
|
complete -o nosort -F %(complete_func)s %(prog_name)s
|
||||||
|
}
|
||||||
|
|
||||||
|
%(complete_func)s_setup;
|
||||||
|
"""
|
||||||
|
|
||||||
|
_SOURCE_ZSH = """\
|
||||||
|
#compdef %(prog_name)s
|
||||||
|
|
||||||
|
%(complete_func)s() {
|
||||||
|
local -a completions
|
||||||
|
local -a completions_with_descriptions
|
||||||
|
local -a response
|
||||||
|
(( ! $+commands[%(prog_name)s] )) && return 1
|
||||||
|
|
||||||
|
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \
|
||||||
|
%(complete_var)s=zsh_complete %(prog_name)s)}")
|
||||||
|
|
||||||
|
for type key descr in ${response}; do
|
||||||
|
if [[ "$type" == "plain" ]]; then
|
||||||
|
if [[ "$descr" == "_" ]]; then
|
||||||
|
completions+=("$key")
|
||||||
|
else
|
||||||
|
completions_with_descriptions+=("$key":"$descr")
|
||||||
|
fi
|
||||||
|
elif [[ "$type" == "dir" ]]; then
|
||||||
|
_path_files -/
|
||||||
|
elif [[ "$type" == "file" ]]; then
|
||||||
|
_path_files -f
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$completions_with_descriptions" ]; then
|
||||||
|
_describe -V unsorted completions_with_descriptions -U
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$completions" ]; then
|
||||||
|
compadd -U -V unsorted -a completions
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
compdef %(complete_func)s %(prog_name)s;
|
||||||
|
"""
|
||||||
|
|
||||||
|
_SOURCE_FISH = """\
|
||||||
|
function %(complete_func)s;
|
||||||
|
set -l response;
|
||||||
|
|
||||||
|
for value in (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \
|
||||||
|
COMP_CWORD=(commandline -t) %(prog_name)s);
|
||||||
|
set response $response $value;
|
||||||
|
end;
|
||||||
|
|
||||||
|
for completion in $response;
|
||||||
|
set -l metadata (string split "," $completion);
|
||||||
|
|
||||||
|
if test $metadata[1] = "dir";
|
||||||
|
__fish_complete_directories $metadata[2];
|
||||||
|
else if test $metadata[1] = "file";
|
||||||
|
__fish_complete_path $metadata[2];
|
||||||
|
else if test $metadata[1] = "plain";
|
||||||
|
echo $metadata[2];
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
complete --no-files --command %(prog_name)s --arguments \
|
||||||
|
"(%(complete_func)s)";
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ShellComplete:
|
||||||
|
"""Base class for providing shell completion support. A subclass for
|
||||||
|
a given shell will override attributes and methods to implement the
|
||||||
|
completion instructions (``source`` and ``complete``).
|
||||||
|
|
||||||
|
:param cli: Command being called.
|
||||||
|
:param prog_name: Name of the executable in the shell.
|
||||||
|
:param complete_var: Name of the environment variable that holds
|
||||||
|
the completion instruction.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: t.ClassVar[str]
|
||||||
|
"""Name to register the shell as with :func:`add_completion_class`.
|
||||||
|
This is used in completion instructions (``{name}_source`` and
|
||||||
|
``{name}_complete``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_template: t.ClassVar[str]
|
||||||
|
"""Completion script template formatted by :meth:`source`. This must
|
||||||
|
be provided by subclasses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cli: BaseCommand,
|
||||||
|
ctx_args: t.Dict[str, t.Any],
|
||||||
|
prog_name: str,
|
||||||
|
complete_var: str,
|
||||||
|
) -> None:
|
||||||
|
self.cli = cli
|
||||||
|
self.ctx_args = ctx_args
|
||||||
|
self.prog_name = prog_name
|
||||||
|
self.complete_var = complete_var
|
||||||
|
|
||||||
|
@property
|
||||||
|
def func_name(self) -> str:
|
||||||
|
"""The name of the shell function defined by the completion
|
||||||
|
script.
|
||||||
|
"""
|
||||||
|
safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), re.ASCII)
|
||||||
|
return f"_{safe_name}_completion"
|
||||||
|
|
||||||
|
def source_vars(self) -> t.Dict[str, t.Any]:
|
||||||
|
"""Vars for formatting :attr:`source_template`.
|
||||||
|
|
||||||
|
By default this provides ``complete_func``, ``complete_var``,
|
||||||
|
and ``prog_name``.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"complete_func": self.func_name,
|
||||||
|
"complete_var": self.complete_var,
|
||||||
|
"prog_name": self.prog_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
def source(self) -> str:
|
||||||
|
"""Produce the shell script that defines the completion
|
||||||
|
function. By default this ``%``-style formats
|
||||||
|
:attr:`source_template` with the dict returned by
|
||||||
|
:meth:`source_vars`.
|
||||||
|
"""
|
||||||
|
return self.source_template % self.source_vars()
|
||||||
|
|
||||||
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
|
||||||
|
"""Use the env vars defined by the shell script to return a
|
||||||
|
tuple of ``args, incomplete``. This must be implemented by
|
||||||
|
subclasses.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_completions(
|
||||||
|
self, args: t.List[str], incomplete: str
|
||||||
|
) -> t.List[CompletionItem]:
|
||||||
|
"""Determine the context and last complete command or parameter
|
||||||
|
from the complete args. Call that object's ``shell_complete``
|
||||||
|
method to get the completions for the incomplete value.
|
||||||
|
|
||||||
|
:param args: List of complete args before the incomplete value.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
"""
|
||||||
|
ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args)
|
||||||
|
obj, incomplete = _resolve_incomplete(ctx, args, incomplete)
|
||||||
|
return obj.shell_complete(ctx, incomplete)
|
||||||
|
|
||||||
|
def format_completion(self, item: CompletionItem) -> str:
|
||||||
|
"""Format a completion item into the form recognized by the
|
||||||
|
shell script. This must be implemented by subclasses.
|
||||||
|
|
||||||
|
:param item: Completion item to format.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def complete(self) -> str:
|
||||||
|
"""Produce the completion data to send back to the shell.
|
||||||
|
|
||||||
|
By default this calls :meth:`get_completion_args`, gets the
|
||||||
|
completions, then calls :meth:`format_completion` for each
|
||||||
|
completion.
|
||||||
|
"""
|
||||||
|
args, incomplete = self.get_completion_args()
|
||||||
|
completions = self.get_completions(args, incomplete)
|
||||||
|
out = [self.format_completion(item) for item in completions]
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
class BashComplete(ShellComplete):
|
||||||
|
"""Shell completion for Bash."""
|
||||||
|
|
||||||
|
name = "bash"
|
||||||
|
source_template = _SOURCE_BASH
|
||||||
|
|
||||||
|
def _check_version(self) -> None:
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
output = subprocess.run(
|
||||||
|
["bash", "-c", "echo ${BASH_VERSION}"], stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode())
|
||||||
|
|
||||||
|
if match is not None:
|
||||||
|
major, minor = match.groups()
|
||||||
|
|
||||||
|
if major < "4" or major == "4" and minor < "4":
|
||||||
|
raise RuntimeError(
|
||||||
|
_(
|
||||||
|
"Shell completion is not supported for Bash"
|
||||||
|
" versions older than 4.4."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
_("Couldn't detect Bash version, shell completion is not supported.")
|
||||||
|
)
|
||||||
|
|
||||||
|
def source(self) -> str:
|
||||||
|
self._check_version()
|
||||||
|
return super().source()
|
||||||
|
|
||||||
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
|
||||||
|
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
||||||
|
cword = int(os.environ["COMP_CWORD"])
|
||||||
|
args = cwords[1:cword]
|
||||||
|
|
||||||
|
try:
|
||||||
|
incomplete = cwords[cword]
|
||||||
|
except IndexError:
|
||||||
|
incomplete = ""
|
||||||
|
|
||||||
|
return args, incomplete
|
||||||
|
|
||||||
|
def format_completion(self, item: CompletionItem) -> str:
|
||||||
|
return f"{item.type},{item.value}"
|
||||||
|
|
||||||
|
|
||||||
|
class ZshComplete(ShellComplete):
|
||||||
|
"""Shell completion for Zsh."""
|
||||||
|
|
||||||
|
name = "zsh"
|
||||||
|
source_template = _SOURCE_ZSH
|
||||||
|
|
||||||
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
|
||||||
|
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
||||||
|
cword = int(os.environ["COMP_CWORD"])
|
||||||
|
args = cwords[1:cword]
|
||||||
|
|
||||||
|
try:
|
||||||
|
incomplete = cwords[cword]
|
||||||
|
except IndexError:
|
||||||
|
incomplete = ""
|
||||||
|
|
||||||
|
return args, incomplete
|
||||||
|
|
||||||
|
def format_completion(self, item: CompletionItem) -> str:
|
||||||
|
return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}"
|
||||||
|
|
||||||
|
|
||||||
|
class FishComplete(ShellComplete):
|
||||||
|
"""Shell completion for Fish."""
|
||||||
|
|
||||||
|
name = "fish"
|
||||||
|
source_template = _SOURCE_FISH
|
||||||
|
|
||||||
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
|
||||||
|
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
||||||
|
incomplete = os.environ["COMP_CWORD"]
|
||||||
|
args = cwords[1:]
|
||||||
|
|
||||||
|
# Fish stores the partial word in both COMP_WORDS and
|
||||||
|
# COMP_CWORD, remove it from complete args.
|
||||||
|
if incomplete and args and args[-1] == incomplete:
|
||||||
|
args.pop()
|
||||||
|
|
||||||
|
return args, incomplete
|
||||||
|
|
||||||
|
def format_completion(self, item: CompletionItem) -> str:
|
||||||
|
if item.help:
|
||||||
|
return f"{item.type},{item.value}\t{item.help}"
|
||||||
|
|
||||||
|
return f"{item.type},{item.value}"
|
||||||
|
|
||||||
|
|
||||||
|
_available_shells: t.Dict[str, t.Type[ShellComplete]] = {
|
||||||
|
"bash": BashComplete,
|
||||||
|
"fish": FishComplete,
|
||||||
|
"zsh": ZshComplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def add_completion_class(
|
||||||
|
cls: t.Type[ShellComplete], name: t.Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""Register a :class:`ShellComplete` subclass under the given name.
|
||||||
|
The name will be provided by the completion instruction environment
|
||||||
|
variable during completion.
|
||||||
|
|
||||||
|
:param cls: The completion class that will handle completion for the
|
||||||
|
shell.
|
||||||
|
:param name: Name to register the class under. Defaults to the
|
||||||
|
class's ``name`` attribute.
|
||||||
|
"""
|
||||||
|
if name is None:
|
||||||
|
name = cls.name
|
||||||
|
|
||||||
|
_available_shells[name] = cls
|
||||||
|
|
||||||
|
|
||||||
|
def get_completion_class(shell: str) -> t.Optional[t.Type[ShellComplete]]:
|
||||||
|
"""Look up a registered :class:`ShellComplete` subclass by the name
|
||||||
|
provided by the completion instruction environment variable. If the
|
||||||
|
name isn't registered, returns ``None``.
|
||||||
|
|
||||||
|
:param shell: Name the class is registered under.
|
||||||
|
"""
|
||||||
|
return _available_shells.get(shell)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool:
|
||||||
|
"""Determine if the given parameter is an argument that can still
|
||||||
|
accept values.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for the command represented by the
|
||||||
|
parsed complete args.
|
||||||
|
:param param: Argument object being checked.
|
||||||
|
"""
|
||||||
|
if not isinstance(param, Argument):
|
||||||
|
return False
|
||||||
|
|
||||||
|
assert param.name is not None
|
||||||
|
value = ctx.params[param.name]
|
||||||
|
return (
|
||||||
|
param.nargs == -1
|
||||||
|
or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE
|
||||||
|
or (
|
||||||
|
param.nargs > 1
|
||||||
|
and isinstance(value, (tuple, list))
|
||||||
|
and len(value) < param.nargs
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _start_of_option(value: str) -> bool:
|
||||||
|
"""Check if the value looks like the start of an option."""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
c = value[0]
|
||||||
|
# Allow "/" since that starts a path.
|
||||||
|
return not c.isalnum() and c != "/"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_incomplete_option(args: t.List[str], param: Parameter) -> bool:
|
||||||
|
"""Determine if the given parameter is an option that needs a value.
|
||||||
|
|
||||||
|
:param args: List of complete args before the incomplete value.
|
||||||
|
:param param: Option object being checked.
|
||||||
|
"""
|
||||||
|
if not isinstance(param, Option):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if param.is_flag:
|
||||||
|
return False
|
||||||
|
|
||||||
|
last_option = None
|
||||||
|
|
||||||
|
for index, arg in enumerate(reversed(args)):
|
||||||
|
if index + 1 > param.nargs:
|
||||||
|
break
|
||||||
|
|
||||||
|
if _start_of_option(arg):
|
||||||
|
last_option = arg
|
||||||
|
|
||||||
|
return last_option is not None and last_option in param.opts
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_context(
|
||||||
|
cli: BaseCommand, ctx_args: t.Dict[str, t.Any], prog_name: str, args: t.List[str]
|
||||||
|
) -> Context:
|
||||||
|
"""Produce the context hierarchy starting with the command and
|
||||||
|
traversing the complete arguments. This only follows the commands,
|
||||||
|
it doesn't trigger input prompts or callbacks.
|
||||||
|
|
||||||
|
:param cli: Command being called.
|
||||||
|
:param prog_name: Name of the executable in the shell.
|
||||||
|
:param args: List of complete args before the incomplete value.
|
||||||
|
"""
|
||||||
|
ctx_args["resilient_parsing"] = True
|
||||||
|
ctx = cli.make_context(prog_name, args.copy(), **ctx_args)
|
||||||
|
args = ctx.protected_args + ctx.args
|
||||||
|
|
||||||
|
while args:
|
||||||
|
command = ctx.command
|
||||||
|
|
||||||
|
if isinstance(command, MultiCommand):
|
||||||
|
if not command.chain:
|
||||||
|
name, cmd, args = command.resolve_command(ctx, args)
|
||||||
|
|
||||||
|
if cmd is None:
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True)
|
||||||
|
args = ctx.protected_args + ctx.args
|
||||||
|
else:
|
||||||
|
while args:
|
||||||
|
name, cmd, args = command.resolve_command(ctx, args)
|
||||||
|
|
||||||
|
if cmd is None:
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
sub_ctx = cmd.make_context(
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
parent=ctx,
|
||||||
|
allow_extra_args=True,
|
||||||
|
allow_interspersed_args=False,
|
||||||
|
resilient_parsing=True,
|
||||||
|
)
|
||||||
|
args = sub_ctx.args
|
||||||
|
|
||||||
|
ctx = sub_ctx
|
||||||
|
args = [*sub_ctx.protected_args, *sub_ctx.args]
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_incomplete(
|
||||||
|
ctx: Context, args: t.List[str], incomplete: str
|
||||||
|
) -> t.Tuple[t.Union[BaseCommand, Parameter], str]:
|
||||||
|
"""Find the Click object that will handle the completion of the
|
||||||
|
incomplete value. Return the object and the incomplete value.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for the command represented by
|
||||||
|
the parsed complete args.
|
||||||
|
:param args: List of complete args before the incomplete value.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
"""
|
||||||
|
# Different shells treat an "=" between a long option name and
|
||||||
|
# value differently. Might keep the value joined, return the "="
|
||||||
|
# as a separate item, or return the split name and value. Always
|
||||||
|
# split and discard the "=" to make completion easier.
|
||||||
|
if incomplete == "=":
|
||||||
|
incomplete = ""
|
||||||
|
elif "=" in incomplete and _start_of_option(incomplete):
|
||||||
|
name, _, incomplete = incomplete.partition("=")
|
||||||
|
args.append(name)
|
||||||
|
|
||||||
|
# The "--" marker tells Click to stop treating values as options
|
||||||
|
# even if they start with the option character. If it hasn't been
|
||||||
|
# given and the incomplete arg looks like an option, the current
|
||||||
|
# command will provide option name completions.
|
||||||
|
if "--" not in args and _start_of_option(incomplete):
|
||||||
|
return ctx.command, incomplete
|
||||||
|
|
||||||
|
params = ctx.command.get_params(ctx)
|
||||||
|
|
||||||
|
# If the last complete arg is an option name with an incomplete
|
||||||
|
# value, the option will provide value completions.
|
||||||
|
for param in params:
|
||||||
|
if _is_incomplete_option(args, param):
|
||||||
|
return param, incomplete
|
||||||
|
|
||||||
|
# It's not an option name or value. The first argument without a
|
||||||
|
# parsed value will provide value completions.
|
||||||
|
for param in params:
|
||||||
|
if _is_incomplete_argument(ctx, param):
|
||||||
|
return param, incomplete
|
||||||
|
|
||||||
|
# There were no unparsed arguments, the command may be a group that
|
||||||
|
# will provide command name completions.
|
||||||
|
return ctx.command, incomplete
|
|
@ -2,29 +2,31 @@ import inspect
|
||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
import struct
|
|
||||||
import sys
|
import sys
|
||||||
|
import typing
|
||||||
|
import typing as t
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
from ._compat import DEFAULT_COLUMNS
|
|
||||||
from ._compat import get_winterm_size
|
|
||||||
from ._compat import isatty
|
from ._compat import isatty
|
||||||
from ._compat import raw_input
|
|
||||||
from ._compat import string_types
|
|
||||||
from ._compat import strip_ansi
|
from ._compat import strip_ansi
|
||||||
from ._compat import text_type
|
|
||||||
from ._compat import WIN
|
from ._compat import WIN
|
||||||
from .exceptions import Abort
|
from .exceptions import Abort
|
||||||
from .exceptions import UsageError
|
from .exceptions import UsageError
|
||||||
from .globals import resolve_color_default
|
from .globals import resolve_color_default
|
||||||
from .types import Choice
|
from .types import Choice
|
||||||
from .types import convert_type
|
from .types import convert_type
|
||||||
from .types import Path
|
from .types import ParamType
|
||||||
from .utils import echo
|
from .utils import echo
|
||||||
from .utils import LazyFile
|
from .utils import LazyFile
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from ._termui_impl import ProgressBar
|
||||||
|
|
||||||
|
V = t.TypeVar("V")
|
||||||
|
|
||||||
# The prompt functions to use. The doc tools currently override these
|
# The prompt functions to use. The doc tools currently override these
|
||||||
# functions to customize how they work.
|
# functions to customize how they work.
|
||||||
visible_prompt_func = raw_input
|
visible_prompt_func: t.Callable[[str], str] = input
|
||||||
|
|
||||||
_ansi_colors = {
|
_ansi_colors = {
|
||||||
"black": 30,
|
"black": 30,
|
||||||
|
@ -48,63 +50,61 @@ _ansi_colors = {
|
||||||
_ansi_reset_all = "\033[0m"
|
_ansi_reset_all = "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
def hidden_prompt_func(prompt):
|
def hidden_prompt_func(prompt: str) -> str:
|
||||||
import getpass
|
import getpass
|
||||||
|
|
||||||
return getpass.getpass(prompt)
|
return getpass.getpass(prompt)
|
||||||
|
|
||||||
|
|
||||||
def _build_prompt(
|
def _build_prompt(
|
||||||
text, suffix, show_default=False, default=None, show_choices=True, type=None
|
text: str,
|
||||||
):
|
suffix: str,
|
||||||
|
show_default: bool = False,
|
||||||
|
default: t.Optional[t.Any] = None,
|
||||||
|
show_choices: bool = True,
|
||||||
|
type: t.Optional[ParamType] = None,
|
||||||
|
) -> str:
|
||||||
prompt = text
|
prompt = text
|
||||||
if type is not None and show_choices and isinstance(type, Choice):
|
if type is not None and show_choices and isinstance(type, Choice):
|
||||||
prompt += " ({})".format(", ".join(map(str, type.choices)))
|
prompt += f" ({', '.join(map(str, type.choices))})"
|
||||||
if default is not None and show_default:
|
if default is not None and show_default:
|
||||||
prompt = "{} [{}]".format(prompt, _format_default(default))
|
prompt = f"{prompt} [{_format_default(default)}]"
|
||||||
return prompt + suffix
|
return f"{prompt}{suffix}"
|
||||||
|
|
||||||
|
|
||||||
def _format_default(default):
|
def _format_default(default: t.Any) -> t.Any:
|
||||||
if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"):
|
if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"):
|
||||||
return default.name
|
return default.name # type: ignore
|
||||||
|
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def prompt(
|
def prompt(
|
||||||
text,
|
text: str,
|
||||||
default=None,
|
default: t.Optional[t.Any] = None,
|
||||||
hide_input=False,
|
hide_input: bool = False,
|
||||||
confirmation_prompt=False,
|
confirmation_prompt: t.Union[bool, str] = False,
|
||||||
type=None,
|
type: t.Optional[t.Union[ParamType, t.Any]] = None,
|
||||||
value_proc=None,
|
value_proc: t.Optional[t.Callable[[str], t.Any]] = None,
|
||||||
prompt_suffix=": ",
|
prompt_suffix: str = ": ",
|
||||||
show_default=True,
|
show_default: bool = True,
|
||||||
err=False,
|
err: bool = False,
|
||||||
show_choices=True,
|
show_choices: bool = True,
|
||||||
):
|
) -> t.Any:
|
||||||
"""Prompts a user for input. This is a convenience function that can
|
"""Prompts a user for input. This is a convenience function that can
|
||||||
be used to prompt a user for input later.
|
be used to prompt a user for input later.
|
||||||
|
|
||||||
If the user aborts the input by sending a interrupt signal, this
|
If the user aborts the input by sending a interrupt signal, this
|
||||||
function will catch it and raise a :exc:`Abort` exception.
|
function will catch it and raise a :exc:`Abort` exception.
|
||||||
|
|
||||||
.. versionadded:: 7.0
|
|
||||||
Added the show_choices parameter.
|
|
||||||
|
|
||||||
.. versionadded:: 6.0
|
|
||||||
Added unicode support for cmd.exe on Windows.
|
|
||||||
|
|
||||||
.. versionadded:: 4.0
|
|
||||||
Added the `err` parameter.
|
|
||||||
|
|
||||||
:param text: the text to show for the prompt.
|
:param text: the text to show for the prompt.
|
||||||
:param default: the default value to use if no input happens. If this
|
:param default: the default value to use if no input happens. If this
|
||||||
is not given it will prompt until it's aborted.
|
is not given it will prompt until it's aborted.
|
||||||
:param hide_input: if this is set to true then the input value will
|
:param hide_input: if this is set to true then the input value will
|
||||||
be hidden.
|
be hidden.
|
||||||
:param confirmation_prompt: asks for confirmation for the value.
|
:param confirmation_prompt: Prompt a second time to confirm the
|
||||||
|
value. Can be set to a string instead of ``True`` to customize
|
||||||
|
the message.
|
||||||
:param type: the type to use to check the value against.
|
:param type: the type to use to check the value against.
|
||||||
:param value_proc: if this parameter is provided it's a function that
|
:param value_proc: if this parameter is provided it's a function that
|
||||||
is invoked instead of the type conversion to
|
is invoked instead of the type conversion to
|
||||||
|
@ -117,23 +117,37 @@ def prompt(
|
||||||
For example if type is a Choice of either day or week,
|
For example if type is a Choice of either day or week,
|
||||||
show_choices is true and text is "Group by" then the
|
show_choices is true and text is "Group by" then the
|
||||||
prompt will be "Group by (day, week): ".
|
prompt will be "Group by (day, week): ".
|
||||||
"""
|
|
||||||
result = None
|
|
||||||
|
|
||||||
def prompt_func(text):
|
.. versionadded:: 8.0
|
||||||
|
``confirmation_prompt`` can be a custom string.
|
||||||
|
|
||||||
|
.. versionadded:: 7.0
|
||||||
|
Added the ``show_choices`` parameter.
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
Added unicode support for cmd.exe on Windows.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
Added the `err` parameter.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def prompt_func(text: str) -> str:
|
||||||
f = hidden_prompt_func if hide_input else visible_prompt_func
|
f = hidden_prompt_func if hide_input else visible_prompt_func
|
||||||
try:
|
try:
|
||||||
# Write the prompt separately so that we get nice
|
# Write the prompt separately so that we get nice
|
||||||
# coloring through colorama on Windows
|
# coloring through colorama on Windows
|
||||||
echo(text, nl=False, err=err)
|
echo(text.rstrip(" "), nl=False, err=err)
|
||||||
return f("")
|
# Echo a space to stdout to work around an issue where
|
||||||
|
# readline causes backspace to clear the whole line.
|
||||||
|
return f(" ")
|
||||||
except (KeyboardInterrupt, EOFError):
|
except (KeyboardInterrupt, EOFError):
|
||||||
# getpass doesn't print a newline if the user aborts input with ^C.
|
# getpass doesn't print a newline if the user aborts input with ^C.
|
||||||
# Allegedly this behavior is inherited from getpass(3).
|
# Allegedly this behavior is inherited from getpass(3).
|
||||||
# A doc bug has been filed at https://bugs.python.org/issue24711
|
# A doc bug has been filed at https://bugs.python.org/issue24711
|
||||||
if hide_input:
|
if hide_input:
|
||||||
echo(None, err=err)
|
echo(None, err=err)
|
||||||
raise Abort()
|
raise Abort() from None
|
||||||
|
|
||||||
if value_proc is None:
|
if value_proc is None:
|
||||||
value_proc = convert_type(type, default)
|
value_proc = convert_type(type, default)
|
||||||
|
@ -142,72 +156,93 @@ def prompt(
|
||||||
text, prompt_suffix, show_default, default, show_choices, type
|
text, prompt_suffix, show_default, default, show_choices, type
|
||||||
)
|
)
|
||||||
|
|
||||||
while 1:
|
if confirmation_prompt:
|
||||||
while 1:
|
if confirmation_prompt is True:
|
||||||
|
confirmation_prompt = _("Repeat for confirmation")
|
||||||
|
|
||||||
|
confirmation_prompt = t.cast(str, confirmation_prompt)
|
||||||
|
confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
while True:
|
||||||
value = prompt_func(prompt)
|
value = prompt_func(prompt)
|
||||||
if value:
|
if value:
|
||||||
break
|
break
|
||||||
elif default is not None:
|
elif default is not None:
|
||||||
if isinstance(value_proc, Path):
|
value = default
|
||||||
# validate Path default value(exists, dir_okay etc.)
|
break
|
||||||
value = default
|
|
||||||
break
|
|
||||||
return default
|
|
||||||
try:
|
try:
|
||||||
result = value_proc(value)
|
result = value_proc(value)
|
||||||
except UsageError as e:
|
except UsageError as e:
|
||||||
echo("Error: {}".format(e.message), err=err) # noqa: B306
|
if hide_input:
|
||||||
|
echo(_("Error: The value you entered was invalid."), err=err)
|
||||||
|
else:
|
||||||
|
echo(_("Error: {e.message}").format(e=e), err=err) # noqa: B306
|
||||||
continue
|
continue
|
||||||
if not confirmation_prompt:
|
if not confirmation_prompt:
|
||||||
return result
|
return result
|
||||||
while 1:
|
while True:
|
||||||
value2 = prompt_func("Repeat for confirmation: ")
|
confirmation_prompt = t.cast(str, confirmation_prompt)
|
||||||
|
value2 = prompt_func(confirmation_prompt)
|
||||||
if value2:
|
if value2:
|
||||||
break
|
break
|
||||||
if value == value2:
|
if value == value2:
|
||||||
return result
|
return result
|
||||||
echo("Error: the two entered values do not match", err=err)
|
echo(_("Error: The two entered values do not match."), err=err)
|
||||||
|
|
||||||
|
|
||||||
def confirm(
|
def confirm(
|
||||||
text, default=False, abort=False, prompt_suffix=": ", show_default=True, err=False
|
text: str,
|
||||||
):
|
default: t.Optional[bool] = False,
|
||||||
|
abort: bool = False,
|
||||||
|
prompt_suffix: str = ": ",
|
||||||
|
show_default: bool = True,
|
||||||
|
err: bool = False,
|
||||||
|
) -> bool:
|
||||||
"""Prompts for confirmation (yes/no question).
|
"""Prompts for confirmation (yes/no question).
|
||||||
|
|
||||||
If the user aborts the input by sending a interrupt signal this
|
If the user aborts the input by sending a interrupt signal this
|
||||||
function will catch it and raise a :exc:`Abort` exception.
|
function will catch it and raise a :exc:`Abort` exception.
|
||||||
|
|
||||||
.. versionadded:: 4.0
|
|
||||||
Added the `err` parameter.
|
|
||||||
|
|
||||||
:param text: the question to ask.
|
:param text: the question to ask.
|
||||||
:param default: the default for the prompt.
|
:param default: The default value to use when no input is given. If
|
||||||
|
``None``, repeat until input is given.
|
||||||
:param abort: if this is set to `True` a negative answer aborts the
|
:param abort: if this is set to `True` a negative answer aborts the
|
||||||
exception by raising :exc:`Abort`.
|
exception by raising :exc:`Abort`.
|
||||||
:param prompt_suffix: a suffix that should be added to the prompt.
|
:param prompt_suffix: a suffix that should be added to the prompt.
|
||||||
:param show_default: shows or hides the default value in the prompt.
|
:param show_default: shows or hides the default value in the prompt.
|
||||||
:param err: if set to true the file defaults to ``stderr`` instead of
|
:param err: if set to true the file defaults to ``stderr`` instead of
|
||||||
``stdout``, the same as with echo.
|
``stdout``, the same as with echo.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Repeat until input is given if ``default`` is ``None``.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
Added the ``err`` parameter.
|
||||||
"""
|
"""
|
||||||
prompt = _build_prompt(
|
prompt = _build_prompt(
|
||||||
text, prompt_suffix, show_default, "Y/n" if default else "y/N"
|
text,
|
||||||
|
prompt_suffix,
|
||||||
|
show_default,
|
||||||
|
"y/n" if default is None else ("Y/n" if default else "y/N"),
|
||||||
)
|
)
|
||||||
while 1:
|
|
||||||
|
while True:
|
||||||
try:
|
try:
|
||||||
# Write the prompt separately so that we get nice
|
# Write the prompt separately so that we get nice
|
||||||
# coloring through colorama on Windows
|
# coloring through colorama on Windows
|
||||||
echo(prompt, nl=False, err=err)
|
echo(prompt, nl=False, err=err)
|
||||||
value = visible_prompt_func("").lower().strip()
|
value = visible_prompt_func("").lower().strip()
|
||||||
except (KeyboardInterrupt, EOFError):
|
except (KeyboardInterrupt, EOFError):
|
||||||
raise Abort()
|
raise Abort() from None
|
||||||
if value in ("y", "yes"):
|
if value in ("y", "yes"):
|
||||||
rv = True
|
rv = True
|
||||||
elif value in ("n", "no"):
|
elif value in ("n", "no"):
|
||||||
rv = False
|
rv = False
|
||||||
elif value == "":
|
elif default is not None and value == "":
|
||||||
rv = default
|
rv = default
|
||||||
else:
|
else:
|
||||||
echo("Error: invalid input", err=err)
|
echo(_("Error: invalid input"), err=err)
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
if abort and not rv:
|
if abort and not rv:
|
||||||
|
@ -215,54 +250,30 @@ def confirm(
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
def get_terminal_size():
|
def get_terminal_size() -> os.terminal_size:
|
||||||
"""Returns the current size of the terminal as tuple in the form
|
"""Returns the current size of the terminal as tuple in the form
|
||||||
``(width, height)`` in columns and rows.
|
``(width, height)`` in columns and rows.
|
||||||
|
|
||||||
|
.. deprecated:: 8.0
|
||||||
|
Will be removed in Click 8.1. Use
|
||||||
|
:func:`shutil.get_terminal_size` instead.
|
||||||
"""
|
"""
|
||||||
# If shutil has get_terminal_size() (Python 3.3 and later) use that
|
import shutil
|
||||||
if sys.version_info >= (3, 3):
|
import warnings
|
||||||
import shutil
|
|
||||||
|
|
||||||
shutil_get_terminal_size = getattr(shutil, "get_terminal_size", None)
|
warnings.warn(
|
||||||
if shutil_get_terminal_size:
|
"'click.get_terminal_size()' is deprecated and will be removed"
|
||||||
sz = shutil_get_terminal_size()
|
" in Click 8.1. Use 'shutil.get_terminal_size()' instead.",
|
||||||
return sz.columns, sz.lines
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
# We provide a sensible default for get_winterm_size() when being invoked
|
)
|
||||||
# inside a subprocess. Without this, it would not provide a useful input.
|
return shutil.get_terminal_size()
|
||||||
if get_winterm_size is not None:
|
|
||||||
size = get_winterm_size()
|
|
||||||
if size == (0, 0):
|
|
||||||
return (79, 24)
|
|
||||||
else:
|
|
||||||
return size
|
|
||||||
|
|
||||||
def ioctl_gwinsz(fd):
|
|
||||||
try:
|
|
||||||
import fcntl
|
|
||||||
import termios
|
|
||||||
|
|
||||||
cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
return cr
|
|
||||||
|
|
||||||
cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2)
|
|
||||||
if not cr:
|
|
||||||
try:
|
|
||||||
fd = os.open(os.ctermid(), os.O_RDONLY)
|
|
||||||
try:
|
|
||||||
cr = ioctl_gwinsz(fd)
|
|
||||||
finally:
|
|
||||||
os.close(fd)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if not cr or not cr[0] or not cr[1]:
|
|
||||||
cr = (os.environ.get("LINES", 25), os.environ.get("COLUMNS", DEFAULT_COLUMNS))
|
|
||||||
return int(cr[1]), int(cr[0])
|
|
||||||
|
|
||||||
|
|
||||||
def echo_via_pager(text_or_generator, color=None):
|
def echo_via_pager(
|
||||||
|
text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str],
|
||||||
|
color: t.Optional[bool] = None,
|
||||||
|
) -> None:
|
||||||
"""This function takes a text and shows it via an environment specific
|
"""This function takes a text and shows it via an environment specific
|
||||||
pager on stdout.
|
pager on stdout.
|
||||||
|
|
||||||
|
@ -277,14 +288,14 @@ def echo_via_pager(text_or_generator, color=None):
|
||||||
color = resolve_color_default(color)
|
color = resolve_color_default(color)
|
||||||
|
|
||||||
if inspect.isgeneratorfunction(text_or_generator):
|
if inspect.isgeneratorfunction(text_or_generator):
|
||||||
i = text_or_generator()
|
i = t.cast(t.Callable[[], t.Iterable[str]], text_or_generator)()
|
||||||
elif isinstance(text_or_generator, string_types):
|
elif isinstance(text_or_generator, str):
|
||||||
i = [text_or_generator]
|
i = [text_or_generator]
|
||||||
else:
|
else:
|
||||||
i = iter(text_or_generator)
|
i = iter(t.cast(t.Iterable[str], text_or_generator))
|
||||||
|
|
||||||
# convert every element of i to a text type if necessary
|
# convert every element of i to a text type if necessary
|
||||||
text_generator = (el if isinstance(el, string_types) else text_type(el) for el in i)
|
text_generator = (el if isinstance(el, str) else str(el) for el in i)
|
||||||
|
|
||||||
from ._termui_impl import pager
|
from ._termui_impl import pager
|
||||||
|
|
||||||
|
@ -292,21 +303,22 @@ def echo_via_pager(text_or_generator, color=None):
|
||||||
|
|
||||||
|
|
||||||
def progressbar(
|
def progressbar(
|
||||||
iterable=None,
|
iterable: t.Optional[t.Iterable[V]] = None,
|
||||||
length=None,
|
length: t.Optional[int] = None,
|
||||||
label=None,
|
label: t.Optional[str] = None,
|
||||||
show_eta=True,
|
show_eta: bool = True,
|
||||||
show_percent=None,
|
show_percent: t.Optional[bool] = None,
|
||||||
show_pos=False,
|
show_pos: bool = False,
|
||||||
item_show_func=None,
|
item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None,
|
||||||
fill_char="#",
|
fill_char: str = "#",
|
||||||
empty_char="-",
|
empty_char: str = "-",
|
||||||
bar_template="%(label)s [%(bar)s] %(info)s",
|
bar_template: str = "%(label)s [%(bar)s] %(info)s",
|
||||||
info_sep=" ",
|
info_sep: str = " ",
|
||||||
width=36,
|
width: int = 36,
|
||||||
file=None,
|
file: t.Optional[t.TextIO] = None,
|
||||||
color=None,
|
color: t.Optional[bool] = None,
|
||||||
):
|
update_min_steps: int = 1,
|
||||||
|
) -> "ProgressBar[V]":
|
||||||
"""This function creates an iterable context manager that can be used
|
"""This function creates an iterable context manager that can be used
|
||||||
to iterate over something while showing a progress bar. It will
|
to iterate over something while showing a progress bar. It will
|
||||||
either iterate over the `iterable` or `length` items (that are counted
|
either iterate over the `iterable` or `length` items (that are counted
|
||||||
|
@ -346,11 +358,19 @@ def progressbar(
|
||||||
process_chunk(chunk)
|
process_chunk(chunk)
|
||||||
bar.update(chunks.bytes)
|
bar.update(chunks.bytes)
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
The ``update()`` method also takes an optional value specifying the
|
||||||
|
``current_item`` at the new position. This is useful when used
|
||||||
|
together with ``item_show_func`` to customize the output for each
|
||||||
|
manual step::
|
||||||
|
|
||||||
.. versionadded:: 4.0
|
with click.progressbar(
|
||||||
Added the `color` parameter. Added a `update` method to the
|
length=total_size,
|
||||||
progressbar object.
|
label='Unzipping archive',
|
||||||
|
item_show_func=lambda a: a.filename
|
||||||
|
) as bar:
|
||||||
|
for archive in zip_file:
|
||||||
|
archive.extract()
|
||||||
|
bar.update(archive.size, archive)
|
||||||
|
|
||||||
:param iterable: an iterable to iterate over. If not provided the length
|
:param iterable: an iterable to iterate over. If not provided the length
|
||||||
is required.
|
is required.
|
||||||
|
@ -369,10 +389,10 @@ def progressbar(
|
||||||
`False` if not.
|
`False` if not.
|
||||||
:param show_pos: enables or disables the absolute position display. The
|
:param show_pos: enables or disables the absolute position display. The
|
||||||
default is `False`.
|
default is `False`.
|
||||||
:param item_show_func: a function called with the current item which
|
:param item_show_func: A function called with the current item which
|
||||||
can return a string to show the current item
|
can return a string to show next to the progress bar. If the
|
||||||
next to the progress bar. Note that the current
|
function returns ``None`` nothing is shown. The current item can
|
||||||
item can be `None`!
|
be ``None``, such as when entering and exiting the bar.
|
||||||
:param fill_char: the character to use to show the filled part of the
|
:param fill_char: the character to use to show the filled part of the
|
||||||
progress bar.
|
progress bar.
|
||||||
:param empty_char: the character to use to show the non-filled part of
|
:param empty_char: the character to use to show the non-filled part of
|
||||||
|
@ -384,12 +404,33 @@ def progressbar(
|
||||||
:param info_sep: the separator between multiple info items (eta etc.)
|
:param info_sep: the separator between multiple info items (eta etc.)
|
||||||
:param width: the width of the progress bar in characters, 0 means full
|
:param width: the width of the progress bar in characters, 0 means full
|
||||||
terminal width
|
terminal width
|
||||||
:param file: the file to write to. If this is not a terminal then
|
:param file: The file to write to. If this is not a terminal then
|
||||||
only the label is printed.
|
only the label is printed.
|
||||||
:param color: controls if the terminal supports ANSI colors or not. The
|
:param color: controls if the terminal supports ANSI colors or not. The
|
||||||
default is autodetection. This is only needed if ANSI
|
default is autodetection. This is only needed if ANSI
|
||||||
codes are included anywhere in the progress bar output
|
codes are included anywhere in the progress bar output
|
||||||
which is not the case by default.
|
which is not the case by default.
|
||||||
|
:param update_min_steps: Render only when this many updates have
|
||||||
|
completed. This allows tuning for very fast iterators.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Output is shown even if execution time is less than 0.5 seconds.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
``item_show_func`` shows the current item, not the previous one.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Labels are echoed if the output is not a TTY. Reverts a change
|
||||||
|
in 7.0 that removed all output.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
Added the ``update_min_steps`` parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
Added the ``color`` parameter. Added the ``update`` method to
|
||||||
|
the object.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
from ._termui_impl import ProgressBar
|
from ._termui_impl import ProgressBar
|
||||||
|
|
||||||
|
@ -409,10 +450,11 @@ def progressbar(
|
||||||
label=label,
|
label=label,
|
||||||
width=width,
|
width=width,
|
||||||
color=color,
|
color=color,
|
||||||
|
update_min_steps=update_min_steps,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def clear():
|
def clear() -> None:
|
||||||
"""Clears the terminal screen. This will have the effect of clearing
|
"""Clears the terminal screen. This will have the effect of clearing
|
||||||
the whole visible space of the terminal and moving the cursor to the
|
the whole visible space of the terminal and moving the cursor to the
|
||||||
top left. This does not do anything if not connected to a terminal.
|
top left. This does not do anything if not connected to a terminal.
|
||||||
|
@ -421,26 +463,39 @@ def clear():
|
||||||
"""
|
"""
|
||||||
if not isatty(sys.stdout):
|
if not isatty(sys.stdout):
|
||||||
return
|
return
|
||||||
# If we're on Windows and we don't have colorama available, then we
|
|
||||||
# clear the screen by shelling out. Otherwise we can use an escape
|
|
||||||
# sequence.
|
|
||||||
if WIN:
|
if WIN:
|
||||||
os.system("cls")
|
os.system("cls")
|
||||||
else:
|
else:
|
||||||
sys.stdout.write("\033[2J\033[1;1H")
|
sys.stdout.write("\033[2J\033[1;1H")
|
||||||
|
|
||||||
|
|
||||||
|
def _interpret_color(
|
||||||
|
color: t.Union[int, t.Tuple[int, int, int], str], offset: int = 0
|
||||||
|
) -> str:
|
||||||
|
if isinstance(color, int):
|
||||||
|
return f"{38 + offset};5;{color:d}"
|
||||||
|
|
||||||
|
if isinstance(color, (tuple, list)):
|
||||||
|
r, g, b = color
|
||||||
|
return f"{38 + offset};2;{r:d};{g:d};{b:d}"
|
||||||
|
|
||||||
|
return str(_ansi_colors[color] + offset)
|
||||||
|
|
||||||
|
|
||||||
def style(
|
def style(
|
||||||
text,
|
text: t.Any,
|
||||||
fg=None,
|
fg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None,
|
||||||
bg=None,
|
bg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None,
|
||||||
bold=None,
|
bold: t.Optional[bool] = None,
|
||||||
dim=None,
|
dim: t.Optional[bool] = None,
|
||||||
underline=None,
|
underline: t.Optional[bool] = None,
|
||||||
blink=None,
|
overline: t.Optional[bool] = None,
|
||||||
reverse=None,
|
italic: t.Optional[bool] = None,
|
||||||
reset=True,
|
blink: t.Optional[bool] = None,
|
||||||
):
|
reverse: t.Optional[bool] = None,
|
||||||
|
strikethrough: t.Optional[bool] = None,
|
||||||
|
reset: bool = True,
|
||||||
|
) -> str:
|
||||||
"""Styles a text with ANSI styles and returns the new string. By
|
"""Styles a text with ANSI styles and returns the new string. By
|
||||||
default the styling is self contained which means that at the end
|
default the styling is self contained which means that at the end
|
||||||
of the string a reset code is issued. This can be prevented by
|
of the string a reset code is issued. This can be prevented by
|
||||||
|
@ -451,6 +506,7 @@ def style(
|
||||||
click.echo(click.style('Hello World!', fg='green'))
|
click.echo(click.style('Hello World!', fg='green'))
|
||||||
click.echo(click.style('ATTENTION!', blink=True))
|
click.echo(click.style('ATTENTION!', blink=True))
|
||||||
click.echo(click.style('Some things', reverse=True, fg='cyan'))
|
click.echo(click.style('Some things', reverse=True, fg='cyan'))
|
||||||
|
click.echo(click.style('More colors', fg=(255, 12, 128), bg=117))
|
||||||
|
|
||||||
Supported color names:
|
Supported color names:
|
||||||
|
|
||||||
|
@ -472,10 +528,15 @@ def style(
|
||||||
* ``bright_white``
|
* ``bright_white``
|
||||||
* ``reset`` (reset the color code only)
|
* ``reset`` (reset the color code only)
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
If the terminal supports it, color may also be specified as:
|
||||||
|
|
||||||
.. versionadded:: 7.0
|
- An integer in the interval [0, 255]. The terminal must support
|
||||||
Added support for bright colors.
|
8-bit/256-color mode.
|
||||||
|
- An RGB tuple of three integers in [0, 255]. The terminal must
|
||||||
|
support 24-bit/true-color mode.
|
||||||
|
|
||||||
|
See https://en.wikipedia.org/wiki/ANSI_color and
|
||||||
|
https://gist.github.com/XVilka/8346728 for more information.
|
||||||
|
|
||||||
:param text: the string to style with ansi codes.
|
:param text: the string to style with ansi codes.
|
||||||
:param fg: if provided this will become the foreground color.
|
:param fg: if provided this will become the foreground color.
|
||||||
|
@ -484,42 +545,73 @@ def style(
|
||||||
:param dim: if provided this will enable or disable dim mode. This is
|
:param dim: if provided this will enable or disable dim mode. This is
|
||||||
badly supported.
|
badly supported.
|
||||||
:param underline: if provided this will enable or disable underline.
|
:param underline: if provided this will enable or disable underline.
|
||||||
|
:param overline: if provided this will enable or disable overline.
|
||||||
|
:param italic: if provided this will enable or disable italic.
|
||||||
:param blink: if provided this will enable or disable blinking.
|
:param blink: if provided this will enable or disable blinking.
|
||||||
:param reverse: if provided this will enable or disable inverse
|
:param reverse: if provided this will enable or disable inverse
|
||||||
rendering (foreground becomes background and the
|
rendering (foreground becomes background and the
|
||||||
other way round).
|
other way round).
|
||||||
|
:param strikethrough: if provided this will enable or disable
|
||||||
|
striking through text.
|
||||||
:param reset: by default a reset-all code is added at the end of the
|
:param reset: by default a reset-all code is added at the end of the
|
||||||
string which means that styles do not carry over. This
|
string which means that styles do not carry over. This
|
||||||
can be disabled to compose styles.
|
can be disabled to compose styles.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
A non-string ``message`` is converted to a string.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added support for 256 and RGB color codes.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the ``strikethrough``, ``italic``, and ``overline``
|
||||||
|
parameters.
|
||||||
|
|
||||||
|
.. versionchanged:: 7.0
|
||||||
|
Added support for bright colors.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
|
if not isinstance(text, str):
|
||||||
|
text = str(text)
|
||||||
|
|
||||||
bits = []
|
bits = []
|
||||||
|
|
||||||
if fg:
|
if fg:
|
||||||
try:
|
try:
|
||||||
bits.append("\033[{}m".format(_ansi_colors[fg]))
|
bits.append(f"\033[{_interpret_color(fg)}m")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise TypeError("Unknown color '{}'".format(fg))
|
raise TypeError(f"Unknown color {fg!r}") from None
|
||||||
|
|
||||||
if bg:
|
if bg:
|
||||||
try:
|
try:
|
||||||
bits.append("\033[{}m".format(_ansi_colors[bg] + 10))
|
bits.append(f"\033[{_interpret_color(bg, 10)}m")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise TypeError("Unknown color '{}'".format(bg))
|
raise TypeError(f"Unknown color {bg!r}") from None
|
||||||
|
|
||||||
if bold is not None:
|
if bold is not None:
|
||||||
bits.append("\033[{}m".format(1 if bold else 22))
|
bits.append(f"\033[{1 if bold else 22}m")
|
||||||
if dim is not None:
|
if dim is not None:
|
||||||
bits.append("\033[{}m".format(2 if dim else 22))
|
bits.append(f"\033[{2 if dim else 22}m")
|
||||||
if underline is not None:
|
if underline is not None:
|
||||||
bits.append("\033[{}m".format(4 if underline else 24))
|
bits.append(f"\033[{4 if underline else 24}m")
|
||||||
|
if overline is not None:
|
||||||
|
bits.append(f"\033[{53 if overline else 55}m")
|
||||||
|
if italic is not None:
|
||||||
|
bits.append(f"\033[{3 if italic else 23}m")
|
||||||
if blink is not None:
|
if blink is not None:
|
||||||
bits.append("\033[{}m".format(5 if blink else 25))
|
bits.append(f"\033[{5 if blink else 25}m")
|
||||||
if reverse is not None:
|
if reverse is not None:
|
||||||
bits.append("\033[{}m".format(7 if reverse else 27))
|
bits.append(f"\033[{7 if reverse else 27}m")
|
||||||
|
if strikethrough is not None:
|
||||||
|
bits.append(f"\033[{9 if strikethrough else 29}m")
|
||||||
bits.append(text)
|
bits.append(text)
|
||||||
if reset:
|
if reset:
|
||||||
bits.append(_ansi_reset_all)
|
bits.append(_ansi_reset_all)
|
||||||
return "".join(bits)
|
return "".join(bits)
|
||||||
|
|
||||||
|
|
||||||
def unstyle(text):
|
def unstyle(text: str) -> str:
|
||||||
"""Removes ANSI styling information from a string. Usually it's not
|
"""Removes ANSI styling information from a string. Usually it's not
|
||||||
necessary to use this function as Click's echo function will
|
necessary to use this function as Click's echo function will
|
||||||
automatically remove styling if necessary.
|
automatically remove styling if necessary.
|
||||||
|
@ -531,7 +623,14 @@ def unstyle(text):
|
||||||
return strip_ansi(text)
|
return strip_ansi(text)
|
||||||
|
|
||||||
|
|
||||||
def secho(message=None, file=None, nl=True, err=False, color=None, **styles):
|
def secho(
|
||||||
|
message: t.Optional[t.Any] = None,
|
||||||
|
file: t.Optional[t.IO] = None,
|
||||||
|
nl: bool = True,
|
||||||
|
err: bool = False,
|
||||||
|
color: t.Optional[bool] = None,
|
||||||
|
**styles: t.Any,
|
||||||
|
) -> None:
|
||||||
"""This function combines :func:`echo` and :func:`style` into one
|
"""This function combines :func:`echo` and :func:`style` into one
|
||||||
call. As such the following two calls are the same::
|
call. As such the following two calls are the same::
|
||||||
|
|
||||||
|
@ -541,16 +640,31 @@ def secho(message=None, file=None, nl=True, err=False, color=None, **styles):
|
||||||
All keyword arguments are forwarded to the underlying functions
|
All keyword arguments are forwarded to the underlying functions
|
||||||
depending on which one they go with.
|
depending on which one they go with.
|
||||||
|
|
||||||
|
Non-string types will be converted to :class:`str`. However,
|
||||||
|
:class:`bytes` are passed directly to :meth:`echo` without applying
|
||||||
|
style. If you want to style bytes that represent text, call
|
||||||
|
:meth:`bytes.decode` first.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
A non-string ``message`` is converted to a string. Bytes are
|
||||||
|
passed through without style applied.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
if message is not None:
|
if message is not None and not isinstance(message, (bytes, bytearray)):
|
||||||
message = style(message, **styles)
|
message = style(message, **styles)
|
||||||
|
|
||||||
return echo(message, file=file, nl=nl, err=err, color=color)
|
return echo(message, file=file, nl=nl, err=err, color=color)
|
||||||
|
|
||||||
|
|
||||||
def edit(
|
def edit(
|
||||||
text=None, editor=None, env=None, require_save=True, extension=".txt", filename=None
|
text: t.Optional[t.AnyStr] = None,
|
||||||
):
|
editor: t.Optional[str] = None,
|
||||||
|
env: t.Optional[t.Mapping[str, str]] = None,
|
||||||
|
require_save: bool = True,
|
||||||
|
extension: str = ".txt",
|
||||||
|
filename: t.Optional[str] = None,
|
||||||
|
) -> t.Optional[t.AnyStr]:
|
||||||
r"""Edits the given text in the defined editor. If an editor is given
|
r"""Edits the given text in the defined editor. If an editor is given
|
||||||
(should be the full path to the executable but the regular operating
|
(should be the full path to the executable but the regular operating
|
||||||
system search path is used for finding the executable) it overrides
|
system search path is used for finding the executable) it overrides
|
||||||
|
@ -580,15 +694,16 @@ def edit(
|
||||||
"""
|
"""
|
||||||
from ._termui_impl import Editor
|
from ._termui_impl import Editor
|
||||||
|
|
||||||
editor = Editor(
|
ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension)
|
||||||
editor=editor, env=env, require_save=require_save, extension=extension
|
|
||||||
)
|
|
||||||
if filename is None:
|
if filename is None:
|
||||||
return editor.edit(text)
|
return ed.edit(text)
|
||||||
editor.edit_file(filename)
|
|
||||||
|
ed.edit_file(filename)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def launch(url, wait=False, locate=False):
|
def launch(url: str, wait: bool = False, locate: bool = False) -> int:
|
||||||
"""This function launches the given URL (or filename) in the default
|
"""This function launches the given URL (or filename) in the default
|
||||||
viewer application for this file type. If this is an executable, it
|
viewer application for this file type. If this is an executable, it
|
||||||
might launch the executable in a new session. The return value is
|
might launch the executable in a new session. The return value is
|
||||||
|
@ -603,7 +718,9 @@ def launch(url, wait=False, locate=False):
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
:param url: URL or filename of the thing to launch.
|
:param url: URL or filename of the thing to launch.
|
||||||
:param wait: waits for the program to stop.
|
:param wait: Wait for the program to exit before returning. This
|
||||||
|
only works if the launched program blocks. In particular,
|
||||||
|
``xdg-open`` on Linux does not block.
|
||||||
:param locate: if this is set to `True` then instead of launching the
|
:param locate: if this is set to `True` then instead of launching the
|
||||||
application associated with the URL it will attempt to
|
application associated with the URL it will attempt to
|
||||||
launch a file manager with the file located. This
|
launch a file manager with the file located. This
|
||||||
|
@ -617,10 +734,10 @@ def launch(url, wait=False, locate=False):
|
||||||
|
|
||||||
# If this is provided, getchar() calls into this instead. This is used
|
# If this is provided, getchar() calls into this instead. This is used
|
||||||
# for unittesting purposes.
|
# for unittesting purposes.
|
||||||
_getchar = None
|
_getchar: t.Optional[t.Callable[[bool], str]] = None
|
||||||
|
|
||||||
|
|
||||||
def getchar(echo=False):
|
def getchar(echo: bool = False) -> str:
|
||||||
"""Fetches a single character from the terminal and returns it. This
|
"""Fetches a single character from the terminal and returns it. This
|
||||||
will always return a unicode character and under certain rare
|
will always return a unicode character and under certain rare
|
||||||
circumstances this might return more than one character. The
|
circumstances this might return more than one character. The
|
||||||
|
@ -640,19 +757,23 @@ def getchar(echo=False):
|
||||||
:param echo: if set to `True`, the character read will also show up on
|
:param echo: if set to `True`, the character read will also show up on
|
||||||
the terminal. The default is to not show it.
|
the terminal. The default is to not show it.
|
||||||
"""
|
"""
|
||||||
f = _getchar
|
global _getchar
|
||||||
if f is None:
|
|
||||||
|
if _getchar is None:
|
||||||
from ._termui_impl import getchar as f
|
from ._termui_impl import getchar as f
|
||||||
return f(echo)
|
|
||||||
|
_getchar = f
|
||||||
|
|
||||||
|
return _getchar(echo)
|
||||||
|
|
||||||
|
|
||||||
def raw_terminal():
|
def raw_terminal() -> t.ContextManager[int]:
|
||||||
from ._termui_impl import raw_terminal as f
|
from ._termui_impl import raw_terminal as f
|
||||||
|
|
||||||
return f()
|
return f()
|
||||||
|
|
||||||
|
|
||||||
def pause(info="Press any key to continue ...", err=False):
|
def pause(info: t.Optional[str] = None, err: bool = False) -> None:
|
||||||
"""This command stops execution and waits for the user to press any
|
"""This command stops execution and waits for the user to press any
|
||||||
key to continue. This is similar to the Windows batch "pause"
|
key to continue. This is similar to the Windows batch "pause"
|
||||||
command. If the program is not run through a terminal, this command
|
command. If the program is not run through a terminal, this command
|
||||||
|
@ -663,12 +784,17 @@ def pause(info="Press any key to continue ...", err=False):
|
||||||
.. versionadded:: 4.0
|
.. versionadded:: 4.0
|
||||||
Added the `err` parameter.
|
Added the `err` parameter.
|
||||||
|
|
||||||
:param info: the info string to print before pausing.
|
:param info: The message to print before pausing. Defaults to
|
||||||
|
``"Press any key to continue..."``.
|
||||||
:param err: if set to message goes to ``stderr`` instead of
|
:param err: if set to message goes to ``stderr`` instead of
|
||||||
``stdout``, the same as with echo.
|
``stdout``, the same as with echo.
|
||||||
"""
|
"""
|
||||||
if not isatty(sys.stdin) or not isatty(sys.stdout):
|
if not isatty(sys.stdin) or not isatty(sys.stdout):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if info is None:
|
||||||
|
info = _("Press any key to continue...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if info:
|
if info:
|
||||||
echo(info, nl=False, err=err)
|
echo(info, nl=False, err=err)
|
||||||
|
|
|
@ -1,77 +1,117 @@
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import typing as t
|
||||||
|
from types import TracebackType
|
||||||
|
|
||||||
from . import formatting
|
from . import formatting
|
||||||
from . import termui
|
from . import termui
|
||||||
from . import utils
|
from . import utils
|
||||||
from ._compat import iteritems
|
from ._compat import _find_binary_reader
|
||||||
from ._compat import PY2
|
|
||||||
from ._compat import string_types
|
if t.TYPE_CHECKING:
|
||||||
|
from .core import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
if PY2:
|
class EchoingStdin:
|
||||||
from cStringIO import StringIO
|
def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None:
|
||||||
else:
|
|
||||||
import io
|
|
||||||
from ._compat import _find_binary_reader
|
|
||||||
|
|
||||||
|
|
||||||
class EchoingStdin(object):
|
|
||||||
def __init__(self, input, output):
|
|
||||||
self._input = input
|
self._input = input
|
||||||
self._output = output
|
self._output = output
|
||||||
|
self._paused = False
|
||||||
|
|
||||||
def __getattr__(self, x):
|
def __getattr__(self, x: str) -> t.Any:
|
||||||
return getattr(self._input, x)
|
return getattr(self._input, x)
|
||||||
|
|
||||||
def _echo(self, rv):
|
def _echo(self, rv: bytes) -> bytes:
|
||||||
self._output.write(rv)
|
if not self._paused:
|
||||||
|
self._output.write(rv)
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def read(self, n=-1):
|
def read(self, n: int = -1) -> bytes:
|
||||||
return self._echo(self._input.read(n))
|
return self._echo(self._input.read(n))
|
||||||
|
|
||||||
def readline(self, n=-1):
|
def read1(self, n: int = -1) -> bytes:
|
||||||
|
return self._echo(self._input.read1(n)) # type: ignore
|
||||||
|
|
||||||
|
def readline(self, n: int = -1) -> bytes:
|
||||||
return self._echo(self._input.readline(n))
|
return self._echo(self._input.readline(n))
|
||||||
|
|
||||||
def readlines(self):
|
def readlines(self) -> t.List[bytes]:
|
||||||
return [self._echo(x) for x in self._input.readlines()]
|
return [self._echo(x) for x in self._input.readlines()]
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self) -> t.Iterator[bytes]:
|
||||||
return iter(self._echo(x) for x in self._input)
|
return iter(self._echo(x) for x in self._input)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return repr(self._input)
|
return repr(self._input)
|
||||||
|
|
||||||
|
|
||||||
def make_input_stream(input, charset):
|
@contextlib.contextmanager
|
||||||
|
def _pause_echo(stream: t.Optional[EchoingStdin]) -> t.Iterator[None]:
|
||||||
|
if stream is None:
|
||||||
|
yield
|
||||||
|
else:
|
||||||
|
stream._paused = True
|
||||||
|
yield
|
||||||
|
stream._paused = False
|
||||||
|
|
||||||
|
|
||||||
|
class _NamedTextIOWrapper(io.TextIOWrapper):
|
||||||
|
def __init__(
|
||||||
|
self, buffer: t.BinaryIO, name: str, mode: str, **kwargs: t.Any
|
||||||
|
) -> None:
|
||||||
|
super().__init__(buffer, **kwargs)
|
||||||
|
self._name = name
|
||||||
|
self._mode = mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mode(self) -> str:
|
||||||
|
return self._mode
|
||||||
|
|
||||||
|
|
||||||
|
def make_input_stream(
|
||||||
|
input: t.Optional[t.Union[str, bytes, t.IO]], charset: str
|
||||||
|
) -> t.BinaryIO:
|
||||||
# Is already an input stream.
|
# Is already an input stream.
|
||||||
if hasattr(input, "read"):
|
if hasattr(input, "read"):
|
||||||
if PY2:
|
rv = _find_binary_reader(t.cast(t.IO, input))
|
||||||
return input
|
|
||||||
rv = _find_binary_reader(input)
|
|
||||||
if rv is not None:
|
if rv is not None:
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
raise TypeError("Could not find binary reader for input stream.")
|
raise TypeError("Could not find binary reader for input stream.")
|
||||||
|
|
||||||
if input is None:
|
if input is None:
|
||||||
input = b""
|
input = b""
|
||||||
elif not isinstance(input, bytes):
|
elif isinstance(input, str):
|
||||||
input = input.encode(charset)
|
input = input.encode(charset)
|
||||||
if PY2:
|
|
||||||
return StringIO(input)
|
return io.BytesIO(t.cast(bytes, input))
|
||||||
return io.BytesIO(input)
|
|
||||||
|
|
||||||
|
|
||||||
class Result(object):
|
class Result:
|
||||||
"""Holds the captured result of an invoked CLI script."""
|
"""Holds the captured result of an invoked CLI script."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, runner, stdout_bytes, stderr_bytes, exit_code, exception, exc_info=None
|
self,
|
||||||
|
runner: "CliRunner",
|
||||||
|
stdout_bytes: bytes,
|
||||||
|
stderr_bytes: t.Optional[bytes],
|
||||||
|
return_value: t.Any,
|
||||||
|
exit_code: int,
|
||||||
|
exception: t.Optional[BaseException],
|
||||||
|
exc_info: t.Optional[
|
||||||
|
t.Tuple[t.Type[BaseException], BaseException, TracebackType]
|
||||||
|
] = None,
|
||||||
):
|
):
|
||||||
#: The runner that created the result
|
#: The runner that created the result
|
||||||
self.runner = runner
|
self.runner = runner
|
||||||
|
@ -79,6 +119,10 @@ class Result(object):
|
||||||
self.stdout_bytes = stdout_bytes
|
self.stdout_bytes = stdout_bytes
|
||||||
#: The standard error as bytes, or None if not available
|
#: The standard error as bytes, or None if not available
|
||||||
self.stderr_bytes = stderr_bytes
|
self.stderr_bytes = stderr_bytes
|
||||||
|
#: The value returned from the invoked command.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 8.0
|
||||||
|
self.return_value = return_value
|
||||||
#: The exit code as integer.
|
#: The exit code as integer.
|
||||||
self.exit_code = exit_code
|
self.exit_code = exit_code
|
||||||
#: The exception that happened if one did.
|
#: The exception that happened if one did.
|
||||||
|
@ -87,19 +131,19 @@ class Result(object):
|
||||||
self.exc_info = exc_info
|
self.exc_info = exc_info
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def output(self):
|
def output(self) -> str:
|
||||||
"""The (standard) output as unicode string."""
|
"""The (standard) output as unicode string."""
|
||||||
return self.stdout
|
return self.stdout
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stdout(self):
|
def stdout(self) -> str:
|
||||||
"""The standard output as unicode string."""
|
"""The standard output as unicode string."""
|
||||||
return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
|
return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
|
||||||
"\r\n", "\n"
|
"\r\n", "\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stderr(self):
|
def stderr(self) -> str:
|
||||||
"""The standard error as unicode string."""
|
"""The standard error as unicode string."""
|
||||||
if self.stderr_bytes is None:
|
if self.stderr_bytes is None:
|
||||||
raise ValueError("stderr not separately captured")
|
raise ValueError("stderr not separately captured")
|
||||||
|
@ -107,21 +151,18 @@ class Result(object):
|
||||||
"\r\n", "\n"
|
"\r\n", "\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return "<{} {}>".format(
|
exc_str = repr(self.exception) if self.exception else "okay"
|
||||||
type(self).__name__, repr(self.exception) if self.exception else "okay"
|
return f"<{type(self).__name__} {exc_str}>"
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CliRunner(object):
|
class CliRunner:
|
||||||
"""The CLI runner provides functionality to invoke a Click command line
|
"""The CLI runner provides functionality to invoke a Click command line
|
||||||
script for unittesting purposes in a isolated environment. This only
|
script for unittesting purposes in a isolated environment. This only
|
||||||
works in single-threaded systems without any concurrency as it changes the
|
works in single-threaded systems without any concurrency as it changes the
|
||||||
global interpreter state.
|
global interpreter state.
|
||||||
|
|
||||||
:param charset: the character set for the input and output data. This is
|
:param charset: the character set for the input and output data.
|
||||||
UTF-8 by default and should not be changed currently as
|
|
||||||
the reporting to Click only works in Python 2 properly.
|
|
||||||
:param env: a dictionary with environment variables for overriding.
|
:param env: a dictionary with environment variables for overriding.
|
||||||
:param echo_stdin: if this is set to `True`, then reading from stdin writes
|
:param echo_stdin: if this is set to `True`, then reading from stdin writes
|
||||||
to stdout. This is useful for showing examples in
|
to stdout. This is useful for showing examples in
|
||||||
|
@ -134,22 +175,28 @@ class CliRunner(object):
|
||||||
independently
|
independently
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, charset=None, env=None, echo_stdin=False, mix_stderr=True):
|
def __init__(
|
||||||
if charset is None:
|
self,
|
||||||
charset = "utf-8"
|
charset: str = "utf-8",
|
||||||
|
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
|
||||||
|
echo_stdin: bool = False,
|
||||||
|
mix_stderr: bool = True,
|
||||||
|
) -> None:
|
||||||
self.charset = charset
|
self.charset = charset
|
||||||
self.env = env or {}
|
self.env = env or {}
|
||||||
self.echo_stdin = echo_stdin
|
self.echo_stdin = echo_stdin
|
||||||
self.mix_stderr = mix_stderr
|
self.mix_stderr = mix_stderr
|
||||||
|
|
||||||
def get_default_prog_name(self, cli):
|
def get_default_prog_name(self, cli: "BaseCommand") -> str:
|
||||||
"""Given a command object it will return the default program name
|
"""Given a command object it will return the default program name
|
||||||
for it. The default is the `name` attribute or ``"root"`` if not
|
for it. The default is the `name` attribute or ``"root"`` if not
|
||||||
set.
|
set.
|
||||||
"""
|
"""
|
||||||
return cli.name or "root"
|
return cli.name or "root"
|
||||||
|
|
||||||
def make_env(self, overrides=None):
|
def make_env(
|
||||||
|
self, overrides: t.Optional[t.Mapping[str, t.Optional[str]]] = None
|
||||||
|
) -> t.Mapping[str, t.Optional[str]]:
|
||||||
"""Returns the environment overrides for invoking a script."""
|
"""Returns the environment overrides for invoking a script."""
|
||||||
rv = dict(self.env)
|
rv = dict(self.env)
|
||||||
if overrides:
|
if overrides:
|
||||||
|
@ -157,7 +204,12 @@ class CliRunner(object):
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def isolation(self, input=None, env=None, color=False):
|
def isolation(
|
||||||
|
self,
|
||||||
|
input: t.Optional[t.Union[str, bytes, t.IO]] = None,
|
||||||
|
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
|
||||||
|
color: bool = False,
|
||||||
|
) -> t.Iterator[t.Tuple[io.BytesIO, t.Optional[io.BytesIO]]]:
|
||||||
"""A context manager that sets up the isolation for invoking of a
|
"""A context manager that sets up the isolation for invoking of a
|
||||||
command line tool. This sets up stdin with the given input data
|
command line tool. This sets up stdin with the given input data
|
||||||
and `os.environ` with the overrides from the given dictionary.
|
and `os.environ` with the overrides from the given dictionary.
|
||||||
|
@ -166,15 +218,20 @@ class CliRunner(object):
|
||||||
|
|
||||||
This is automatically done in the :meth:`invoke` method.
|
This is automatically done in the :meth:`invoke` method.
|
||||||
|
|
||||||
.. versionadded:: 4.0
|
|
||||||
The ``color`` parameter was added.
|
|
||||||
|
|
||||||
:param input: the input stream to put into sys.stdin.
|
:param input: the input stream to put into sys.stdin.
|
||||||
:param env: the environment overrides as dictionary.
|
:param env: the environment overrides as dictionary.
|
||||||
:param color: whether the output should contain color codes. The
|
:param color: whether the output should contain color codes. The
|
||||||
application can still override this explicitly.
|
application can still override this explicitly.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
``stderr`` is opened with ``errors="backslashreplace"``
|
||||||
|
instead of the default ``"strict"``.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
Added the ``color`` parameter.
|
||||||
"""
|
"""
|
||||||
input = make_input_stream(input, self.charset)
|
bytes_input = make_input_stream(input, self.charset)
|
||||||
|
echo_input = None
|
||||||
|
|
||||||
old_stdin = sys.stdin
|
old_stdin = sys.stdin
|
||||||
old_stdout = sys.stdout
|
old_stdout = sys.stdout
|
||||||
|
@ -184,51 +241,68 @@ class CliRunner(object):
|
||||||
|
|
||||||
env = self.make_env(env)
|
env = self.make_env(env)
|
||||||
|
|
||||||
if PY2:
|
bytes_output = io.BytesIO()
|
||||||
bytes_output = StringIO()
|
|
||||||
if self.echo_stdin:
|
|
||||||
input = EchoingStdin(input, bytes_output)
|
|
||||||
sys.stdout = bytes_output
|
|
||||||
if not self.mix_stderr:
|
|
||||||
bytes_error = StringIO()
|
|
||||||
sys.stderr = bytes_error
|
|
||||||
else:
|
|
||||||
bytes_output = io.BytesIO()
|
|
||||||
if self.echo_stdin:
|
|
||||||
input = EchoingStdin(input, bytes_output)
|
|
||||||
input = io.TextIOWrapper(input, encoding=self.charset)
|
|
||||||
sys.stdout = io.TextIOWrapper(bytes_output, encoding=self.charset)
|
|
||||||
if not self.mix_stderr:
|
|
||||||
bytes_error = io.BytesIO()
|
|
||||||
sys.stderr = io.TextIOWrapper(bytes_error, encoding=self.charset)
|
|
||||||
|
|
||||||
|
if self.echo_stdin:
|
||||||
|
bytes_input = echo_input = t.cast(
|
||||||
|
t.BinaryIO, EchoingStdin(bytes_input, bytes_output)
|
||||||
|
)
|
||||||
|
|
||||||
|
sys.stdin = text_input = _NamedTextIOWrapper(
|
||||||
|
bytes_input, encoding=self.charset, name="<stdin>", mode="r"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.echo_stdin:
|
||||||
|
# Force unbuffered reads, otherwise TextIOWrapper reads a
|
||||||
|
# large chunk which is echoed early.
|
||||||
|
text_input._CHUNK_SIZE = 1 # type: ignore
|
||||||
|
|
||||||
|
sys.stdout = _NamedTextIOWrapper(
|
||||||
|
bytes_output, encoding=self.charset, name="<stdout>", mode="w"
|
||||||
|
)
|
||||||
|
|
||||||
|
bytes_error = None
|
||||||
if self.mix_stderr:
|
if self.mix_stderr:
|
||||||
sys.stderr = sys.stdout
|
sys.stderr = sys.stdout
|
||||||
|
else:
|
||||||
|
bytes_error = io.BytesIO()
|
||||||
|
sys.stderr = _NamedTextIOWrapper(
|
||||||
|
bytes_error,
|
||||||
|
encoding=self.charset,
|
||||||
|
name="<stderr>",
|
||||||
|
mode="w",
|
||||||
|
errors="backslashreplace",
|
||||||
|
)
|
||||||
|
|
||||||
sys.stdin = input
|
@_pause_echo(echo_input) # type: ignore
|
||||||
|
def visible_input(prompt: t.Optional[str] = None) -> str:
|
||||||
def visible_input(prompt=None):
|
|
||||||
sys.stdout.write(prompt or "")
|
sys.stdout.write(prompt or "")
|
||||||
val = input.readline().rstrip("\r\n")
|
val = text_input.readline().rstrip("\r\n")
|
||||||
sys.stdout.write("{}\n".format(val))
|
sys.stdout.write(f"{val}\n")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def hidden_input(prompt=None):
|
@_pause_echo(echo_input) # type: ignore
|
||||||
sys.stdout.write("{}\n".format(prompt or ""))
|
def hidden_input(prompt: t.Optional[str] = None) -> str:
|
||||||
|
sys.stdout.write(f"{prompt or ''}\n")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
return input.readline().rstrip("\r\n")
|
return text_input.readline().rstrip("\r\n")
|
||||||
|
|
||||||
def _getchar(echo):
|
@_pause_echo(echo_input) # type: ignore
|
||||||
|
def _getchar(echo: bool) -> str:
|
||||||
char = sys.stdin.read(1)
|
char = sys.stdin.read(1)
|
||||||
|
|
||||||
if echo:
|
if echo:
|
||||||
sys.stdout.write(char)
|
sys.stdout.write(char)
|
||||||
sys.stdout.flush()
|
|
||||||
|
sys.stdout.flush()
|
||||||
return char
|
return char
|
||||||
|
|
||||||
default_color = color
|
default_color = color
|
||||||
|
|
||||||
def should_strip_ansi(stream=None, color=None):
|
def should_strip_ansi(
|
||||||
|
stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None
|
||||||
|
) -> bool:
|
||||||
if color is None:
|
if color is None:
|
||||||
return not default_color
|
return not default_color
|
||||||
return not color
|
return not color
|
||||||
|
@ -236,15 +310,15 @@ class CliRunner(object):
|
||||||
old_visible_prompt_func = termui.visible_prompt_func
|
old_visible_prompt_func = termui.visible_prompt_func
|
||||||
old_hidden_prompt_func = termui.hidden_prompt_func
|
old_hidden_prompt_func = termui.hidden_prompt_func
|
||||||
old__getchar_func = termui._getchar
|
old__getchar_func = termui._getchar
|
||||||
old_should_strip_ansi = utils.should_strip_ansi
|
old_should_strip_ansi = utils.should_strip_ansi # type: ignore
|
||||||
termui.visible_prompt_func = visible_input
|
termui.visible_prompt_func = visible_input
|
||||||
termui.hidden_prompt_func = hidden_input
|
termui.hidden_prompt_func = hidden_input
|
||||||
termui._getchar = _getchar
|
termui._getchar = _getchar
|
||||||
utils.should_strip_ansi = should_strip_ansi
|
utils.should_strip_ansi = should_strip_ansi # type: ignore
|
||||||
|
|
||||||
old_env = {}
|
old_env = {}
|
||||||
try:
|
try:
|
||||||
for key, value in iteritems(env):
|
for key, value in env.items():
|
||||||
old_env[key] = os.environ.get(key)
|
old_env[key] = os.environ.get(key)
|
||||||
if value is None:
|
if value is None:
|
||||||
try:
|
try:
|
||||||
|
@ -253,9 +327,9 @@ class CliRunner(object):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
os.environ[key] = value
|
os.environ[key] = value
|
||||||
yield (bytes_output, not self.mix_stderr and bytes_error)
|
yield (bytes_output, bytes_error)
|
||||||
finally:
|
finally:
|
||||||
for key, value in iteritems(old_env):
|
for key, value in old_env.items():
|
||||||
if value is None:
|
if value is None:
|
||||||
try:
|
try:
|
||||||
del os.environ[key]
|
del os.environ[key]
|
||||||
|
@ -269,19 +343,19 @@ class CliRunner(object):
|
||||||
termui.visible_prompt_func = old_visible_prompt_func
|
termui.visible_prompt_func = old_visible_prompt_func
|
||||||
termui.hidden_prompt_func = old_hidden_prompt_func
|
termui.hidden_prompt_func = old_hidden_prompt_func
|
||||||
termui._getchar = old__getchar_func
|
termui._getchar = old__getchar_func
|
||||||
utils.should_strip_ansi = old_should_strip_ansi
|
utils.should_strip_ansi = old_should_strip_ansi # type: ignore
|
||||||
formatting.FORCED_WIDTH = old_forced_width
|
formatting.FORCED_WIDTH = old_forced_width
|
||||||
|
|
||||||
def invoke(
|
def invoke(
|
||||||
self,
|
self,
|
||||||
cli,
|
cli: "BaseCommand",
|
||||||
args=None,
|
args: t.Optional[t.Union[str, t.Sequence[str]]] = None,
|
||||||
input=None,
|
input: t.Optional[t.Union[str, bytes, t.IO]] = None,
|
||||||
env=None,
|
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
|
||||||
catch_exceptions=True,
|
catch_exceptions: bool = True,
|
||||||
color=False,
|
color: bool = False,
|
||||||
**extra
|
**extra: t.Any,
|
||||||
):
|
) -> Result:
|
||||||
"""Invokes a command in an isolated environment. The arguments are
|
"""Invokes a command in an isolated environment. The arguments are
|
||||||
forwarded directly to the command line script, the `extra` keyword
|
forwarded directly to the command line script, the `extra` keyword
|
||||||
arguments are passed to the :meth:`~clickpkg.Command.main` function of
|
arguments are passed to the :meth:`~clickpkg.Command.main` function of
|
||||||
|
@ -289,16 +363,6 @@ class CliRunner(object):
|
||||||
|
|
||||||
This returns a :class:`Result` object.
|
This returns a :class:`Result` object.
|
||||||
|
|
||||||
.. versionadded:: 3.0
|
|
||||||
The ``catch_exceptions`` parameter was added.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
The result object now has an `exc_info` attribute with the
|
|
||||||
traceback if available.
|
|
||||||
|
|
||||||
.. versionadded:: 4.0
|
|
||||||
The ``color`` parameter was added.
|
|
||||||
|
|
||||||
:param cli: the command to invoke
|
:param cli: the command to invoke
|
||||||
:param args: the arguments to invoke. It may be given as an iterable
|
:param args: the arguments to invoke. It may be given as an iterable
|
||||||
or a string. When given as string it will be interpreted
|
or a string. When given as string it will be interpreted
|
||||||
|
@ -311,13 +375,28 @@ class CliRunner(object):
|
||||||
:param extra: the keyword arguments to pass to :meth:`main`.
|
:param extra: the keyword arguments to pass to :meth:`main`.
|
||||||
:param color: whether the output should contain color codes. The
|
:param color: whether the output should contain color codes. The
|
||||||
application can still override this explicitly.
|
application can still override this explicitly.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
The result object has the ``return_value`` attribute with
|
||||||
|
the value returned from the invoked command.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
Added the ``color`` parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Added the ``catch_exceptions`` parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
The result object has the ``exc_info`` attribute with the
|
||||||
|
traceback if available.
|
||||||
"""
|
"""
|
||||||
exc_info = None
|
exc_info = None
|
||||||
with self.isolation(input=input, env=env, color=color) as outstreams:
|
with self.isolation(input=input, env=env, color=color) as outstreams:
|
||||||
exception = None
|
return_value = None
|
||||||
|
exception: t.Optional[BaseException] = None
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
|
|
||||||
if isinstance(args, string_types):
|
if isinstance(args, str):
|
||||||
args = shlex.split(args)
|
args = shlex.split(args)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -326,20 +405,23 @@ class CliRunner(object):
|
||||||
prog_name = self.get_default_prog_name(cli)
|
prog_name = self.get_default_prog_name(cli)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cli.main(args=args or (), prog_name=prog_name, **extra)
|
return_value = cli.main(args=args or (), prog_name=prog_name, **extra)
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
exc_info = sys.exc_info()
|
exc_info = sys.exc_info()
|
||||||
exit_code = e.code
|
e_code = t.cast(t.Optional[t.Union[int, t.Any]], e.code)
|
||||||
if exit_code is None:
|
|
||||||
exit_code = 0
|
|
||||||
|
|
||||||
if exit_code != 0:
|
if e_code is None:
|
||||||
|
e_code = 0
|
||||||
|
|
||||||
|
if e_code != 0:
|
||||||
exception = e
|
exception = e
|
||||||
|
|
||||||
if not isinstance(exit_code, int):
|
if not isinstance(e_code, int):
|
||||||
sys.stdout.write(str(exit_code))
|
sys.stdout.write(str(e_code))
|
||||||
sys.stdout.write("\n")
|
sys.stdout.write("\n")
|
||||||
exit_code = 1
|
e_code = 1
|
||||||
|
|
||||||
|
exit_code = e_code
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not catch_exceptions:
|
if not catch_exceptions:
|
||||||
|
@ -353,30 +435,45 @@ class CliRunner(object):
|
||||||
if self.mix_stderr:
|
if self.mix_stderr:
|
||||||
stderr = None
|
stderr = None
|
||||||
else:
|
else:
|
||||||
stderr = outstreams[1].getvalue()
|
stderr = outstreams[1].getvalue() # type: ignore
|
||||||
|
|
||||||
return Result(
|
return Result(
|
||||||
runner=self,
|
runner=self,
|
||||||
stdout_bytes=stdout,
|
stdout_bytes=stdout,
|
||||||
stderr_bytes=stderr,
|
stderr_bytes=stderr,
|
||||||
|
return_value=return_value,
|
||||||
exit_code=exit_code,
|
exit_code=exit_code,
|
||||||
exception=exception,
|
exception=exception,
|
||||||
exc_info=exc_info,
|
exc_info=exc_info, # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def isolated_filesystem(self):
|
def isolated_filesystem(
|
||||||
"""A context manager that creates a temporary folder and changes
|
self, temp_dir: t.Optional[t.Union[str, os.PathLike]] = None
|
||||||
the current working directory to it for isolated filesystem tests.
|
) -> t.Iterator[str]:
|
||||||
|
"""A context manager that creates a temporary directory and
|
||||||
|
changes the current working directory to it. This isolates tests
|
||||||
|
that affect the contents of the CWD to prevent them from
|
||||||
|
interfering with each other.
|
||||||
|
|
||||||
|
:param temp_dir: Create the temporary directory under this
|
||||||
|
directory. If given, the created directory is not removed
|
||||||
|
when exiting.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the ``temp_dir`` parameter.
|
||||||
"""
|
"""
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
t = tempfile.mkdtemp()
|
t = tempfile.mkdtemp(dir=temp_dir)
|
||||||
os.chdir(t)
|
os.chdir(t)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield t
|
yield t
|
||||||
finally:
|
finally:
|
||||||
os.chdir(cwd)
|
os.chdir(cwd)
|
||||||
try:
|
|
||||||
shutil.rmtree(t)
|
if temp_dir is None:
|
||||||
except (OSError, IOError): # noqa: B014
|
try:
|
||||||
pass
|
shutil.rmtree(t)
|
||||||
|
except OSError: # noqa: B014
|
||||||
|
pass
|
||||||
|
|
|
@ -1,37 +1,47 @@
|
||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
|
import typing as t
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from gettext import gettext as _
|
||||||
|
from gettext import ngettext
|
||||||
|
|
||||||
from ._compat import _get_argv_encoding
|
from ._compat import _get_argv_encoding
|
||||||
from ._compat import filename_to_ui
|
|
||||||
from ._compat import get_filesystem_encoding
|
from ._compat import get_filesystem_encoding
|
||||||
from ._compat import get_streerror
|
|
||||||
from ._compat import open_stream
|
from ._compat import open_stream
|
||||||
from ._compat import PY2
|
|
||||||
from ._compat import text_type
|
|
||||||
from .exceptions import BadParameter
|
from .exceptions import BadParameter
|
||||||
from .utils import LazyFile
|
from .utils import LazyFile
|
||||||
from .utils import safecall
|
from .utils import safecall
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .core import Context
|
||||||
|
from .core import Parameter
|
||||||
|
from .shell_completion import CompletionItem
|
||||||
|
|
||||||
class ParamType(object):
|
|
||||||
"""Helper for converting values through types. The following is
|
|
||||||
necessary for a valid type:
|
|
||||||
|
|
||||||
* it needs a name
|
class ParamType:
|
||||||
* it needs to pass through None unchanged
|
"""Represents the type of a parameter. Validates and converts values
|
||||||
* it needs to convert from a string
|
from the command line or Python into the correct type.
|
||||||
* it needs to convert its result type through unchanged
|
|
||||||
(eg: needs to be idempotent)
|
To implement a custom type, subclass and implement at least the
|
||||||
* it needs to be able to deal with param and context being `None`.
|
following:
|
||||||
This can be the case when the object is used with prompt
|
|
||||||
inputs.
|
- The :attr:`name` class attribute must be set.
|
||||||
|
- Calling an instance of the type with ``None`` must return
|
||||||
|
``None``. This is already implemented by default.
|
||||||
|
- :meth:`convert` must convert string values to the correct type.
|
||||||
|
- :meth:`convert` must accept values that are already the correct
|
||||||
|
type.
|
||||||
|
- It must be able to convert a value if the ``ctx`` and ``param``
|
||||||
|
arguments are ``None``. This can occur when converting prompt
|
||||||
|
input.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
is_composite = False
|
is_composite: t.ClassVar[bool] = False
|
||||||
|
arity: t.ClassVar[int] = 1
|
||||||
|
|
||||||
#: the descriptive name of this type
|
#: the descriptive name of this type
|
||||||
name = None
|
name: str
|
||||||
|
|
||||||
#: if a list of this type is expected and the value is pulled from a
|
#: if a list of this type is expected and the value is pulled from a
|
||||||
#: string environment variable, this is what splits it up. `None`
|
#: string environment variable, this is what splits it up. `None`
|
||||||
|
@ -39,29 +49,66 @@ class ParamType(object):
|
||||||
#: whitespace splits them up. The exception are paths and files which
|
#: whitespace splits them up. The exception are paths and files which
|
||||||
#: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
|
#: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
|
||||||
#: Windows).
|
#: Windows).
|
||||||
envvar_list_splitter = None
|
envvar_list_splitter: t.ClassVar[t.Optional[str]] = None
|
||||||
|
|
||||||
def __call__(self, value, param=None, ctx=None):
|
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||||
|
"""Gather information that could be useful for a tool generating
|
||||||
|
user-facing documentation.
|
||||||
|
|
||||||
|
Use :meth:`click.Context.to_info_dict` to traverse the entire
|
||||||
|
CLI structure.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
# The class name without the "ParamType" suffix.
|
||||||
|
param_type = type(self).__name__.partition("ParamType")[0]
|
||||||
|
param_type = param_type.partition("ParameterType")[0]
|
||||||
|
return {"param_type": param_type, "name": self.name}
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
value: t.Any,
|
||||||
|
param: t.Optional["Parameter"] = None,
|
||||||
|
ctx: t.Optional["Context"] = None,
|
||||||
|
) -> t.Any:
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return self.convert(value, param, ctx)
|
return self.convert(value, param, ctx)
|
||||||
|
|
||||||
def get_metavar(self, param):
|
def get_metavar(self, param: "Parameter") -> t.Optional[str]:
|
||||||
"""Returns the metavar default for this param if it provides one."""
|
"""Returns the metavar default for this param if it provides one."""
|
||||||
|
|
||||||
def get_missing_message(self, param):
|
def get_missing_message(self, param: "Parameter") -> t.Optional[str]:
|
||||||
"""Optionally might return extra information about a missing
|
"""Optionally might return extra information about a missing
|
||||||
parameter.
|
parameter.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(
|
||||||
"""Converts the value. This is not invoked for values that are
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
`None` (the missing value).
|
) -> t.Any:
|
||||||
|
"""Convert the value to the correct type. This is not called if
|
||||||
|
the value is ``None`` (the missing value).
|
||||||
|
|
||||||
|
This must accept string values from the command line, as well as
|
||||||
|
values that are already the correct type. It may also convert
|
||||||
|
other compatible types.
|
||||||
|
|
||||||
|
The ``param`` and ``ctx`` arguments may be ``None`` in certain
|
||||||
|
situations, such as when converting prompt input.
|
||||||
|
|
||||||
|
If the value cannot be converted, call :meth:`fail` with a
|
||||||
|
descriptive message.
|
||||||
|
|
||||||
|
:param value: The value to convert.
|
||||||
|
:param param: The parameter that is using this type to convert
|
||||||
|
its value. May be ``None``.
|
||||||
|
:param ctx: The current context that arrived at this value. May
|
||||||
|
be ``None``.
|
||||||
"""
|
"""
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def split_envvar_value(self, rv):
|
def split_envvar_value(self, rv: str) -> t.Sequence[str]:
|
||||||
"""Given a value from an environment variable this splits it up
|
"""Given a value from an environment variable this splits it up
|
||||||
into small chunks depending on the defined envvar list splitter.
|
into small chunks depending on the defined envvar list splitter.
|
||||||
|
|
||||||
|
@ -71,49 +118,83 @@ class ParamType(object):
|
||||||
"""
|
"""
|
||||||
return (rv or "").split(self.envvar_list_splitter)
|
return (rv or "").split(self.envvar_list_splitter)
|
||||||
|
|
||||||
def fail(self, message, param=None, ctx=None):
|
def fail(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
param: t.Optional["Parameter"] = None,
|
||||||
|
ctx: t.Optional["Context"] = None,
|
||||||
|
) -> "t.NoReturn":
|
||||||
"""Helper method to fail with an invalid value message."""
|
"""Helper method to fail with an invalid value message."""
|
||||||
raise BadParameter(message, ctx=ctx, param=param)
|
raise BadParameter(message, ctx=ctx, param=param)
|
||||||
|
|
||||||
|
def shell_complete(
|
||||||
|
self, ctx: "Context", param: "Parameter", incomplete: str
|
||||||
|
) -> t.List["CompletionItem"]:
|
||||||
|
"""Return a list of
|
||||||
|
:class:`~click.shell_completion.CompletionItem` objects for the
|
||||||
|
incomplete value. Most types do not provide completions, but
|
||||||
|
some do, and this allows custom types to provide custom
|
||||||
|
completions as well.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for this command.
|
||||||
|
:param param: The parameter that is requesting completion.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class CompositeParamType(ParamType):
|
class CompositeParamType(ParamType):
|
||||||
is_composite = True
|
is_composite = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def arity(self):
|
def arity(self) -> int: # type: ignore
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class FuncParamType(ParamType):
|
class FuncParamType(ParamType):
|
||||||
def __init__(self, func):
|
def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None:
|
||||||
self.name = func.__name__
|
self.name = func.__name__
|
||||||
self.func = func
|
self.func = func
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||||
|
info_dict = super().to_info_dict()
|
||||||
|
info_dict["func"] = self.func
|
||||||
|
return info_dict
|
||||||
|
|
||||||
|
def convert(
|
||||||
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
|
) -> t.Any:
|
||||||
try:
|
try:
|
||||||
return self.func(value)
|
return self.func(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
try:
|
try:
|
||||||
value = text_type(value)
|
value = str(value)
|
||||||
except UnicodeError:
|
except UnicodeError:
|
||||||
value = str(value).decode("utf-8", "replace")
|
value = value.decode("utf-8", "replace")
|
||||||
|
|
||||||
self.fail(value, param, ctx)
|
self.fail(value, param, ctx)
|
||||||
|
|
||||||
|
|
||||||
class UnprocessedParamType(ParamType):
|
class UnprocessedParamType(ParamType):
|
||||||
name = "text"
|
name = "text"
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(
|
||||||
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
|
) -> t.Any:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return "UNPROCESSED"
|
return "UNPROCESSED"
|
||||||
|
|
||||||
|
|
||||||
class StringParamType(ParamType):
|
class StringParamType(ParamType):
|
||||||
name = "text"
|
name = "text"
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(
|
||||||
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
|
) -> t.Any:
|
||||||
if isinstance(value, bytes):
|
if isinstance(value, bytes):
|
||||||
enc = _get_argv_encoding()
|
enc = _get_argv_encoding()
|
||||||
try:
|
try:
|
||||||
|
@ -128,9 +209,9 @@ class StringParamType(ParamType):
|
||||||
else:
|
else:
|
||||||
value = value.decode("utf-8", "replace")
|
value = value.decode("utf-8", "replace")
|
||||||
return value
|
return value
|
||||||
return value
|
return str(value)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return "STRING"
|
return "STRING"
|
||||||
|
|
||||||
|
|
||||||
|
@ -153,17 +234,32 @@ class Choice(ParamType):
|
||||||
|
|
||||||
name = "choice"
|
name = "choice"
|
||||||
|
|
||||||
def __init__(self, choices, case_sensitive=True):
|
def __init__(self, choices: t.Sequence[str], case_sensitive: bool = True) -> None:
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
self.case_sensitive = case_sensitive
|
self.case_sensitive = case_sensitive
|
||||||
|
|
||||||
def get_metavar(self, param):
|
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||||
return "[{}]".format("|".join(self.choices))
|
info_dict = super().to_info_dict()
|
||||||
|
info_dict["choices"] = self.choices
|
||||||
|
info_dict["case_sensitive"] = self.case_sensitive
|
||||||
|
return info_dict
|
||||||
|
|
||||||
def get_missing_message(self, param):
|
def get_metavar(self, param: "Parameter") -> str:
|
||||||
return "Choose from:\n\t{}.".format(",\n\t".join(self.choices))
|
choices_str = "|".join(self.choices)
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
# Use curly braces to indicate a required argument.
|
||||||
|
if param.required and param.param_type_name == "argument":
|
||||||
|
return f"{{{choices_str}}}"
|
||||||
|
|
||||||
|
# Use square braces to indicate an option or optional argument.
|
||||||
|
return f"[{choices_str}]"
|
||||||
|
|
||||||
|
def get_missing_message(self, param: "Parameter") -> str:
|
||||||
|
return _("Choose from:\n\t{choices}").format(choices=",\n\t".join(self.choices))
|
||||||
|
|
||||||
|
def convert(
|
||||||
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
|
) -> t.Any:
|
||||||
# Match through normalization and case sensitivity
|
# Match through normalization and case sensitivity
|
||||||
# first do token_normalize_func, then lowercase
|
# first do token_normalize_func, then lowercase
|
||||||
# preserve original `value` to produce an accurate message in
|
# preserve original `value` to produce an accurate message in
|
||||||
|
@ -179,30 +275,51 @@ class Choice(ParamType):
|
||||||
}
|
}
|
||||||
|
|
||||||
if not self.case_sensitive:
|
if not self.case_sensitive:
|
||||||
if PY2:
|
normed_value = normed_value.casefold()
|
||||||
lower = str.lower
|
|
||||||
else:
|
|
||||||
lower = str.casefold
|
|
||||||
|
|
||||||
normed_value = lower(normed_value)
|
|
||||||
normed_choices = {
|
normed_choices = {
|
||||||
lower(normed_choice): original
|
normed_choice.casefold(): original
|
||||||
for normed_choice, original in normed_choices.items()
|
for normed_choice, original in normed_choices.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
if normed_value in normed_choices:
|
if normed_value in normed_choices:
|
||||||
return normed_choices[normed_value]
|
return normed_choices[normed_value]
|
||||||
|
|
||||||
|
choices_str = ", ".join(map(repr, self.choices))
|
||||||
self.fail(
|
self.fail(
|
||||||
"invalid choice: {}. (choose from {})".format(
|
ngettext(
|
||||||
value, ", ".join(self.choices)
|
"{value!r} is not {choice}.",
|
||||||
),
|
"{value!r} is not one of {choices}.",
|
||||||
|
len(self.choices),
|
||||||
|
).format(value=value, choice=choices_str, choices=choices_str),
|
||||||
param,
|
param,
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return "Choice('{}')".format(list(self.choices))
|
return f"Choice({list(self.choices)})"
|
||||||
|
|
||||||
|
def shell_complete(
|
||||||
|
self, ctx: "Context", param: "Parameter", incomplete: str
|
||||||
|
) -> t.List["CompletionItem"]:
|
||||||
|
"""Complete choices that start with the incomplete value.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for this command.
|
||||||
|
:param param: The parameter that is requesting completion.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
from click.shell_completion import CompletionItem
|
||||||
|
|
||||||
|
str_choices = map(str, self.choices)
|
||||||
|
|
||||||
|
if self.case_sensitive:
|
||||||
|
matched = (c for c in str_choices if c.startswith(incomplete))
|
||||||
|
else:
|
||||||
|
incomplete = incomplete.lower()
|
||||||
|
matched = (c for c in str_choices if c.lower().startswith(incomplete))
|
||||||
|
|
||||||
|
return [CompletionItem(c) for c in matched]
|
||||||
|
|
||||||
|
|
||||||
class DateTime(ParamType):
|
class DateTime(ParamType):
|
||||||
|
@ -228,212 +345,285 @@ class DateTime(ParamType):
|
||||||
|
|
||||||
name = "datetime"
|
name = "datetime"
|
||||||
|
|
||||||
def __init__(self, formats=None):
|
def __init__(self, formats: t.Optional[t.Sequence[str]] = None):
|
||||||
self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
|
self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
|
||||||
|
|
||||||
def get_metavar(self, param):
|
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||||
return "[{}]".format("|".join(self.formats))
|
info_dict = super().to_info_dict()
|
||||||
|
info_dict["formats"] = self.formats
|
||||||
|
return info_dict
|
||||||
|
|
||||||
def _try_to_convert_date(self, value, format):
|
def get_metavar(self, param: "Parameter") -> str:
|
||||||
|
return f"[{'|'.join(self.formats)}]"
|
||||||
|
|
||||||
|
def _try_to_convert_date(self, value: t.Any, format: str) -> t.Optional[datetime]:
|
||||||
try:
|
try:
|
||||||
return datetime.strptime(value, format)
|
return datetime.strptime(value, format)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(
|
||||||
# Exact match
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
for format in self.formats:
|
) -> t.Any:
|
||||||
dtime = self._try_to_convert_date(value, format)
|
if isinstance(value, datetime):
|
||||||
if dtime:
|
return value
|
||||||
return dtime
|
|
||||||
|
|
||||||
|
for format in self.formats:
|
||||||
|
converted = self._try_to_convert_date(value, format)
|
||||||
|
|
||||||
|
if converted is not None:
|
||||||
|
return converted
|
||||||
|
|
||||||
|
formats_str = ", ".join(map(repr, self.formats))
|
||||||
self.fail(
|
self.fail(
|
||||||
"invalid datetime format: {}. (choose from {})".format(
|
ngettext(
|
||||||
value, ", ".join(self.formats)
|
"{value!r} does not match the format {format}.",
|
||||||
)
|
"{value!r} does not match the formats {formats}.",
|
||||||
|
len(self.formats),
|
||||||
|
).format(value=value, format=formats_str, formats=formats_str),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return "DateTime"
|
return "DateTime"
|
||||||
|
|
||||||
|
|
||||||
class IntParamType(ParamType):
|
class _NumberParamTypeBase(ParamType):
|
||||||
name = "integer"
|
_number_class: t.ClassVar[t.Type]
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(
|
||||||
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
|
) -> t.Any:
|
||||||
try:
|
try:
|
||||||
return int(value)
|
return self._number_class(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.fail("{} is not a valid integer".format(value), param, ctx)
|
self.fail(
|
||||||
|
_("{value!r} is not a valid {number_type}.").format(
|
||||||
|
value=value, number_type=self.name
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
|
class _NumberRangeBase(_NumberParamTypeBase):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
min: t.Optional[float] = None,
|
||||||
|
max: t.Optional[float] = None,
|
||||||
|
min_open: bool = False,
|
||||||
|
max_open: bool = False,
|
||||||
|
clamp: bool = False,
|
||||||
|
) -> None:
|
||||||
|
self.min = min
|
||||||
|
self.max = max
|
||||||
|
self.min_open = min_open
|
||||||
|
self.max_open = max_open
|
||||||
|
self.clamp = clamp
|
||||||
|
|
||||||
|
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||||
|
info_dict = super().to_info_dict()
|
||||||
|
info_dict.update(
|
||||||
|
min=self.min,
|
||||||
|
max=self.max,
|
||||||
|
min_open=self.min_open,
|
||||||
|
max_open=self.max_open,
|
||||||
|
clamp=self.clamp,
|
||||||
|
)
|
||||||
|
return info_dict
|
||||||
|
|
||||||
|
def convert(
|
||||||
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
|
) -> t.Any:
|
||||||
|
import operator
|
||||||
|
|
||||||
|
rv = super().convert(value, param, ctx)
|
||||||
|
lt_min: bool = self.min is not None and (
|
||||||
|
operator.le if self.min_open else operator.lt
|
||||||
|
)(rv, self.min)
|
||||||
|
gt_max: bool = self.max is not None and (
|
||||||
|
operator.ge if self.max_open else operator.gt
|
||||||
|
)(rv, self.max)
|
||||||
|
|
||||||
|
if self.clamp:
|
||||||
|
if lt_min:
|
||||||
|
return self._clamp(self.min, 1, self.min_open) # type: ignore
|
||||||
|
|
||||||
|
if gt_max:
|
||||||
|
return self._clamp(self.max, -1, self.max_open) # type: ignore
|
||||||
|
|
||||||
|
if lt_min or gt_max:
|
||||||
|
self.fail(
|
||||||
|
_("{value} is not in the range {range}.").format(
|
||||||
|
value=rv, range=self._describe_range()
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float:
|
||||||
|
"""Find the valid value to clamp to bound in the given
|
||||||
|
direction.
|
||||||
|
|
||||||
|
:param bound: The boundary value.
|
||||||
|
:param dir: 1 or -1 indicating the direction to move.
|
||||||
|
:param open: If true, the range does not include the bound.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _describe_range(self) -> str:
|
||||||
|
"""Describe the range for use in help text."""
|
||||||
|
if self.min is None:
|
||||||
|
op = "<" if self.max_open else "<="
|
||||||
|
return f"x{op}{self.max}"
|
||||||
|
|
||||||
|
if self.max is None:
|
||||||
|
op = ">" if self.min_open else ">="
|
||||||
|
return f"x{op}{self.min}"
|
||||||
|
|
||||||
|
lop = "<" if self.min_open else "<="
|
||||||
|
rop = "<" if self.max_open else "<="
|
||||||
|
return f"{self.min}{lop}x{rop}{self.max}"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
clamp = " clamped" if self.clamp else ""
|
||||||
|
return f"<{type(self).__name__} {self._describe_range()}{clamp}>"
|
||||||
|
|
||||||
|
|
||||||
|
class IntParamType(_NumberParamTypeBase):
|
||||||
|
name = "integer"
|
||||||
|
_number_class = int
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
return "INT"
|
return "INT"
|
||||||
|
|
||||||
|
|
||||||
class IntRange(IntParamType):
|
class IntRange(_NumberRangeBase, IntParamType):
|
||||||
"""A parameter that works similar to :data:`click.INT` but restricts
|
"""Restrict an :data:`click.INT` value to a range of accepted
|
||||||
the value to fit into a range. The default behavior is to fail if the
|
values. See :ref:`ranges`.
|
||||||
value falls outside the range, but it can also be silently clamped
|
|
||||||
between the two edges.
|
|
||||||
|
|
||||||
See :ref:`ranges` for an example.
|
If ``min`` or ``max`` are not passed, any value is accepted in that
|
||||||
|
direction. If ``min_open`` or ``max_open`` are enabled, the
|
||||||
|
corresponding boundary is not included in the range.
|
||||||
|
|
||||||
|
If ``clamp`` is enabled, a value outside the range is clamped to the
|
||||||
|
boundary instead of failing.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the ``min_open`` and ``max_open`` parameters.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "integer range"
|
name = "integer range"
|
||||||
|
|
||||||
def __init__(self, min=None, max=None, clamp=False):
|
def _clamp( # type: ignore
|
||||||
self.min = min
|
self, bound: int, dir: "te.Literal[1, -1]", open: bool
|
||||||
self.max = max
|
) -> int:
|
||||||
self.clamp = clamp
|
if not open:
|
||||||
|
return bound
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
return bound + dir
|
||||||
rv = IntParamType.convert(self, value, param, ctx)
|
|
||||||
if self.clamp:
|
|
||||||
if self.min is not None and rv < self.min:
|
|
||||||
return self.min
|
|
||||||
if self.max is not None and rv > self.max:
|
|
||||||
return self.max
|
|
||||||
if (
|
|
||||||
self.min is not None
|
|
||||||
and rv < self.min
|
|
||||||
or self.max is not None
|
|
||||||
and rv > self.max
|
|
||||||
):
|
|
||||||
if self.min is None:
|
|
||||||
self.fail(
|
|
||||||
"{} is bigger than the maximum valid value {}.".format(
|
|
||||||
rv, self.max
|
|
||||||
),
|
|
||||||
param,
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
elif self.max is None:
|
|
||||||
self.fail(
|
|
||||||
"{} is smaller than the minimum valid value {}.".format(
|
|
||||||
rv, self.min
|
|
||||||
),
|
|
||||||
param,
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.fail(
|
|
||||||
"{} is not in the valid range of {} to {}.".format(
|
|
||||||
rv, self.min, self.max
|
|
||||||
),
|
|
||||||
param,
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "IntRange({}, {})".format(self.min, self.max)
|
|
||||||
|
|
||||||
|
|
||||||
class FloatParamType(ParamType):
|
class FloatParamType(_NumberParamTypeBase):
|
||||||
name = "float"
|
name = "float"
|
||||||
|
_number_class = float
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def __repr__(self) -> str:
|
||||||
try:
|
|
||||||
return float(value)
|
|
||||||
except ValueError:
|
|
||||||
self.fail(
|
|
||||||
"{} is not a valid floating point value".format(value), param, ctx
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "FLOAT"
|
return "FLOAT"
|
||||||
|
|
||||||
|
|
||||||
class FloatRange(FloatParamType):
|
class FloatRange(_NumberRangeBase, FloatParamType):
|
||||||
"""A parameter that works similar to :data:`click.FLOAT` but restricts
|
"""Restrict a :data:`click.FLOAT` value to a range of accepted
|
||||||
the value to fit into a range. The default behavior is to fail if the
|
values. See :ref:`ranges`.
|
||||||
value falls outside the range, but it can also be silently clamped
|
|
||||||
between the two edges.
|
|
||||||
|
|
||||||
See :ref:`ranges` for an example.
|
If ``min`` or ``max`` are not passed, any value is accepted in that
|
||||||
|
direction. If ``min_open`` or ``max_open`` are enabled, the
|
||||||
|
corresponding boundary is not included in the range.
|
||||||
|
|
||||||
|
If ``clamp`` is enabled, a value outside the range is clamped to the
|
||||||
|
boundary instead of failing. This is not supported if either
|
||||||
|
boundary is marked ``open``.
|
||||||
|
|
||||||
|
.. versionchanged:: 8.0
|
||||||
|
Added the ``min_open`` and ``max_open`` parameters.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "float range"
|
name = "float range"
|
||||||
|
|
||||||
def __init__(self, min=None, max=None, clamp=False):
|
def __init__(
|
||||||
self.min = min
|
self,
|
||||||
self.max = max
|
min: t.Optional[float] = None,
|
||||||
self.clamp = clamp
|
max: t.Optional[float] = None,
|
||||||
|
min_open: bool = False,
|
||||||
|
max_open: bool = False,
|
||||||
|
clamp: bool = False,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp
|
||||||
|
)
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
if (min_open or max_open) and clamp:
|
||||||
rv = FloatParamType.convert(self, value, param, ctx)
|
raise TypeError("Clamping is not supported for open bounds.")
|
||||||
if self.clamp:
|
|
||||||
if self.min is not None and rv < self.min:
|
|
||||||
return self.min
|
|
||||||
if self.max is not None and rv > self.max:
|
|
||||||
return self.max
|
|
||||||
if (
|
|
||||||
self.min is not None
|
|
||||||
and rv < self.min
|
|
||||||
or self.max is not None
|
|
||||||
and rv > self.max
|
|
||||||
):
|
|
||||||
if self.min is None:
|
|
||||||
self.fail(
|
|
||||||
"{} is bigger than the maximum valid value {}.".format(
|
|
||||||
rv, self.max
|
|
||||||
),
|
|
||||||
param,
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
elif self.max is None:
|
|
||||||
self.fail(
|
|
||||||
"{} is smaller than the minimum valid value {}.".format(
|
|
||||||
rv, self.min
|
|
||||||
),
|
|
||||||
param,
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.fail(
|
|
||||||
"{} is not in the valid range of {} to {}.".format(
|
|
||||||
rv, self.min, self.max
|
|
||||||
),
|
|
||||||
param,
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def __repr__(self):
|
def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float:
|
||||||
return "FloatRange({}, {})".format(self.min, self.max)
|
if not open:
|
||||||
|
return bound
|
||||||
|
|
||||||
|
# Could use Python 3.9's math.nextafter here, but clamping an
|
||||||
|
# open float range doesn't seem to be particularly useful. It's
|
||||||
|
# left up to the user to write a callback to do it if needed.
|
||||||
|
raise RuntimeError("Clamping is not supported for open bounds.")
|
||||||
|
|
||||||
|
|
||||||
class BoolParamType(ParamType):
|
class BoolParamType(ParamType):
|
||||||
name = "boolean"
|
name = "boolean"
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(
|
||||||
if isinstance(value, bool):
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
|
) -> t.Any:
|
||||||
|
if value in {False, True}:
|
||||||
return bool(value)
|
return bool(value)
|
||||||
value = value.lower()
|
|
||||||
if value in ("true", "t", "1", "yes", "y"):
|
|
||||||
return True
|
|
||||||
elif value in ("false", "f", "0", "no", "n"):
|
|
||||||
return False
|
|
||||||
self.fail("{} is not a valid boolean".format(value), param, ctx)
|
|
||||||
|
|
||||||
def __repr__(self):
|
norm = value.strip().lower()
|
||||||
|
|
||||||
|
if norm in {"1", "true", "t", "yes", "y", "on"}:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if norm in {"0", "false", "f", "no", "n", "off"}:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.fail(
|
||||||
|
_("{value!r} is not a valid boolean.").format(value=value), param, ctx
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
return "BOOL"
|
return "BOOL"
|
||||||
|
|
||||||
|
|
||||||
class UUIDParameterType(ParamType):
|
class UUIDParameterType(ParamType):
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(
|
||||||
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
|
) -> t.Any:
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
if isinstance(value, uuid.UUID):
|
||||||
|
return value
|
||||||
|
|
||||||
|
value = value.strip()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if PY2 and isinstance(value, text_type):
|
|
||||||
value = value.encode("ascii")
|
|
||||||
return uuid.UUID(value)
|
return uuid.UUID(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.fail("{} is not a valid UUID value".format(value), param, ctx)
|
self.fail(
|
||||||
|
_("{value!r} is not a valid UUID.").format(value=value), param, ctx
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return "UUID"
|
return "UUID"
|
||||||
|
|
||||||
|
|
||||||
|
@ -468,15 +658,25 @@ class File(ParamType):
|
||||||
envvar_list_splitter = os.path.pathsep
|
envvar_list_splitter = os.path.pathsep
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, mode="r", encoding=None, errors="strict", lazy=None, atomic=False
|
self,
|
||||||
):
|
mode: str = "r",
|
||||||
|
encoding: t.Optional[str] = None,
|
||||||
|
errors: t.Optional[str] = "strict",
|
||||||
|
lazy: t.Optional[bool] = None,
|
||||||
|
atomic: bool = False,
|
||||||
|
) -> None:
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.encoding = encoding
|
self.encoding = encoding
|
||||||
self.errors = errors
|
self.errors = errors
|
||||||
self.lazy = lazy
|
self.lazy = lazy
|
||||||
self.atomic = atomic
|
self.atomic = atomic
|
||||||
|
|
||||||
def resolve_lazy_flag(self, value):
|
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||||
|
info_dict = super().to_info_dict()
|
||||||
|
info_dict.update(mode=self.mode, encoding=self.encoding)
|
||||||
|
return info_dict
|
||||||
|
|
||||||
|
def resolve_lazy_flag(self, value: t.Any) -> bool:
|
||||||
if self.lazy is not None:
|
if self.lazy is not None:
|
||||||
return self.lazy
|
return self.lazy
|
||||||
if value == "-":
|
if value == "-":
|
||||||
|
@ -485,7 +685,9 @@ class File(ParamType):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(
|
||||||
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
|
) -> t.Any:
|
||||||
try:
|
try:
|
||||||
if hasattr(value, "read") or hasattr(value, "write"):
|
if hasattr(value, "read") or hasattr(value, "write"):
|
||||||
return value
|
return value
|
||||||
|
@ -493,16 +695,22 @@ class File(ParamType):
|
||||||
lazy = self.resolve_lazy_flag(value)
|
lazy = self.resolve_lazy_flag(value)
|
||||||
|
|
||||||
if lazy:
|
if lazy:
|
||||||
f = LazyFile(
|
f: t.IO = t.cast(
|
||||||
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
t.IO,
|
||||||
|
LazyFile(
|
||||||
|
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if ctx is not None:
|
if ctx is not None:
|
||||||
ctx.call_on_close(f.close_intelligently)
|
ctx.call_on_close(f.close_intelligently) # type: ignore
|
||||||
|
|
||||||
return f
|
return f
|
||||||
|
|
||||||
f, should_close = open_stream(
|
f, should_close = open_stream(
|
||||||
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||||
)
|
)
|
||||||
|
|
||||||
# If a context is provided, we automatically close the file
|
# If a context is provided, we automatically close the file
|
||||||
# at the end of the context execution (or flush out). If a
|
# at the end of the context execution (or flush out). If a
|
||||||
# context does not exist, it's the caller's responsibility to
|
# context does not exist, it's the caller's responsibility to
|
||||||
|
@ -513,15 +721,26 @@ class File(ParamType):
|
||||||
ctx.call_on_close(safecall(f.close))
|
ctx.call_on_close(safecall(f.close))
|
||||||
else:
|
else:
|
||||||
ctx.call_on_close(safecall(f.flush))
|
ctx.call_on_close(safecall(f.flush))
|
||||||
|
|
||||||
return f
|
return f
|
||||||
except (IOError, OSError) as e: # noqa: B014
|
except OSError as e: # noqa: B014
|
||||||
self.fail(
|
self.fail(f"{os.fsdecode(value)!r}: {e.strerror}", param, ctx)
|
||||||
"Could not open file: {}: {}".format(
|
|
||||||
filename_to_ui(value), get_streerror(e)
|
def shell_complete(
|
||||||
),
|
self, ctx: "Context", param: "Parameter", incomplete: str
|
||||||
param,
|
) -> t.List["CompletionItem"]:
|
||||||
ctx,
|
"""Return a special completion marker that tells the completion
|
||||||
)
|
system to use the shell to provide file path completions.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for this command.
|
||||||
|
:param param: The parameter that is requesting completion.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
from click.shell_completion import CompletionItem
|
||||||
|
|
||||||
|
return [CompletionItem(incomplete, type="file")]
|
||||||
|
|
||||||
|
|
||||||
class Path(ParamType):
|
class Path(ParamType):
|
||||||
|
@ -530,9 +749,6 @@ class Path(ParamType):
|
||||||
handle it returns just the filename. Secondly, it can perform various
|
handle it returns just the filename. Secondly, it can perform various
|
||||||
basic checks about what the file or directory should be.
|
basic checks about what the file or directory should be.
|
||||||
|
|
||||||
.. versionchanged:: 6.0
|
|
||||||
`allow_dash` was added.
|
|
||||||
|
|
||||||
:param exists: if set to true, the file or directory needs to exist for
|
:param exists: if set to true, the file or directory needs to exist for
|
||||||
this value to be valid. If this is not required and a
|
this value to be valid. If this is not required and a
|
||||||
file does indeed not exist, then all further checks are
|
file does indeed not exist, then all further checks are
|
||||||
|
@ -548,25 +764,29 @@ class Path(ParamType):
|
||||||
supposed to be done by the shell only.
|
supposed to be done by the shell only.
|
||||||
:param allow_dash: If this is set to `True`, a single dash to indicate
|
:param allow_dash: If this is set to `True`, a single dash to indicate
|
||||||
standard streams is permitted.
|
standard streams is permitted.
|
||||||
:param path_type: optionally a string type that should be used to
|
:param path_type: Convert the incoming path value to this type. If
|
||||||
represent the path. The default is `None` which
|
``None``, keep Python's default, which is ``str``. Useful to
|
||||||
means the return value will be either bytes or
|
convert to :class:`pathlib.Path`.
|
||||||
unicode depending on what makes most sense given the
|
|
||||||
input data Click deals with.
|
.. versionchanged:: 8.0
|
||||||
|
Allow passing ``type=pathlib.Path``.
|
||||||
|
|
||||||
|
.. versionchanged:: 6.0
|
||||||
|
Added the ``allow_dash`` parameter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
envvar_list_splitter = os.path.pathsep
|
envvar_list_splitter = os.path.pathsep
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
exists=False,
|
exists: bool = False,
|
||||||
file_okay=True,
|
file_okay: bool = True,
|
||||||
dir_okay=True,
|
dir_okay: bool = True,
|
||||||
writable=False,
|
writable: bool = False,
|
||||||
readable=True,
|
readable: bool = True,
|
||||||
resolve_path=False,
|
resolve_path: bool = False,
|
||||||
allow_dash=False,
|
allow_dash: bool = False,
|
||||||
path_type=None,
|
path_type: t.Optional[t.Type] = None,
|
||||||
):
|
):
|
||||||
self.exists = exists
|
self.exists = exists
|
||||||
self.file_okay = file_okay
|
self.file_okay = file_okay
|
||||||
|
@ -578,31 +798,58 @@ class Path(ParamType):
|
||||||
self.type = path_type
|
self.type = path_type
|
||||||
|
|
||||||
if self.file_okay and not self.dir_okay:
|
if self.file_okay and not self.dir_okay:
|
||||||
self.name = "file"
|
self.name = _("file")
|
||||||
self.path_type = "File"
|
|
||||||
elif self.dir_okay and not self.file_okay:
|
elif self.dir_okay and not self.file_okay:
|
||||||
self.name = "directory"
|
self.name = _("directory")
|
||||||
self.path_type = "Directory"
|
|
||||||
else:
|
else:
|
||||||
self.name = "path"
|
self.name = _("path")
|
||||||
self.path_type = "Path"
|
|
||||||
|
|
||||||
def coerce_path_result(self, rv):
|
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||||
|
info_dict = super().to_info_dict()
|
||||||
|
info_dict.update(
|
||||||
|
exists=self.exists,
|
||||||
|
file_okay=self.file_okay,
|
||||||
|
dir_okay=self.dir_okay,
|
||||||
|
writable=self.writable,
|
||||||
|
readable=self.readable,
|
||||||
|
allow_dash=self.allow_dash,
|
||||||
|
)
|
||||||
|
return info_dict
|
||||||
|
|
||||||
|
def coerce_path_result(self, rv: t.Any) -> t.Any:
|
||||||
if self.type is not None and not isinstance(rv, self.type):
|
if self.type is not None and not isinstance(rv, self.type):
|
||||||
if self.type is text_type:
|
if self.type is str:
|
||||||
rv = rv.decode(get_filesystem_encoding())
|
rv = os.fsdecode(rv)
|
||||||
|
elif self.type is bytes:
|
||||||
|
rv = os.fsencode(rv)
|
||||||
else:
|
else:
|
||||||
rv = rv.encode(get_filesystem_encoding())
|
rv = self.type(rv)
|
||||||
|
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(
|
||||||
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
|
) -> t.Any:
|
||||||
rv = value
|
rv = value
|
||||||
|
|
||||||
is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
|
is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
|
||||||
|
|
||||||
if not is_dash:
|
if not is_dash:
|
||||||
if self.resolve_path:
|
if self.resolve_path:
|
||||||
rv = os.path.realpath(rv)
|
# Get the absolute directory containing the path.
|
||||||
|
dir_ = os.path.dirname(os.path.abspath(rv))
|
||||||
|
|
||||||
|
# Resolve a symlink. realpath on Windows Python < 3.9
|
||||||
|
# doesn't resolve symlinks. This might return a relative
|
||||||
|
# path even if the path to the link is absolute.
|
||||||
|
if os.path.islink(rv):
|
||||||
|
rv = os.readlink(rv)
|
||||||
|
|
||||||
|
# Join dir_ with the resolved symlink if the resolved
|
||||||
|
# path is relative. This will make it relative to the
|
||||||
|
# original containing directory.
|
||||||
|
if not os.path.isabs(rv):
|
||||||
|
rv = os.path.join(dir_, rv)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
st = os.stat(rv)
|
st = os.stat(rv)
|
||||||
|
@ -610,8 +857,8 @@ class Path(ParamType):
|
||||||
if not self.exists:
|
if not self.exists:
|
||||||
return self.coerce_path_result(rv)
|
return self.coerce_path_result(rv)
|
||||||
self.fail(
|
self.fail(
|
||||||
"{} '{}' does not exist.".format(
|
_("{name} {filename!r} does not exist.").format(
|
||||||
self.path_type, filename_to_ui(value)
|
name=self.name.title(), filename=os.fsdecode(value)
|
||||||
),
|
),
|
||||||
param,
|
param,
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -619,30 +866,32 @@ class Path(ParamType):
|
||||||
|
|
||||||
if not self.file_okay and stat.S_ISREG(st.st_mode):
|
if not self.file_okay and stat.S_ISREG(st.st_mode):
|
||||||
self.fail(
|
self.fail(
|
||||||
"{} '{}' is a file.".format(self.path_type, filename_to_ui(value)),
|
_("{name} {filename!r} is a file.").format(
|
||||||
|
name=self.name.title(), filename=os.fsdecode(value)
|
||||||
|
),
|
||||||
param,
|
param,
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
if not self.dir_okay and stat.S_ISDIR(st.st_mode):
|
if not self.dir_okay and stat.S_ISDIR(st.st_mode):
|
||||||
self.fail(
|
self.fail(
|
||||||
"{} '{}' is a directory.".format(
|
_("{name} {filename!r} is a directory.").format(
|
||||||
self.path_type, filename_to_ui(value)
|
name=self.name.title(), filename=os.fsdecode(value)
|
||||||
),
|
),
|
||||||
param,
|
param,
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
if self.writable and not os.access(value, os.W_OK):
|
if self.writable and not os.access(rv, os.W_OK):
|
||||||
self.fail(
|
self.fail(
|
||||||
"{} '{}' is not writable.".format(
|
_("{name} {filename!r} is not writable.").format(
|
||||||
self.path_type, filename_to_ui(value)
|
name=self.name.title(), filename=os.fsdecode(value)
|
||||||
),
|
),
|
||||||
param,
|
param,
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
if self.readable and not os.access(value, os.R_OK):
|
if self.readable and not os.access(rv, os.R_OK):
|
||||||
self.fail(
|
self.fail(
|
||||||
"{} '{}' is not readable.".format(
|
_("{name} {filename!r} is not readable.").format(
|
||||||
self.path_type, filename_to_ui(value)
|
name=self.name.title(), filename=os.fsdecode(value)
|
||||||
),
|
),
|
||||||
param,
|
param,
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -650,6 +899,24 @@ class Path(ParamType):
|
||||||
|
|
||||||
return self.coerce_path_result(rv)
|
return self.coerce_path_result(rv)
|
||||||
|
|
||||||
|
def shell_complete(
|
||||||
|
self, ctx: "Context", param: "Parameter", incomplete: str
|
||||||
|
) -> t.List["CompletionItem"]:
|
||||||
|
"""Return a special completion marker that tells the completion
|
||||||
|
system to use the shell to provide path completions for only
|
||||||
|
directories or any paths.
|
||||||
|
|
||||||
|
:param ctx: Invocation context for this command.
|
||||||
|
:param param: The parameter that is requesting completion.
|
||||||
|
:param incomplete: Value being completed. May be empty.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
"""
|
||||||
|
from click.shell_completion import CompletionItem
|
||||||
|
|
||||||
|
type = "dir" if self.dir_okay and not self.file_okay else "file"
|
||||||
|
return [CompletionItem(incomplete, type=type)]
|
||||||
|
|
||||||
|
|
||||||
class Tuple(CompositeParamType):
|
class Tuple(CompositeParamType):
|
||||||
"""The default behavior of Click is to apply a type on a value directly.
|
"""The default behavior of Click is to apply a type on a value directly.
|
||||||
|
@ -665,75 +932,107 @@ class Tuple(CompositeParamType):
|
||||||
:param types: a list of types that should be used for the tuple items.
|
:param types: a list of types that should be used for the tuple items.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, types):
|
def __init__(self, types: t.Sequence[t.Union[t.Type, ParamType]]) -> None:
|
||||||
self.types = [convert_type(ty) for ty in types]
|
self.types = [convert_type(ty) for ty in types]
|
||||||
|
|
||||||
@property
|
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||||
def name(self):
|
info_dict = super().to_info_dict()
|
||||||
return "<{}>".format(" ".join(ty.name for ty in self.types))
|
info_dict["types"] = [t.to_info_dict() for t in self.types]
|
||||||
|
return info_dict
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def arity(self):
|
def name(self) -> str: # type: ignore
|
||||||
|
return f"<{' '.join(ty.name for ty in self.types)}>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def arity(self) -> int: # type: ignore
|
||||||
return len(self.types)
|
return len(self.types)
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(
|
||||||
if len(value) != len(self.types):
|
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||||
raise TypeError(
|
) -> t.Any:
|
||||||
"It would appear that nargs is set to conflict with the"
|
len_type = len(self.types)
|
||||||
" composite type arity."
|
len_value = len(value)
|
||||||
|
|
||||||
|
if len_value != len_type:
|
||||||
|
self.fail(
|
||||||
|
ngettext(
|
||||||
|
"{len_type} values are required, but {len_value} was given.",
|
||||||
|
"{len_type} values are required, but {len_value} were given.",
|
||||||
|
len_value,
|
||||||
|
).format(len_type=len_type, len_value=len_value),
|
||||||
|
param=param,
|
||||||
|
ctx=ctx,
|
||||||
)
|
)
|
||||||
|
|
||||||
return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value))
|
return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value))
|
||||||
|
|
||||||
|
|
||||||
def convert_type(ty, default=None):
|
def convert_type(ty: t.Optional[t.Any], default: t.Optional[t.Any] = None) -> ParamType:
|
||||||
"""Converts a callable or python type into the most appropriate
|
"""Find the most appropriate :class:`ParamType` for the given Python
|
||||||
param type.
|
type. If the type isn't provided, it can be inferred from a default
|
||||||
|
value.
|
||||||
"""
|
"""
|
||||||
guessed_type = False
|
guessed_type = False
|
||||||
|
|
||||||
if ty is None and default is not None:
|
if ty is None and default is not None:
|
||||||
if isinstance(default, tuple):
|
if isinstance(default, (tuple, list)):
|
||||||
ty = tuple(map(type, default))
|
# If the default is empty, ty will remain None and will
|
||||||
|
# return STRING.
|
||||||
|
if default:
|
||||||
|
item = default[0]
|
||||||
|
|
||||||
|
# A tuple of tuples needs to detect the inner types.
|
||||||
|
# Can't call convert recursively because that would
|
||||||
|
# incorrectly unwind the tuple to a single type.
|
||||||
|
if isinstance(item, (tuple, list)):
|
||||||
|
ty = tuple(map(type, item))
|
||||||
|
else:
|
||||||
|
ty = type(item)
|
||||||
else:
|
else:
|
||||||
ty = type(default)
|
ty = type(default)
|
||||||
|
|
||||||
guessed_type = True
|
guessed_type = True
|
||||||
|
|
||||||
if isinstance(ty, tuple):
|
if isinstance(ty, tuple):
|
||||||
return Tuple(ty)
|
return Tuple(ty)
|
||||||
|
|
||||||
if isinstance(ty, ParamType):
|
if isinstance(ty, ParamType):
|
||||||
return ty
|
return ty
|
||||||
if ty is text_type or ty is str or ty is None:
|
|
||||||
|
if ty is str or ty is None:
|
||||||
return STRING
|
return STRING
|
||||||
|
|
||||||
if ty is int:
|
if ty is int:
|
||||||
return INT
|
return INT
|
||||||
# Booleans are only okay if not guessed. This is done because for
|
|
||||||
# flags the default value is actually a bit of a lie in that it
|
|
||||||
# indicates which of the flags is the one we want. See get_default()
|
|
||||||
# for more information.
|
|
||||||
if ty is bool and not guessed_type:
|
|
||||||
return BOOL
|
|
||||||
if ty is float:
|
if ty is float:
|
||||||
return FLOAT
|
return FLOAT
|
||||||
|
|
||||||
|
if ty is bool:
|
||||||
|
return BOOL
|
||||||
|
|
||||||
if guessed_type:
|
if guessed_type:
|
||||||
return STRING
|
return STRING
|
||||||
|
|
||||||
# Catch a common mistake
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
try:
|
try:
|
||||||
if issubclass(ty, ParamType):
|
if issubclass(ty, ParamType):
|
||||||
raise AssertionError(
|
raise AssertionError(
|
||||||
"Attempted to use an uninstantiated parameter type ({}).".format(ty)
|
f"Attempted to use an uninstantiated parameter type ({ty})."
|
||||||
)
|
)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
# ty is an instance (correct), so issubclass fails.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return FuncParamType(ty)
|
return FuncParamType(ty)
|
||||||
|
|
||||||
|
|
||||||
#: A dummy parameter type that just does nothing. From a user's
|
#: A dummy parameter type that just does nothing. From a user's
|
||||||
#: perspective this appears to just be the same as `STRING` but internally
|
#: perspective this appears to just be the same as `STRING` but
|
||||||
#: no string conversion takes place. This is necessary to achieve the
|
#: internally no string conversion takes place if the input was bytes.
|
||||||
#: same bytes/unicode behavior on Python 2/3 in situations where you want
|
#: This is usually useful when working with file paths as they can
|
||||||
#: to not convert argument types. This is usually useful when working
|
#: appear in bytes and unicode.
|
||||||
#: with file paths as they can appear in bytes and unicode.
|
|
||||||
#:
|
#:
|
||||||
#: For path related uses the :class:`Path` type is a better choice but
|
#: For path related uses the :class:`Path` type is a better choice but
|
||||||
#: there are situations where an unprocessed type is useful which is why
|
#: there are situations where an unprocessed type is useful which is why
|
||||||
|
|
|
@ -1,86 +1,105 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import typing as t
|
||||||
|
from functools import update_wrapper
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
from ._compat import _default_text_stderr
|
from ._compat import _default_text_stderr
|
||||||
from ._compat import _default_text_stdout
|
from ._compat import _default_text_stdout
|
||||||
|
from ._compat import _find_binary_writer
|
||||||
from ._compat import auto_wrap_for_ansi
|
from ._compat import auto_wrap_for_ansi
|
||||||
from ._compat import binary_streams
|
from ._compat import binary_streams
|
||||||
from ._compat import filename_to_ui
|
|
||||||
from ._compat import get_filesystem_encoding
|
from ._compat import get_filesystem_encoding
|
||||||
from ._compat import get_streerror
|
|
||||||
from ._compat import is_bytes
|
|
||||||
from ._compat import open_stream
|
from ._compat import open_stream
|
||||||
from ._compat import PY2
|
|
||||||
from ._compat import should_strip_ansi
|
from ._compat import should_strip_ansi
|
||||||
from ._compat import string_types
|
|
||||||
from ._compat import strip_ansi
|
from ._compat import strip_ansi
|
||||||
from ._compat import text_streams
|
from ._compat import text_streams
|
||||||
from ._compat import text_type
|
|
||||||
from ._compat import WIN
|
from ._compat import WIN
|
||||||
from .globals import resolve_color_default
|
from .globals import resolve_color_default
|
||||||
|
|
||||||
if not PY2:
|
if t.TYPE_CHECKING:
|
||||||
from ._compat import _find_binary_writer
|
import typing_extensions as te
|
||||||
elif WIN:
|
|
||||||
from ._winconsole import _get_windows_argv
|
|
||||||
from ._winconsole import _hash_py_argv
|
|
||||||
from ._winconsole import _initial_argv_hash
|
|
||||||
|
|
||||||
echo_native_types = string_types + (bytes, bytearray)
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
|
||||||
|
|
||||||
def _posixify(name):
|
def _posixify(name: str) -> str:
|
||||||
return "-".join(name.split()).lower()
|
return "-".join(name.split()).lower()
|
||||||
|
|
||||||
|
|
||||||
def safecall(func):
|
def safecall(func: F) -> F:
|
||||||
"""Wraps a function so that it swallows exceptions."""
|
"""Wraps a function so that it swallows exceptions."""
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs): # type: ignore
|
||||||
try:
|
try:
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return wrapper
|
return update_wrapper(t.cast(F, wrapper), func)
|
||||||
|
|
||||||
|
|
||||||
def make_str(value):
|
def make_str(value: t.Any) -> str:
|
||||||
"""Converts a value into a valid string."""
|
"""Converts a value into a valid string."""
|
||||||
if isinstance(value, bytes):
|
if isinstance(value, bytes):
|
||||||
try:
|
try:
|
||||||
return value.decode(get_filesystem_encoding())
|
return value.decode(get_filesystem_encoding())
|
||||||
except UnicodeError:
|
except UnicodeError:
|
||||||
return value.decode("utf-8", "replace")
|
return value.decode("utf-8", "replace")
|
||||||
return text_type(value)
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
def make_default_short_help(help, max_length=45):
|
def make_default_short_help(help: str, max_length: int = 45) -> str:
|
||||||
"""Return a condensed version of help string."""
|
"""Returns a condensed version of help string."""
|
||||||
|
# Consider only the first paragraph.
|
||||||
|
paragraph_end = help.find("\n\n")
|
||||||
|
|
||||||
|
if paragraph_end != -1:
|
||||||
|
help = help[:paragraph_end]
|
||||||
|
|
||||||
|
# Collapse newlines, tabs, and spaces.
|
||||||
words = help.split()
|
words = help.split()
|
||||||
|
|
||||||
|
if not words:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# The first paragraph started with a "no rewrap" marker, ignore it.
|
||||||
|
if words[0] == "\b":
|
||||||
|
words = words[1:]
|
||||||
|
|
||||||
total_length = 0
|
total_length = 0
|
||||||
result = []
|
last_index = len(words) - 1
|
||||||
done = False
|
|
||||||
|
|
||||||
for word in words:
|
for i, word in enumerate(words):
|
||||||
if word[-1:] == ".":
|
total_length += len(word) + (i > 0)
|
||||||
done = True
|
|
||||||
new_length = 1 + len(word) if result else len(word)
|
if total_length > max_length: # too long, truncate
|
||||||
if total_length + new_length > max_length:
|
|
||||||
result.append("...")
|
|
||||||
done = True
|
|
||||||
else:
|
|
||||||
if result:
|
|
||||||
result.append(" ")
|
|
||||||
result.append(word)
|
|
||||||
if done:
|
|
||||||
break
|
break
|
||||||
total_length += new_length
|
|
||||||
|
|
||||||
return "".join(result)
|
if word[-1] == ".": # sentence end, truncate without "..."
|
||||||
|
return " ".join(words[: i + 1])
|
||||||
|
|
||||||
|
if total_length == max_length and i != last_index:
|
||||||
|
break # not at sentence end, truncate with "..."
|
||||||
|
else:
|
||||||
|
return " ".join(words) # no truncation needed
|
||||||
|
|
||||||
|
# Account for the length of the suffix.
|
||||||
|
total_length += len("...")
|
||||||
|
|
||||||
|
# remove words until the length is short enough
|
||||||
|
while i > 0:
|
||||||
|
total_length -= len(words[i]) + (i > 0)
|
||||||
|
|
||||||
|
if total_length <= max_length:
|
||||||
|
break
|
||||||
|
|
||||||
|
i -= 1
|
||||||
|
|
||||||
|
return " ".join(words[:i]) + "..."
|
||||||
|
|
||||||
|
|
||||||
class LazyFile(object):
|
class LazyFile:
|
||||||
"""A lazy file works like a regular file but it does not fully open
|
"""A lazy file works like a regular file but it does not fully open
|
||||||
the file but it does perform some basic checks early to see if the
|
the file but it does perform some basic checks early to see if the
|
||||||
filename parameter does make sense. This is useful for safely opening
|
filename parameter does make sense. This is useful for safely opening
|
||||||
|
@ -88,13 +107,19 @@ class LazyFile(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, filename, mode="r", encoding=None, errors="strict", atomic=False
|
self,
|
||||||
|
filename: str,
|
||||||
|
mode: str = "r",
|
||||||
|
encoding: t.Optional[str] = None,
|
||||||
|
errors: t.Optional[str] = "strict",
|
||||||
|
atomic: bool = False,
|
||||||
):
|
):
|
||||||
self.name = filename
|
self.name = filename
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.encoding = encoding
|
self.encoding = encoding
|
||||||
self.errors = errors
|
self.errors = errors
|
||||||
self.atomic = atomic
|
self.atomic = atomic
|
||||||
|
self._f: t.Optional[t.IO]
|
||||||
|
|
||||||
if filename == "-":
|
if filename == "-":
|
||||||
self._f, self.should_close = open_stream(filename, mode, encoding, errors)
|
self._f, self.should_close = open_stream(filename, mode, encoding, errors)
|
||||||
|
@ -107,15 +132,15 @@ class LazyFile(object):
|
||||||
self._f = None
|
self._f = None
|
||||||
self.should_close = True
|
self.should_close = True
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
return getattr(self.open(), name)
|
return getattr(self.open(), name)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
if self._f is not None:
|
if self._f is not None:
|
||||||
return repr(self._f)
|
return repr(self._f)
|
||||||
return "<unopened file '{}' {}>".format(self.name, self.mode)
|
return f"<unopened file '{self.name}' {self.mode}>"
|
||||||
|
|
||||||
def open(self):
|
def open(self) -> t.IO:
|
||||||
"""Opens the file if it's not yet open. This call might fail with
|
"""Opens the file if it's not yet open. This call might fail with
|
||||||
a :exc:`FileError`. Not handling this error will produce an error
|
a :exc:`FileError`. Not handling this error will produce an error
|
||||||
that Click shows.
|
that Click shows.
|
||||||
|
@ -126,102 +151,100 @@ class LazyFile(object):
|
||||||
rv, self.should_close = open_stream(
|
rv, self.should_close = open_stream(
|
||||||
self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
|
self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||||
)
|
)
|
||||||
except (IOError, OSError) as e: # noqa: E402
|
except OSError as e: # noqa: E402
|
||||||
from .exceptions import FileError
|
from .exceptions import FileError
|
||||||
|
|
||||||
raise FileError(self.name, hint=get_streerror(e))
|
raise FileError(self.name, hint=e.strerror) from e
|
||||||
self._f = rv
|
self._f = rv
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
"""Closes the underlying file, no matter what."""
|
"""Closes the underlying file, no matter what."""
|
||||||
if self._f is not None:
|
if self._f is not None:
|
||||||
self._f.close()
|
self._f.close()
|
||||||
|
|
||||||
def close_intelligently(self):
|
def close_intelligently(self) -> None:
|
||||||
"""This function only closes the file if it was opened by the lazy
|
"""This function only closes the file if it was opened by the lazy
|
||||||
file wrapper. For instance this will never close stdin.
|
file wrapper. For instance this will never close stdin.
|
||||||
"""
|
"""
|
||||||
if self.should_close:
|
if self.should_close:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> "LazyFile":
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb):
|
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||||
self.close_intelligently()
|
self.close_intelligently()
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self) -> t.Iterator[t.AnyStr]:
|
||||||
self.open()
|
self.open()
|
||||||
return iter(self._f)
|
return iter(self._f) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class KeepOpenFile(object):
|
class KeepOpenFile:
|
||||||
def __init__(self, file):
|
def __init__(self, file: t.IO) -> None:
|
||||||
self._file = file
|
self._file = file
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name: str) -> t.Any:
|
||||||
return getattr(self._file, name)
|
return getattr(self._file, name)
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> "KeepOpenFile":
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb):
|
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return repr(self._file)
|
return repr(self._file)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self) -> t.Iterator[t.AnyStr]:
|
||||||
return iter(self._file)
|
return iter(self._file)
|
||||||
|
|
||||||
|
|
||||||
def echo(message=None, file=None, nl=True, err=False, color=None):
|
def echo(
|
||||||
"""Prints a message plus a newline to the given file or stdout. On
|
message: t.Optional[t.Any] = None,
|
||||||
first sight, this looks like the print function, but it has improved
|
file: t.Optional[t.IO] = None,
|
||||||
support for handling Unicode and binary data that does not fail no
|
nl: bool = True,
|
||||||
matter how badly configured the system is.
|
err: bool = False,
|
||||||
|
color: t.Optional[bool] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Print a message and newline to stdout or a file. This should be
|
||||||
|
used instead of :func:`print` because it provides better support
|
||||||
|
for different data, files, and environments.
|
||||||
|
|
||||||
Primarily it means that you can print binary data as well as Unicode
|
Compared to :func:`print`, this does the following:
|
||||||
data on both 2.x and 3.x to the given file in the most appropriate way
|
|
||||||
possible. This is a very carefree function in that it will try its
|
|
||||||
best to not fail. As of Click 6.0 this includes support for unicode
|
|
||||||
output on the Windows console.
|
|
||||||
|
|
||||||
In addition to that, if `colorama`_ is installed, the echo function will
|
- Ensures that the output encoding is not misconfigured on Linux.
|
||||||
also support clever handling of ANSI codes. Essentially it will then
|
- Supports Unicode in the Windows console.
|
||||||
do the following:
|
- Supports writing to binary outputs, and supports writing bytes
|
||||||
|
to text outputs.
|
||||||
|
- Supports colors and styles on Windows.
|
||||||
|
- Removes ANSI color and style codes if the output does not look
|
||||||
|
like an interactive terminal.
|
||||||
|
- Always flushes the output.
|
||||||
|
|
||||||
- add transparent handling of ANSI color codes on Windows.
|
:param message: The string or bytes to output. Other objects are
|
||||||
- hide ANSI codes automatically if the destination file is not a
|
converted to strings.
|
||||||
terminal.
|
:param file: The file to write to. Defaults to ``stdout``.
|
||||||
|
:param err: Write to ``stderr`` instead of ``stdout``.
|
||||||
.. _colorama: https://pypi.org/project/colorama/
|
:param nl: Print a newline after the message. Enabled by default.
|
||||||
|
:param color: Force showing or hiding colors and other styles. By
|
||||||
|
default Click will remove color if the output does not look like
|
||||||
|
an interactive terminal.
|
||||||
|
|
||||||
.. versionchanged:: 6.0
|
.. versionchanged:: 6.0
|
||||||
As of Click 6.0 the echo function will properly support unicode
|
Support Unicode output on the Windows console. Click does not
|
||||||
output on the windows console. Not that click does not modify
|
modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()``
|
||||||
the interpreter in any way which means that `sys.stdout` or the
|
will still not support Unicode.
|
||||||
print statement or function will still not provide unicode support.
|
|
||||||
|
|
||||||
.. versionchanged:: 2.0
|
|
||||||
Starting with version 2.0 of Click, the echo function will work
|
|
||||||
with colorama if it's installed.
|
|
||||||
|
|
||||||
.. versionadded:: 3.0
|
|
||||||
The `err` parameter was added.
|
|
||||||
|
|
||||||
.. versionchanged:: 4.0
|
.. versionchanged:: 4.0
|
||||||
Added the `color` flag.
|
Added the ``color`` parameter.
|
||||||
|
|
||||||
:param message: the message to print
|
.. versionadded:: 3.0
|
||||||
:param file: the file to write to (defaults to ``stdout``)
|
Added the ``err`` parameter.
|
||||||
:param err: if set to true the file defaults to ``stderr`` instead of
|
|
||||||
``stdout``. This is faster and easier than calling
|
.. versionchanged:: 2.0
|
||||||
:func:`get_text_stderr` yourself.
|
Support colors on Windows if colorama is installed.
|
||||||
:param nl: if set to `True` (the default) a newline is printed afterwards.
|
|
||||||
:param color: controls if the terminal supports ANSI colors or not. The
|
|
||||||
default is autodetection.
|
|
||||||
"""
|
"""
|
||||||
if file is None:
|
if file is None:
|
||||||
if err:
|
if err:
|
||||||
|
@ -230,70 +253,73 @@ def echo(message=None, file=None, nl=True, err=False, color=None):
|
||||||
file = _default_text_stdout()
|
file = _default_text_stdout()
|
||||||
|
|
||||||
# Convert non bytes/text into the native string type.
|
# Convert non bytes/text into the native string type.
|
||||||
if message is not None and not isinstance(message, echo_native_types):
|
if message is not None and not isinstance(message, (str, bytes, bytearray)):
|
||||||
message = text_type(message)
|
out: t.Optional[t.Union[str, bytes]] = str(message)
|
||||||
|
else:
|
||||||
|
out = message
|
||||||
|
|
||||||
if nl:
|
if nl:
|
||||||
message = message or u""
|
out = out or ""
|
||||||
if isinstance(message, text_type):
|
if isinstance(out, str):
|
||||||
message += u"\n"
|
out += "\n"
|
||||||
else:
|
else:
|
||||||
message += b"\n"
|
out += b"\n"
|
||||||
|
|
||||||
# If there is a message, and we're in Python 3, and the value looks
|
if not out:
|
||||||
# like bytes, we manually need to find the binary stream and write the
|
file.flush()
|
||||||
# message in there. This is done separately so that most stream
|
return
|
||||||
# types will work as you would expect. Eg: you can write to StringIO
|
|
||||||
# for other cases.
|
# If there is a message and the value looks like bytes, we manually
|
||||||
if message and not PY2 and is_bytes(message):
|
# need to find the binary stream and write the message in there.
|
||||||
|
# This is done separately so that most stream types will work as you
|
||||||
|
# would expect. Eg: you can write to StringIO for other cases.
|
||||||
|
if isinstance(out, (bytes, bytearray)):
|
||||||
binary_file = _find_binary_writer(file)
|
binary_file = _find_binary_writer(file)
|
||||||
|
|
||||||
if binary_file is not None:
|
if binary_file is not None:
|
||||||
file.flush()
|
file.flush()
|
||||||
binary_file.write(message)
|
binary_file.write(out)
|
||||||
binary_file.flush()
|
binary_file.flush()
|
||||||
return
|
return
|
||||||
|
|
||||||
# ANSI-style support. If there is no message or we are dealing with
|
# ANSI style code support. For no message or bytes, nothing happens.
|
||||||
# bytes nothing is happening. If we are connected to a file we want
|
# When outputting to a file instead of a terminal, strip codes.
|
||||||
# to strip colors. If we are on windows we either wrap the stream
|
else:
|
||||||
# to strip the color or we use the colorama support to translate the
|
|
||||||
# ansi codes to API calls.
|
|
||||||
if message and not is_bytes(message):
|
|
||||||
color = resolve_color_default(color)
|
color = resolve_color_default(color)
|
||||||
|
|
||||||
if should_strip_ansi(file, color):
|
if should_strip_ansi(file, color):
|
||||||
message = strip_ansi(message)
|
out = strip_ansi(out)
|
||||||
elif WIN:
|
elif WIN:
|
||||||
if auto_wrap_for_ansi is not None:
|
if auto_wrap_for_ansi is not None:
|
||||||
file = auto_wrap_for_ansi(file)
|
file = auto_wrap_for_ansi(file) # type: ignore
|
||||||
elif not color:
|
elif not color:
|
||||||
message = strip_ansi(message)
|
out = strip_ansi(out)
|
||||||
|
|
||||||
if message:
|
file.write(out) # type: ignore
|
||||||
file.write(message)
|
|
||||||
file.flush()
|
file.flush()
|
||||||
|
|
||||||
|
|
||||||
def get_binary_stream(name):
|
def get_binary_stream(name: "te.Literal['stdin', 'stdout', 'stderr']") -> t.BinaryIO:
|
||||||
"""Returns a system stream for byte processing. This essentially
|
"""Returns a system stream for byte processing.
|
||||||
returns the stream from the sys module with the given name but it
|
|
||||||
solves some compatibility issues between different Python versions.
|
|
||||||
Primarily this function is necessary for getting binary streams on
|
|
||||||
Python 3.
|
|
||||||
|
|
||||||
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
||||||
``'stdout'`` and ``'stderr'``
|
``'stdout'`` and ``'stderr'``
|
||||||
"""
|
"""
|
||||||
opener = binary_streams.get(name)
|
opener = binary_streams.get(name)
|
||||||
if opener is None:
|
if opener is None:
|
||||||
raise TypeError("Unknown standard stream '{}'".format(name))
|
raise TypeError(f"Unknown standard stream '{name}'")
|
||||||
return opener()
|
return opener()
|
||||||
|
|
||||||
|
|
||||||
def get_text_stream(name, encoding=None, errors="strict"):
|
def get_text_stream(
|
||||||
|
name: "te.Literal['stdin', 'stdout', 'stderr']",
|
||||||
|
encoding: t.Optional[str] = None,
|
||||||
|
errors: t.Optional[str] = "strict",
|
||||||
|
) -> t.TextIO:
|
||||||
"""Returns a system stream for text processing. This usually returns
|
"""Returns a system stream for text processing. This usually returns
|
||||||
a wrapped stream around a binary stream returned from
|
a wrapped stream around a binary stream returned from
|
||||||
:func:`get_binary_stream` but it also can take shortcuts on Python 3
|
:func:`get_binary_stream` but it also can take shortcuts for already
|
||||||
for already correctly configured streams.
|
correctly configured streams.
|
||||||
|
|
||||||
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
||||||
``'stdout'`` and ``'stderr'``
|
``'stdout'`` and ``'stderr'``
|
||||||
|
@ -302,13 +328,18 @@ def get_text_stream(name, encoding=None, errors="strict"):
|
||||||
"""
|
"""
|
||||||
opener = text_streams.get(name)
|
opener = text_streams.get(name)
|
||||||
if opener is None:
|
if opener is None:
|
||||||
raise TypeError("Unknown standard stream '{}'".format(name))
|
raise TypeError(f"Unknown standard stream '{name}'")
|
||||||
return opener(encoding, errors)
|
return opener(encoding, errors)
|
||||||
|
|
||||||
|
|
||||||
def open_file(
|
def open_file(
|
||||||
filename, mode="r", encoding=None, errors="strict", lazy=False, atomic=False
|
filename: str,
|
||||||
):
|
mode: str = "r",
|
||||||
|
encoding: t.Optional[str] = None,
|
||||||
|
errors: t.Optional[str] = "strict",
|
||||||
|
lazy: bool = False,
|
||||||
|
atomic: bool = False,
|
||||||
|
) -> t.IO:
|
||||||
"""This is similar to how the :class:`File` works but for manual
|
"""This is similar to how the :class:`File` works but for manual
|
||||||
usage. Files are opened non lazy by default. This can open regular
|
usage. Files are opened non lazy by default. This can open regular
|
||||||
files as well as stdin/stdout if ``'-'`` is passed.
|
files as well as stdin/stdout if ``'-'`` is passed.
|
||||||
|
@ -332,35 +363,35 @@ def open_file(
|
||||||
moved on close.
|
moved on close.
|
||||||
"""
|
"""
|
||||||
if lazy:
|
if lazy:
|
||||||
return LazyFile(filename, mode, encoding, errors, atomic=atomic)
|
return t.cast(t.IO, LazyFile(filename, mode, encoding, errors, atomic=atomic))
|
||||||
f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
|
f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
|
||||||
if not should_close:
|
if not should_close:
|
||||||
f = KeepOpenFile(f)
|
f = t.cast(t.IO, KeepOpenFile(f))
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
def get_os_args():
|
def get_os_args() -> t.Sequence[str]:
|
||||||
"""This returns the argument part of sys.argv in the most appropriate
|
"""Returns the argument part of ``sys.argv``, removing the first
|
||||||
form for processing. What this means is that this return value is in
|
value which is the name of the script.
|
||||||
a format that works for Click to process but does not necessarily
|
|
||||||
correspond well to what's actually standard for the interpreter.
|
|
||||||
|
|
||||||
On most environments the return value is ``sys.argv[:1]`` unchanged.
|
.. deprecated:: 8.0
|
||||||
However if you are on Windows and running Python 2 the return value
|
Will be removed in Click 8.1. Access ``sys.argv[1:]`` directly
|
||||||
will actually be a list of unicode strings instead because the
|
instead.
|
||||||
default behavior on that platform otherwise will not be able to
|
|
||||||
carry all possible values that sys.argv can have.
|
|
||||||
|
|
||||||
.. versionadded:: 6.0
|
|
||||||
"""
|
"""
|
||||||
# We can only extract the unicode argv if sys.argv has not been
|
import warnings
|
||||||
# changed since the startup of the application.
|
|
||||||
if PY2 and WIN and _initial_argv_hash == _hash_py_argv():
|
warnings.warn(
|
||||||
return _get_windows_argv()
|
"'get_os_args' is deprecated and will be removed in Click 8.1."
|
||||||
|
" Access 'sys.argv[1:]' directly instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
return sys.argv[1:]
|
return sys.argv[1:]
|
||||||
|
|
||||||
|
|
||||||
def format_filename(filename, shorten=False):
|
def format_filename(
|
||||||
|
filename: t.Union[str, bytes, os.PathLike], shorten: bool = False
|
||||||
|
) -> str:
|
||||||
"""Formats a filename for user display. The main purpose of this
|
"""Formats a filename for user display. The main purpose of this
|
||||||
function is to ensure that the filename can be displayed at all. This
|
function is to ensure that the filename can be displayed at all. This
|
||||||
will decode the filename to unicode if necessary in a way that it will
|
will decode the filename to unicode if necessary in a way that it will
|
||||||
|
@ -374,10 +405,11 @@ def format_filename(filename, shorten=False):
|
||||||
"""
|
"""
|
||||||
if shorten:
|
if shorten:
|
||||||
filename = os.path.basename(filename)
|
filename = os.path.basename(filename)
|
||||||
return filename_to_ui(filename)
|
|
||||||
|
return os.fsdecode(filename)
|
||||||
|
|
||||||
|
|
||||||
def get_app_dir(app_name, roaming=True, force_posix=False):
|
def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str:
|
||||||
r"""Returns the config folder for the application. The default behavior
|
r"""Returns the config folder for the application. The default behavior
|
||||||
is to return whatever is most appropriate for the operating system.
|
is to return whatever is most appropriate for the operating system.
|
||||||
|
|
||||||
|
@ -392,13 +424,9 @@ def get_app_dir(app_name, roaming=True, force_posix=False):
|
||||||
``~/.config/foo-bar``
|
``~/.config/foo-bar``
|
||||||
Unix (POSIX):
|
Unix (POSIX):
|
||||||
``~/.foo-bar``
|
``~/.foo-bar``
|
||||||
Win XP (roaming):
|
Windows (roaming):
|
||||||
``C:\Documents and Settings\<user>\Local Settings\Application Data\Foo Bar``
|
|
||||||
Win XP (not roaming):
|
|
||||||
``C:\Documents and Settings\<user>\Application Data\Foo Bar``
|
|
||||||
Win 7 (roaming):
|
|
||||||
``C:\Users\<user>\AppData\Roaming\Foo Bar``
|
``C:\Users\<user>\AppData\Roaming\Foo Bar``
|
||||||
Win 7 (not roaming):
|
Windows (not roaming):
|
||||||
``C:\Users\<user>\AppData\Local\Foo Bar``
|
``C:\Users\<user>\AppData\Local\Foo Bar``
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
|
@ -419,7 +447,7 @@ def get_app_dir(app_name, roaming=True, force_posix=False):
|
||||||
folder = os.path.expanduser("~")
|
folder = os.path.expanduser("~")
|
||||||
return os.path.join(folder, app_name)
|
return os.path.join(folder, app_name)
|
||||||
if force_posix:
|
if force_posix:
|
||||||
return os.path.join(os.path.expanduser("~/.{}".format(_posixify(app_name))))
|
return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}"))
|
||||||
if sys.platform == "darwin":
|
if sys.platform == "darwin":
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
os.path.expanduser("~/Library/Application Support"), app_name
|
os.path.expanduser("~/Library/Application Support"), app_name
|
||||||
|
@ -430,7 +458,7 @@ def get_app_dir(app_name, roaming=True, force_posix=False):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PacifyFlushWrapper(object):
|
class PacifyFlushWrapper:
|
||||||
"""This wrapper is used to catch and suppress BrokenPipeErrors resulting
|
"""This wrapper is used to catch and suppress BrokenPipeErrors resulting
|
||||||
from ``.flush()`` being called on broken pipe during the shutdown/final-GC
|
from ``.flush()`` being called on broken pipe during the shutdown/final-GC
|
||||||
of the Python interpreter. Notably ``.flush()`` is always called on
|
of the Python interpreter. Notably ``.flush()`` is always called on
|
||||||
|
@ -439,17 +467,113 @@ class PacifyFlushWrapper(object):
|
||||||
pipe, all calls and attributes are proxied.
|
pipe, all calls and attributes are proxied.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, wrapped):
|
def __init__(self, wrapped: t.IO) -> None:
|
||||||
self.wrapped = wrapped
|
self.wrapped = wrapped
|
||||||
|
|
||||||
def flush(self):
|
def flush(self) -> None:
|
||||||
try:
|
try:
|
||||||
self.wrapped.flush()
|
self.wrapped.flush()
|
||||||
except IOError as e:
|
except OSError as e:
|
||||||
import errno
|
import errno
|
||||||
|
|
||||||
if e.errno != errno.EPIPE:
|
if e.errno != errno.EPIPE:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr: str) -> t.Any:
|
||||||
return getattr(self.wrapped, attr)
|
return getattr(self.wrapped, attr)
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_program_name(
|
||||||
|
path: t.Optional[str] = None, _main: ModuleType = sys.modules["__main__"]
|
||||||
|
) -> str:
|
||||||
|
"""Determine the command used to run the program, for use in help
|
||||||
|
text. If a file or entry point was executed, the file name is
|
||||||
|
returned. If ``python -m`` was used to execute a module or package,
|
||||||
|
``python -m name`` is returned.
|
||||||
|
|
||||||
|
This doesn't try to be too precise, the goal is to give a concise
|
||||||
|
name for help text. Files are only shown as their name without the
|
||||||
|
path. ``python`` is only shown for modules, and the full path to
|
||||||
|
``sys.executable`` is not shown.
|
||||||
|
|
||||||
|
:param path: The Python file being executed. Python puts this in
|
||||||
|
``sys.argv[0]``, which is used by default.
|
||||||
|
:param _main: The ``__main__`` module. This should only be passed
|
||||||
|
during internal testing.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
Based on command args detection in the Werkzeug reloader.
|
||||||
|
|
||||||
|
:meta private:
|
||||||
|
"""
|
||||||
|
if not path:
|
||||||
|
path = sys.argv[0]
|
||||||
|
|
||||||
|
# The value of __package__ indicates how Python was called. It may
|
||||||
|
# not exist if a setuptools script is installed as an egg. It may be
|
||||||
|
# set incorrectly for entry points created with pip on Windows.
|
||||||
|
if getattr(_main, "__package__", None) is None or (
|
||||||
|
os.name == "nt"
|
||||||
|
and _main.__package__ == ""
|
||||||
|
and not os.path.exists(path)
|
||||||
|
and os.path.exists(f"{path}.exe")
|
||||||
|
):
|
||||||
|
# Executed a file, like "python app.py".
|
||||||
|
return os.path.basename(path)
|
||||||
|
|
||||||
|
# Executed a module, like "python -m example".
|
||||||
|
# Rewritten by Python from "-m script" to "/path/to/script.py".
|
||||||
|
# Need to look at main module to determine how it was executed.
|
||||||
|
py_module = t.cast(str, _main.__package__)
|
||||||
|
name = os.path.splitext(os.path.basename(path))[0]
|
||||||
|
|
||||||
|
# A submodule like "example.cli".
|
||||||
|
if name != "__main__":
|
||||||
|
py_module = f"{py_module}.{name}"
|
||||||
|
|
||||||
|
return f"python -m {py_module.lstrip('.')}"
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_args(
|
||||||
|
args: t.Iterable[str],
|
||||||
|
*,
|
||||||
|
user: bool = True,
|
||||||
|
env: bool = True,
|
||||||
|
glob_recursive: bool = True,
|
||||||
|
) -> t.List[str]:
|
||||||
|
"""Simulate Unix shell expansion with Python functions.
|
||||||
|
|
||||||
|
See :func:`glob.glob`, :func:`os.path.expanduser`, and
|
||||||
|
:func:`os.path.expandvars`.
|
||||||
|
|
||||||
|
This intended for use on Windows, where the shell does not do any
|
||||||
|
expansion. It may not exactly match what a Unix shell would do.
|
||||||
|
|
||||||
|
:param args: List of command line arguments to expand.
|
||||||
|
:param user: Expand user home directory.
|
||||||
|
:param env: Expand environment variables.
|
||||||
|
:param glob_recursive: ``**`` matches directories recursively.
|
||||||
|
|
||||||
|
.. versionadded:: 8.0
|
||||||
|
|
||||||
|
:meta private:
|
||||||
|
"""
|
||||||
|
from glob import glob
|
||||||
|
|
||||||
|
out = []
|
||||||
|
|
||||||
|
for arg in args:
|
||||||
|
if user:
|
||||||
|
arg = os.path.expanduser(arg)
|
||||||
|
|
||||||
|
if env:
|
||||||
|
arg = os.path.expandvars(arg)
|
||||||
|
|
||||||
|
matches = glob(arg, recursive=glob_recursive)
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
out.append(arg)
|
||||||
|
else:
|
||||||
|
out.extend(matches)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
@ -6,3 +10,22 @@ from click.testing import CliRunner
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def runner(request):
|
def runner(request):
|
||||||
return CliRunner()
|
return CliRunner()
|
||||||
|
|
||||||
|
|
||||||
|
def check_symlink_impl():
|
||||||
|
"""This function checks if using symlinks is allowed
|
||||||
|
on the host machine"""
|
||||||
|
tempdir = tempfile.mkdtemp(prefix="click-")
|
||||||
|
test_pth = os.path.join(tempdir, "check_sym_impl")
|
||||||
|
sym_pth = os.path.join(tempdir, "link")
|
||||||
|
open(test_pth, "w").close()
|
||||||
|
rv = True
|
||||||
|
try:
|
||||||
|
os.symlink(test_pth, sym_pth)
|
||||||
|
except (NotImplementedError, OSError):
|
||||||
|
# Creating symlinks on Windows require elevated access.
|
||||||
|
# OSError is thrown if the function is called without it.
|
||||||
|
rv = False
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tempdir, ignore_errors=True)
|
||||||
|
return rv
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from click._compat import PY2
|
|
||||||
from click._compat import text_type
|
|
||||||
|
|
||||||
|
|
||||||
def test_nargs_star(runner):
|
def test_nargs_star(runner):
|
||||||
|
@ -13,19 +10,19 @@ def test_nargs_star(runner):
|
||||||
@click.argument("src", nargs=-1)
|
@click.argument("src", nargs=-1)
|
||||||
@click.argument("dst")
|
@click.argument("dst")
|
||||||
def copy(src, dst):
|
def copy(src, dst):
|
||||||
click.echo("src={}".format("|".join(src)))
|
click.echo(f"src={'|'.join(src)}")
|
||||||
click.echo("dst={}".format(dst))
|
click.echo(f"dst={dst}")
|
||||||
|
|
||||||
result = runner.invoke(copy, ["foo.txt", "bar.txt", "dir"])
|
result = runner.invoke(copy, ["foo.txt", "bar.txt", "dir"])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
assert result.output.splitlines() == ["src=foo.txt|bar.txt", "dst=dir"]
|
assert result.output.splitlines() == ["src=foo.txt|bar.txt", "dst=dir"]
|
||||||
|
|
||||||
|
|
||||||
def test_nargs_default(runner):
|
def test_argument_unbounded_nargs_cant_have_default(runner):
|
||||||
with pytest.raises(TypeError, match="nargs=-1"):
|
with pytest.raises(TypeError, match="nargs=-1"):
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("src", nargs=-1, default=42)
|
@click.argument("src", nargs=-1, default=["42"])
|
||||||
def copy(src):
|
def copy(src):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -35,8 +32,9 @@ def test_nargs_tup(runner):
|
||||||
@click.argument("name", nargs=1)
|
@click.argument("name", nargs=1)
|
||||||
@click.argument("point", nargs=2, type=click.INT)
|
@click.argument("point", nargs=2, type=click.INT)
|
||||||
def copy(name, point):
|
def copy(name, point):
|
||||||
click.echo("name={}".format(name))
|
click.echo(f"name={name}")
|
||||||
click.echo("point={0[0]}/{0[1]}".format(point))
|
x, y = point
|
||||||
|
click.echo(f"point={x}/{y}")
|
||||||
|
|
||||||
result = runner.invoke(copy, ["peter", "1", "2"])
|
result = runner.invoke(copy, ["peter", "1", "2"])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
|
@ -56,7 +54,8 @@ def test_nargs_tup_composite(runner):
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("item", **opts)
|
@click.argument("item", **opts)
|
||||||
def copy(item):
|
def copy(item):
|
||||||
click.echo("name={0[0]} id={0[1]:d}".format(item))
|
name, id = item
|
||||||
|
click.echo(f"name={name} id={id:d}")
|
||||||
|
|
||||||
result = runner.invoke(copy, ["peter", "1"])
|
result = runner.invoke(copy, ["peter", "1"])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
|
@ -83,22 +82,17 @@ def test_bytes_args(runner, monkeypatch):
|
||||||
@click.argument("arg")
|
@click.argument("arg")
|
||||||
def from_bytes(arg):
|
def from_bytes(arg):
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
arg, text_type
|
arg, str
|
||||||
), "UTF-8 encoded argument should be implicitly converted to Unicode"
|
), "UTF-8 encoded argument should be implicitly converted to Unicode"
|
||||||
|
|
||||||
# Simulate empty locale environment variables
|
# Simulate empty locale environment variables
|
||||||
if PY2:
|
monkeypatch.setattr(sys.stdin, "encoding", "utf-8")
|
||||||
monkeypatch.setattr(sys.stdin, "encoding", "ANSI_X3.4-1968")
|
monkeypatch.setattr(sys, "getfilesystemencoding", lambda: "utf-8")
|
||||||
monkeypatch.setattr(sys, "getfilesystemencoding", lambda: "ANSI_X3.4-1968")
|
monkeypatch.setattr(sys, "getdefaultencoding", lambda: "utf-8")
|
||||||
monkeypatch.setattr(sys, "getdefaultencoding", lambda: "ascii")
|
|
||||||
else:
|
|
||||||
monkeypatch.setattr(sys.stdin, "encoding", "utf-8")
|
|
||||||
monkeypatch.setattr(sys, "getfilesystemencoding", lambda: "utf-8")
|
|
||||||
monkeypatch.setattr(sys, "getdefaultencoding", lambda: "utf-8")
|
|
||||||
|
|
||||||
runner.invoke(
|
runner.invoke(
|
||||||
from_bytes,
|
from_bytes,
|
||||||
[u"Something outside of ASCII range: 林".encode("UTF-8")],
|
["Something outside of ASCII range: 林".encode()],
|
||||||
catch_exceptions=False,
|
catch_exceptions=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -169,33 +163,55 @@ def test_stdout_default(runner):
|
||||||
assert result.output == "Foo bar baz\n"
|
assert result.output == "Foo bar baz\n"
|
||||||
|
|
||||||
|
|
||||||
def test_nargs_envvar(runner):
|
@pytest.mark.parametrize(
|
||||||
@click.command()
|
("nargs", "value", "expect"),
|
||||||
@click.option("--arg", nargs=2)
|
[
|
||||||
def cmd(arg):
|
(2, "", None),
|
||||||
click.echo("|".join(arg))
|
(2, "a", "Takes 2 values but 1 was given."),
|
||||||
|
(2, "a b", ("a", "b")),
|
||||||
result = runner.invoke(
|
(2, "a b c", "Takes 2 values but 3 were given."),
|
||||||
cmd, [], auto_envvar_prefix="TEST", env={"TEST_ARG": "foo bar"}
|
(-1, "a b c", ("a", "b", "c")),
|
||||||
)
|
(-1, "", ()),
|
||||||
assert not result.exception
|
],
|
||||||
assert result.output == "foo|bar\n"
|
)
|
||||||
|
def test_nargs_envvar(runner, nargs, value, expect):
|
||||||
|
if nargs == -1:
|
||||||
|
param = click.argument("arg", envvar="X", nargs=nargs)
|
||||||
|
else:
|
||||||
|
param = click.option("--arg", envvar="X", nargs=nargs)
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--arg", envvar="X", nargs=2)
|
@param
|
||||||
def cmd(arg):
|
def cmd(arg):
|
||||||
click.echo("|".join(arg))
|
return arg
|
||||||
|
|
||||||
result = runner.invoke(cmd, [], env={"X": "foo bar"})
|
result = runner.invoke(cmd, env={"X": value}, standalone_mode=False)
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "foo|bar\n"
|
if isinstance(expect, str):
|
||||||
|
assert isinstance(result.exception, click.BadParameter)
|
||||||
|
assert expect in result.exception.format_message()
|
||||||
|
else:
|
||||||
|
assert result.return_value == expect
|
||||||
|
|
||||||
|
|
||||||
|
def test_nargs_envvar_only_if_values_empty(runner):
|
||||||
|
@click.command()
|
||||||
|
@click.argument("arg", envvar="X", nargs=-1)
|
||||||
|
def cli(arg):
|
||||||
|
return arg
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["a", "b"], standalone_mode=False)
|
||||||
|
assert result.return_value == ("a", "b")
|
||||||
|
|
||||||
|
result = runner.invoke(cli, env={"X": "a"}, standalone_mode=False)
|
||||||
|
assert result.return_value == ("a",)
|
||||||
|
|
||||||
|
|
||||||
def test_empty_nargs(runner):
|
def test_empty_nargs(runner):
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("arg", nargs=-1)
|
@click.argument("arg", nargs=-1)
|
||||||
def cmd(arg):
|
def cmd(arg):
|
||||||
click.echo("arg:{}".format("|".join(arg)))
|
click.echo(f"arg:{'|'.join(arg)}")
|
||||||
|
|
||||||
result = runner.invoke(cmd, [])
|
result = runner.invoke(cmd, [])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
@ -204,7 +220,7 @@ def test_empty_nargs(runner):
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("arg", nargs=-1, required=True)
|
@click.argument("arg", nargs=-1, required=True)
|
||||||
def cmd2(arg):
|
def cmd2(arg):
|
||||||
click.echo("arg:{}".format("|".join(arg)))
|
click.echo(f"arg:{'|'.join(arg)}")
|
||||||
|
|
||||||
result = runner.invoke(cmd2, [])
|
result = runner.invoke(cmd2, [])
|
||||||
assert result.exit_code == 2
|
assert result.exit_code == 2
|
||||||
|
@ -215,7 +231,7 @@ def test_missing_arg(runner):
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("arg")
|
@click.argument("arg")
|
||||||
def cmd(arg):
|
def cmd(arg):
|
||||||
click.echo("arg:{}".format(arg))
|
click.echo(f"arg:{arg}")
|
||||||
|
|
||||||
result = runner.invoke(cmd, [])
|
result = runner.invoke(cmd, [])
|
||||||
assert result.exit_code == 2
|
assert result.exit_code == 2
|
||||||
|
@ -226,9 +242,9 @@ def test_missing_argument_string_cast():
|
||||||
ctx = click.Context(click.Command(""))
|
ctx = click.Context(click.Command(""))
|
||||||
|
|
||||||
with pytest.raises(click.MissingParameter) as excinfo:
|
with pytest.raises(click.MissingParameter) as excinfo:
|
||||||
click.Argument(["a"], required=True).full_process_value(ctx, None)
|
click.Argument(["a"], required=True).process_value(ctx, None)
|
||||||
|
|
||||||
assert str(excinfo.value) == "missing parameter: a"
|
assert str(excinfo.value) == "Missing parameter: a"
|
||||||
|
|
||||||
|
|
||||||
def test_implicit_non_required(runner):
|
def test_implicit_non_required(runner):
|
||||||
|
@ -268,7 +284,7 @@ def test_nargs_star_ordering(runner):
|
||||||
click.echo(arg)
|
click.echo(arg)
|
||||||
|
|
||||||
result = runner.invoke(cmd, ["a", "b", "c"])
|
result = runner.invoke(cmd, ["a", "b", "c"])
|
||||||
assert result.output.splitlines() == ["(u'a',)" if PY2 else "('a',)", "b", "c"]
|
assert result.output.splitlines() == ["('a',)", "b", "c"]
|
||||||
|
|
||||||
|
|
||||||
def test_nargs_specified_plus_star_ordering(runner):
|
def test_nargs_specified_plus_star_ordering(runner):
|
||||||
|
@ -281,11 +297,7 @@ def test_nargs_specified_plus_star_ordering(runner):
|
||||||
click.echo(arg)
|
click.echo(arg)
|
||||||
|
|
||||||
result = runner.invoke(cmd, ["a", "b", "c", "d", "e", "f"])
|
result = runner.invoke(cmd, ["a", "b", "c", "d", "e", "f"])
|
||||||
assert result.output.splitlines() == [
|
assert result.output.splitlines() == ["('a', 'b', 'c')", "d", "('e', 'f')"]
|
||||||
"(u'a', u'b', u'c')" if PY2 else "('a', 'b', 'c')",
|
|
||||||
"d",
|
|
||||||
"(u'e', u'f')" if PY2 else "('e', 'f')",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_defaults_for_nargs(runner):
|
def test_defaults_for_nargs(runner):
|
||||||
|
@ -303,7 +315,7 @@ def test_defaults_for_nargs(runner):
|
||||||
|
|
||||||
result = runner.invoke(cmd, ["3"])
|
result = runner.invoke(cmd, ["3"])
|
||||||
assert result.exception is not None
|
assert result.exception is not None
|
||||||
assert "argument a takes 2 values" in result.output
|
assert "Argument 'a' takes 2 values." in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_param_decls_not_allowed(runner):
|
def test_multiple_param_decls_not_allowed(runner):
|
||||||
|
@ -313,3 +325,55 @@ def test_multiple_param_decls_not_allowed(runner):
|
||||||
@click.argument("x", click.Choice(["a", "b"]))
|
@click.argument("x", click.Choice(["a", "b"]))
|
||||||
def copy(x):
|
def copy(x):
|
||||||
click.echo(x)
|
click.echo(x)
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_not_allowed():
|
||||||
|
with pytest.raises(TypeError, match="multiple"):
|
||||||
|
click.Argument(["a"], multiple=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", [(), ("a",), ("a", "b", "c")])
|
||||||
|
def test_nargs_bad_default(runner, value):
|
||||||
|
with pytest.raises(ValueError, match="nargs=2"):
|
||||||
|
click.Argument(["a"], nargs=2, default=value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_subcommand_help(runner):
|
||||||
|
@click.group()
|
||||||
|
@click.argument("name")
|
||||||
|
@click.argument("val")
|
||||||
|
@click.option("--opt")
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, name, val, opt):
|
||||||
|
ctx.obj = dict(name=name, val=val)
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_obj
|
||||||
|
def cmd(obj):
|
||||||
|
click.echo(f"CMD for {obj['name']} with value {obj['val']}")
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["foo", "bar", "cmd", "--help"])
|
||||||
|
assert not result.exception
|
||||||
|
assert "Usage: cli NAME VAL cmd [OPTIONS]" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_subcommand_help(runner):
|
||||||
|
@click.group()
|
||||||
|
@click.argument("arg1")
|
||||||
|
@click.option("--opt1")
|
||||||
|
def cli(arg1, opt1):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@cli.group()
|
||||||
|
@click.argument("arg2")
|
||||||
|
@click.option("--opt2")
|
||||||
|
def cmd(arg2, opt2):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
def subcmd():
|
||||||
|
click.echo("subcommand")
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["arg1", "cmd", "arg2", "subcmd", "--help"])
|
||||||
|
assert not result.exception
|
||||||
|
assert "Usage: cli ARG1 cmd ARG2 subcmd [OPTIONS]" in result.output
|
||||||
|
|
|
@ -1,500 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
import click
|
|
||||||
from click._bashcomplete import get_choices
|
|
||||||
|
|
||||||
|
|
||||||
def choices_without_help(cli, args, incomplete):
|
|
||||||
completions = get_choices(cli, "dummy", args, incomplete)
|
|
||||||
return [c[0] for c in completions]
|
|
||||||
|
|
||||||
|
|
||||||
def choices_with_help(cli, args, incomplete):
|
|
||||||
return list(get_choices(cli, "dummy", args, incomplete))
|
|
||||||
|
|
||||||
|
|
||||||
def test_single_command():
|
|
||||||
@click.command()
|
|
||||||
@click.option("--local-opt")
|
|
||||||
def cli(local_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, [], "-") == ["--local-opt"]
|
|
||||||
assert choices_without_help(cli, [], "") == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_boolean_flag():
|
|
||||||
@click.command()
|
|
||||||
@click.option("--shout/--no-shout", default=False)
|
|
||||||
def cli(local_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, [], "-") == ["--shout", "--no-shout"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_multi_value_option():
|
|
||||||
@click.group()
|
|
||||||
@click.option("--pos", nargs=2, type=float)
|
|
||||||
def cli(local_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.option("--local-opt")
|
|
||||||
def sub(local_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, [], "-") == ["--pos"]
|
|
||||||
assert choices_without_help(cli, ["--pos"], "") == []
|
|
||||||
assert choices_without_help(cli, ["--pos", "1.0"], "") == []
|
|
||||||
assert choices_without_help(cli, ["--pos", "1.0", "1.0"], "") == ["sub"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_multi_option():
|
|
||||||
@click.command()
|
|
||||||
@click.option("--message", "-m", multiple=True)
|
|
||||||
def cli(local_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, [], "-") == ["--message", "-m"]
|
|
||||||
assert choices_without_help(cli, ["-m"], "") == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_small_chain():
|
|
||||||
@click.group()
|
|
||||||
@click.option("--global-opt")
|
|
||||||
def cli(global_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.option("--local-opt")
|
|
||||||
def sub(local_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, [], "") == ["sub"]
|
|
||||||
assert choices_without_help(cli, [], "-") == ["--global-opt"]
|
|
||||||
assert choices_without_help(cli, ["sub"], "") == []
|
|
||||||
assert choices_without_help(cli, ["sub"], "-") == ["--local-opt"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_long_chain():
|
|
||||||
@click.group("cli")
|
|
||||||
@click.option("--cli-opt")
|
|
||||||
def cli(cli_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.group("asub")
|
|
||||||
@click.option("--asub-opt")
|
|
||||||
def asub(asub_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@asub.group("bsub")
|
|
||||||
@click.option("--bsub-opt")
|
|
||||||
def bsub(bsub_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
COLORS = ["red", "green", "blue"]
|
|
||||||
|
|
||||||
def get_colors(ctx, args, incomplete):
|
|
||||||
for c in COLORS:
|
|
||||||
if c.startswith(incomplete):
|
|
||||||
yield c
|
|
||||||
|
|
||||||
def search_colors(ctx, args, incomplete):
|
|
||||||
for c in COLORS:
|
|
||||||
if incomplete in c:
|
|
||||||
yield c
|
|
||||||
|
|
||||||
CSUB_OPT_CHOICES = ["foo", "bar"]
|
|
||||||
CSUB_CHOICES = ["bar", "baz"]
|
|
||||||
|
|
||||||
@bsub.command("csub")
|
|
||||||
@click.option("--csub-opt", type=click.Choice(CSUB_OPT_CHOICES))
|
|
||||||
@click.option("--csub", type=click.Choice(CSUB_CHOICES))
|
|
||||||
@click.option("--search-color", autocompletion=search_colors)
|
|
||||||
@click.argument("color", autocompletion=get_colors)
|
|
||||||
def csub(csub_opt, color):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, [], "-") == ["--cli-opt"]
|
|
||||||
assert choices_without_help(cli, [], "") == ["asub"]
|
|
||||||
assert choices_without_help(cli, ["asub"], "-") == ["--asub-opt"]
|
|
||||||
assert choices_without_help(cli, ["asub"], "") == ["bsub"]
|
|
||||||
assert choices_without_help(cli, ["asub", "bsub"], "-") == ["--bsub-opt"]
|
|
||||||
assert choices_without_help(cli, ["asub", "bsub"], "") == ["csub"]
|
|
||||||
assert choices_without_help(cli, ["asub", "bsub", "csub"], "-") == [
|
|
||||||
"--csub-opt",
|
|
||||||
"--csub",
|
|
||||||
"--search-color",
|
|
||||||
]
|
|
||||||
assert (
|
|
||||||
choices_without_help(cli, ["asub", "bsub", "csub", "--csub-opt"], "")
|
|
||||||
== CSUB_OPT_CHOICES
|
|
||||||
)
|
|
||||||
assert choices_without_help(cli, ["asub", "bsub", "csub"], "--csub") == [
|
|
||||||
"--csub-opt",
|
|
||||||
"--csub",
|
|
||||||
]
|
|
||||||
assert (
|
|
||||||
choices_without_help(cli, ["asub", "bsub", "csub", "--csub"], "")
|
|
||||||
== CSUB_CHOICES
|
|
||||||
)
|
|
||||||
assert choices_without_help(cli, ["asub", "bsub", "csub", "--csub-opt"], "f") == [
|
|
||||||
"foo"
|
|
||||||
]
|
|
||||||
assert choices_without_help(cli, ["asub", "bsub", "csub"], "") == COLORS
|
|
||||||
assert choices_without_help(cli, ["asub", "bsub", "csub"], "b") == ["blue"]
|
|
||||||
assert choices_without_help(
|
|
||||||
cli, ["asub", "bsub", "csub", "--search-color"], "een"
|
|
||||||
) == ["green"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_chaining():
|
|
||||||
@click.group("cli", chain=True)
|
|
||||||
@click.option("--cli-opt")
|
|
||||||
@click.argument("arg", type=click.Choice(["cliarg1", "cliarg2"]))
|
|
||||||
def cli(cli_opt, arg):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.option("--asub-opt")
|
|
||||||
def asub(asub_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.command(help="bsub help")
|
|
||||||
@click.option("--bsub-opt")
|
|
||||||
@click.argument("arg", type=click.Choice(["arg1", "arg2"]))
|
|
||||||
def bsub(bsub_opt, arg):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.option("--csub-opt")
|
|
||||||
@click.argument("arg", type=click.Choice(["carg1", "carg2"]), default="carg1")
|
|
||||||
def csub(csub_opt, arg):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, [], "-") == ["--cli-opt"]
|
|
||||||
assert choices_without_help(cli, [], "") == ["cliarg1", "cliarg2"]
|
|
||||||
assert choices_without_help(cli, ["cliarg1", "asub"], "-") == ["--asub-opt"]
|
|
||||||
assert choices_without_help(cli, ["cliarg1", "asub"], "") == ["bsub", "csub"]
|
|
||||||
assert choices_without_help(cli, ["cliarg1", "bsub"], "") == ["arg1", "arg2"]
|
|
||||||
assert choices_without_help(cli, ["cliarg1", "asub", "--asub-opt"], "") == []
|
|
||||||
assert choices_without_help(
|
|
||||||
cli, ["cliarg1", "asub", "--asub-opt", "5", "bsub"], "-"
|
|
||||||
) == ["--bsub-opt"]
|
|
||||||
assert choices_without_help(cli, ["cliarg1", "asub", "bsub"], "-") == ["--bsub-opt"]
|
|
||||||
assert choices_without_help(cli, ["cliarg1", "asub", "csub"], "") == [
|
|
||||||
"carg1",
|
|
||||||
"carg2",
|
|
||||||
]
|
|
||||||
assert choices_without_help(cli, ["cliarg1", "bsub", "arg1", "csub"], "") == [
|
|
||||||
"carg1",
|
|
||||||
"carg2",
|
|
||||||
]
|
|
||||||
assert choices_without_help(cli, ["cliarg1", "asub", "csub"], "-") == ["--csub-opt"]
|
|
||||||
assert choices_with_help(cli, ["cliarg1", "asub"], "b") == [("bsub", "bsub help")]
|
|
||||||
|
|
||||||
|
|
||||||
def test_argument_choice():
|
|
||||||
@click.command()
|
|
||||||
@click.argument("arg1", required=True, type=click.Choice(["arg11", "arg12"]))
|
|
||||||
@click.argument("arg2", type=click.Choice(["arg21", "arg22"]), default="arg21")
|
|
||||||
@click.argument("arg3", type=click.Choice(["arg", "argument"]), default="arg")
|
|
||||||
def cli():
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, [], "") == ["arg11", "arg12"]
|
|
||||||
assert choices_without_help(cli, [], "arg") == ["arg11", "arg12"]
|
|
||||||
assert choices_without_help(cli, ["arg11"], "") == ["arg21", "arg22"]
|
|
||||||
assert choices_without_help(cli, ["arg12", "arg21"], "") == ["arg", "argument"]
|
|
||||||
assert choices_without_help(cli, ["arg12", "arg21"], "argu") == ["argument"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_option_choice():
|
|
||||||
@click.command()
|
|
||||||
@click.option("--opt1", type=click.Choice(["opt11", "opt12"]), help="opt1 help")
|
|
||||||
@click.option("--opt2", type=click.Choice(["opt21", "opt22"]), default="opt21")
|
|
||||||
@click.option("--opt3", type=click.Choice(["opt", "option"]))
|
|
||||||
def cli():
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_with_help(cli, [], "-") == [
|
|
||||||
("--opt1", "opt1 help"),
|
|
||||||
("--opt2", None),
|
|
||||||
("--opt3", None),
|
|
||||||
]
|
|
||||||
assert choices_without_help(cli, [], "--opt") == ["--opt1", "--opt2", "--opt3"]
|
|
||||||
assert choices_without_help(cli, [], "--opt1=") == ["opt11", "opt12"]
|
|
||||||
assert choices_without_help(cli, [], "--opt2=") == ["opt21", "opt22"]
|
|
||||||
assert choices_without_help(cli, ["--opt2"], "=") == ["opt21", "opt22"]
|
|
||||||
assert choices_without_help(cli, ["--opt2", "="], "opt") == ["opt21", "opt22"]
|
|
||||||
assert choices_without_help(cli, ["--opt1"], "") == ["opt11", "opt12"]
|
|
||||||
assert choices_without_help(cli, ["--opt2"], "") == ["opt21", "opt22"]
|
|
||||||
assert choices_without_help(cli, ["--opt1", "opt11", "--opt2"], "") == [
|
|
||||||
"opt21",
|
|
||||||
"opt22",
|
|
||||||
]
|
|
||||||
assert choices_without_help(cli, ["--opt2", "opt21"], "-") == ["--opt1", "--opt3"]
|
|
||||||
assert choices_without_help(cli, ["--opt1", "opt11"], "-") == ["--opt2", "--opt3"]
|
|
||||||
assert choices_without_help(cli, ["--opt1"], "opt") == ["opt11", "opt12"]
|
|
||||||
assert choices_without_help(cli, ["--opt3"], "opti") == ["option"]
|
|
||||||
|
|
||||||
assert choices_without_help(cli, ["--opt1", "invalid_opt"], "-") == [
|
|
||||||
"--opt2",
|
|
||||||
"--opt3",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_option_and_arg_choice():
|
|
||||||
@click.command()
|
|
||||||
@click.option("--opt1", type=click.Choice(["opt11", "opt12"]))
|
|
||||||
@click.argument("arg1", required=False, type=click.Choice(["arg11", "arg12"]))
|
|
||||||
@click.option("--opt2", type=click.Choice(["opt21", "opt22"]))
|
|
||||||
def cli():
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, ["--opt1"], "") == ["opt11", "opt12"]
|
|
||||||
assert choices_without_help(cli, [""], "--opt1=") == ["opt11", "opt12"]
|
|
||||||
assert choices_without_help(cli, [], "") == ["arg11", "arg12"]
|
|
||||||
assert choices_without_help(cli, ["--opt2"], "") == ["opt21", "opt22"]
|
|
||||||
assert choices_without_help(cli, ["arg11"], "--opt") == ["--opt1", "--opt2"]
|
|
||||||
assert choices_without_help(cli, [], "--opt") == ["--opt1", "--opt2"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_boolean_flag_choice():
|
|
||||||
@click.command()
|
|
||||||
@click.option("--shout/--no-shout", default=False)
|
|
||||||
@click.argument("arg", required=False, type=click.Choice(["arg1", "arg2"]))
|
|
||||||
def cli(local_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, [], "-") == ["--shout", "--no-shout"]
|
|
||||||
assert choices_without_help(cli, ["--shout"], "") == ["arg1", "arg2"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_multi_value_option_choice():
|
|
||||||
@click.command()
|
|
||||||
@click.option("--pos", nargs=2, type=click.Choice(["pos1", "pos2"]))
|
|
||||||
@click.argument("arg", required=False, type=click.Choice(["arg1", "arg2"]))
|
|
||||||
def cli(local_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, ["--pos"], "") == ["pos1", "pos2"]
|
|
||||||
assert choices_without_help(cli, ["--pos", "pos1"], "") == ["pos1", "pos2"]
|
|
||||||
assert choices_without_help(cli, ["--pos", "pos1", "pos2"], "") == ["arg1", "arg2"]
|
|
||||||
assert choices_without_help(cli, ["--pos", "pos1", "pos2", "arg1"], "") == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_multi_option_choice():
|
|
||||||
@click.command()
|
|
||||||
@click.option("--message", "-m", multiple=True, type=click.Choice(["m1", "m2"]))
|
|
||||||
@click.argument("arg", required=False, type=click.Choice(["arg1", "arg2"]))
|
|
||||||
def cli(local_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, ["-m"], "") == ["m1", "m2"]
|
|
||||||
assert choices_without_help(cli, ["-m", "m1", "-m"], "") == ["m1", "m2"]
|
|
||||||
assert choices_without_help(cli, ["-m", "m1"], "") == ["arg1", "arg2"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_variadic_argument_choice():
|
|
||||||
@click.command()
|
|
||||||
@click.option("--opt", type=click.Choice(["opt1", "opt2"]))
|
|
||||||
@click.argument("src", nargs=-1, type=click.Choice(["src1", "src2"]))
|
|
||||||
def cli(local_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, ["src1", "src2"], "") == ["src1", "src2"]
|
|
||||||
assert choices_without_help(cli, ["src1", "src2"], "--o") == ["--opt"]
|
|
||||||
assert choices_without_help(cli, ["src1", "src2", "--opt"], "") == ["opt1", "opt2"]
|
|
||||||
assert choices_without_help(cli, ["src1", "src2"], "") == ["src1", "src2"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_variadic_argument_complete():
|
|
||||||
def _complete(ctx, args, incomplete):
|
|
||||||
return ["abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz"]
|
|
||||||
|
|
||||||
@click.group()
|
|
||||||
def entrypoint():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option("--opt", autocompletion=_complete)
|
|
||||||
@click.argument("arg", nargs=-1)
|
|
||||||
def subcommand(opt, arg):
|
|
||||||
pass
|
|
||||||
|
|
||||||
entrypoint.add_command(subcommand)
|
|
||||||
|
|
||||||
assert choices_without_help(entrypoint, ["subcommand", "--opt"], "") == _complete(
|
|
||||||
0, 0, 0
|
|
||||||
)
|
|
||||||
assert choices_without_help(
|
|
||||||
entrypoint, ["subcommand", "whatever", "--opt"], ""
|
|
||||||
) == _complete(0, 0, 0)
|
|
||||||
assert (
|
|
||||||
choices_without_help(entrypoint, ["subcommand", "whatever", "--opt", "abc"], "")
|
|
||||||
== []
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_long_chain_choice():
|
|
||||||
@click.group()
|
|
||||||
def cli():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.group()
|
|
||||||
@click.option("--sub-opt", type=click.Choice(["subopt1", "subopt2"]))
|
|
||||||
@click.argument(
|
|
||||||
"sub-arg", required=False, type=click.Choice(["subarg1", "subarg2"])
|
|
||||||
)
|
|
||||||
def sub(sub_opt, sub_arg):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@sub.command(short_help="bsub help")
|
|
||||||
@click.option("--bsub-opt", type=click.Choice(["bsubopt1", "bsubopt2"]))
|
|
||||||
@click.argument(
|
|
||||||
"bsub-arg1", required=False, type=click.Choice(["bsubarg1", "bsubarg2"])
|
|
||||||
)
|
|
||||||
@click.argument(
|
|
||||||
"bbsub-arg2", required=False, type=click.Choice(["bbsubarg1", "bbsubarg2"])
|
|
||||||
)
|
|
||||||
def bsub(bsub_opt):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@sub.group("csub")
|
|
||||||
def csub():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@csub.command()
|
|
||||||
def dsub():
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_with_help(cli, ["sub", "subarg1"], "") == [
|
|
||||||
("bsub", "bsub help"),
|
|
||||||
("csub", ""),
|
|
||||||
]
|
|
||||||
assert choices_without_help(cli, ["sub"], "") == ["subarg1", "subarg2"]
|
|
||||||
assert choices_without_help(cli, ["sub", "--sub-opt"], "") == ["subopt1", "subopt2"]
|
|
||||||
assert choices_without_help(cli, ["sub", "--sub-opt", "subopt1"], "") == [
|
|
||||||
"subarg1",
|
|
||||||
"subarg2",
|
|
||||||
]
|
|
||||||
assert choices_without_help(
|
|
||||||
cli, ["sub", "--sub-opt", "subopt1", "subarg1", "bsub"], "-"
|
|
||||||
) == ["--bsub-opt"]
|
|
||||||
assert choices_without_help(
|
|
||||||
cli, ["sub", "--sub-opt", "subopt1", "subarg1", "bsub"], ""
|
|
||||||
) == ["bsubarg1", "bsubarg2"]
|
|
||||||
assert choices_without_help(
|
|
||||||
cli, ["sub", "--sub-opt", "subopt1", "subarg1", "bsub", "--bsub-opt"], ""
|
|
||||||
) == ["bsubopt1", "bsubopt2"]
|
|
||||||
assert choices_without_help(
|
|
||||||
cli,
|
|
||||||
[
|
|
||||||
"sub",
|
|
||||||
"--sub-opt",
|
|
||||||
"subopt1",
|
|
||||||
"subarg1",
|
|
||||||
"bsub",
|
|
||||||
"--bsub-opt",
|
|
||||||
"bsubopt1",
|
|
||||||
"bsubarg1",
|
|
||||||
],
|
|
||||||
"",
|
|
||||||
) == ["bbsubarg1", "bbsubarg2"]
|
|
||||||
assert choices_without_help(
|
|
||||||
cli, ["sub", "--sub-opt", "subopt1", "subarg1", "csub"], ""
|
|
||||||
) == ["dsub"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_chained_multi():
|
|
||||||
@click.group()
|
|
||||||
def cli():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.group()
|
|
||||||
def sub():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@sub.group()
|
|
||||||
def bsub():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@sub.group(chain=True)
|
|
||||||
def csub():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@csub.command()
|
|
||||||
def dsub():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@csub.command()
|
|
||||||
def esub():
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, ["sub"], "") == ["bsub", "csub"]
|
|
||||||
assert choices_without_help(cli, ["sub"], "c") == ["csub"]
|
|
||||||
assert choices_without_help(cli, ["sub", "csub"], "") == ["dsub", "esub"]
|
|
||||||
assert choices_without_help(cli, ["sub", "csub", "dsub"], "") == ["esub"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_hidden():
|
|
||||||
@click.group()
|
|
||||||
@click.option("--name", hidden=True)
|
|
||||||
@click.option("--choices", type=click.Choice([1, 2]), hidden=True)
|
|
||||||
def cli(name):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.group(hidden=True)
|
|
||||||
def hgroup():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@hgroup.group()
|
|
||||||
def hgroupsub():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
def asub():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.command(hidden=True)
|
|
||||||
@click.option("--hname")
|
|
||||||
def hsub():
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, [], "--n") == []
|
|
||||||
assert choices_without_help(cli, [], "--c") == []
|
|
||||||
# If the user exactly types out the hidden param, complete its options.
|
|
||||||
assert choices_without_help(cli, ["--choices"], "") == [1, 2]
|
|
||||||
assert choices_without_help(cli, [], "") == ["asub"]
|
|
||||||
assert choices_without_help(cli, [], "") == ["asub"]
|
|
||||||
assert choices_without_help(cli, [], "h") == []
|
|
||||||
# If the user exactly types out the hidden command, complete its subcommands.
|
|
||||||
assert choices_without_help(cli, ["hgroup"], "") == ["hgroupsub"]
|
|
||||||
assert choices_without_help(cli, ["hsub"], "--h") == ["--hname"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("args", "part", "expect"),
|
|
||||||
[
|
|
||||||
([], "-", ["--opt"]),
|
|
||||||
(["value"], "--", ["--opt"]),
|
|
||||||
([], "-o", []),
|
|
||||||
(["--opt"], "-o", []),
|
|
||||||
(["--"], "", ["name", "-o", "--opt", "--"]),
|
|
||||||
(["--"], "--o", ["--opt"]),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_args_with_double_dash_complete(args, part, expect):
|
|
||||||
def _complete(ctx, args, incomplete):
|
|
||||||
values = ["name", "-o", "--opt", "--"]
|
|
||||||
return [x for x in values if x.startswith(incomplete)]
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option("--opt")
|
|
||||||
@click.argument("args", nargs=-1, autocompletion=_complete)
|
|
||||||
def cli(opt, args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert choices_without_help(cli, args, part) == expect
|
|
|
@ -1,6 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
@ -78,11 +80,35 @@ def test_basic_group(runner):
|
||||||
assert "SUBCOMMAND EXECUTED" in result.output
|
assert "SUBCOMMAND EXECUTED" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_commands_dict(runner):
|
||||||
|
"""A Group can be built with a dict of commands."""
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def sub():
|
||||||
|
click.echo("sub", nl=False)
|
||||||
|
|
||||||
|
cli = click.Group(commands={"other": sub})
|
||||||
|
result = runner.invoke(cli, ["other"])
|
||||||
|
assert result.output == "sub"
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_from_list(runner):
|
||||||
|
"""A Group can be built with a list of commands."""
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def sub():
|
||||||
|
click.echo("sub", nl=False)
|
||||||
|
|
||||||
|
cli = click.Group(commands=[sub])
|
||||||
|
result = runner.invoke(cli, ["sub"])
|
||||||
|
assert result.output == "sub"
|
||||||
|
|
||||||
|
|
||||||
def test_basic_option(runner):
|
def test_basic_option(runner):
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--foo", default="no value")
|
@click.option("--foo", default="no value")
|
||||||
def cli(foo):
|
def cli(foo):
|
||||||
click.echo(u"FOO:[{}]".format(foo))
|
click.echo(f"FOO:[{foo}]")
|
||||||
|
|
||||||
result = runner.invoke(cli, [])
|
result = runner.invoke(cli, [])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
|
@ -94,22 +120,22 @@ def test_basic_option(runner):
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--foo"])
|
result = runner.invoke(cli, ["--foo"])
|
||||||
assert result.exception
|
assert result.exception
|
||||||
assert "--foo option requires an argument" in result.output
|
assert "Option '--foo' requires an argument." in result.output
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--foo="])
|
result = runner.invoke(cli, ["--foo="])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
assert "FOO:[]" in result.output
|
assert "FOO:[]" in result.output
|
||||||
|
|
||||||
result = runner.invoke(cli, [u"--foo=\N{SNOWMAN}"])
|
result = runner.invoke(cli, ["--foo=\N{SNOWMAN}"])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
assert u"FOO:[\N{SNOWMAN}]" in result.output
|
assert "FOO:[\N{SNOWMAN}]" in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_int_option(runner):
|
def test_int_option(runner):
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--foo", default=42)
|
@click.option("--foo", default=42)
|
||||||
def cli(foo):
|
def cli(foo):
|
||||||
click.echo("FOO:[{}]".format(foo * 2))
|
click.echo(f"FOO:[{foo * 2}]")
|
||||||
|
|
||||||
result = runner.invoke(cli, [])
|
result = runner.invoke(cli, [])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
|
@ -121,7 +147,7 @@ def test_int_option(runner):
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--foo=bar"])
|
result = runner.invoke(cli, ["--foo=bar"])
|
||||||
assert result.exception
|
assert result.exception
|
||||||
assert "Invalid value for '--foo': bar is not a valid integer" in result.output
|
assert "Invalid value for '--foo': 'bar' is not a valid integer." in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_uuid_option(runner):
|
def test_uuid_option(runner):
|
||||||
|
@ -131,7 +157,7 @@ def test_uuid_option(runner):
|
||||||
)
|
)
|
||||||
def cli(u):
|
def cli(u):
|
||||||
assert type(u) is uuid.UUID
|
assert type(u) is uuid.UUID
|
||||||
click.echo("U:[{}]".format(u))
|
click.echo(f"U:[{u}]")
|
||||||
|
|
||||||
result = runner.invoke(cli, [])
|
result = runner.invoke(cli, [])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
|
@ -143,7 +169,7 @@ def test_uuid_option(runner):
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--u=bar"])
|
result = runner.invoke(cli, ["--u=bar"])
|
||||||
assert result.exception
|
assert result.exception
|
||||||
assert "Invalid value for '--u': bar is not a valid UUID value" in result.output
|
assert "Invalid value for '--u': 'bar' is not a valid UUID." in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_float_option(runner):
|
def test_float_option(runner):
|
||||||
|
@ -151,7 +177,7 @@ def test_float_option(runner):
|
||||||
@click.option("--foo", default=42, type=click.FLOAT)
|
@click.option("--foo", default=42, type=click.FLOAT)
|
||||||
def cli(foo):
|
def cli(foo):
|
||||||
assert type(foo) is float
|
assert type(foo) is float
|
||||||
click.echo("FOO:[{}]".format(foo))
|
click.echo(f"FOO:[{foo}]")
|
||||||
|
|
||||||
result = runner.invoke(cli, [])
|
result = runner.invoke(cli, [])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
|
@ -163,7 +189,7 @@ def test_float_option(runner):
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--foo=bar"])
|
result = runner.invoke(cli, ["--foo=bar"])
|
||||||
assert result.exception
|
assert result.exception
|
||||||
assert "Invalid value for '--foo': bar is not a valid float" in result.output
|
assert "Invalid value for '--foo': 'bar' is not a valid float." in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_boolean_option(runner):
|
def test_boolean_option(runner):
|
||||||
|
@ -182,7 +208,7 @@ def test_boolean_option(runner):
|
||||||
assert result.output == "False\n"
|
assert result.output == "False\n"
|
||||||
result = runner.invoke(cli, [])
|
result = runner.invoke(cli, [])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
assert result.output == "{}\n".format(default)
|
assert result.output == f"{default}\n"
|
||||||
|
|
||||||
for default in True, False:
|
for default in True, False:
|
||||||
|
|
||||||
|
@ -193,33 +219,30 @@ def test_boolean_option(runner):
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--flag"])
|
result = runner.invoke(cli, ["--flag"])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
assert result.output == "{}\n".format(not default)
|
assert result.output == f"{not default}\n"
|
||||||
result = runner.invoke(cli, [])
|
result = runner.invoke(cli, [])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
assert result.output == "{}\n".format(default)
|
assert result.output == f"{default}\n"
|
||||||
|
|
||||||
|
|
||||||
def test_boolean_conversion(runner):
|
@pytest.mark.parametrize(
|
||||||
for default in True, False:
|
("value", "expect"),
|
||||||
|
chain(
|
||||||
|
((x, "True") for x in ("1", "true", "t", "yes", "y", "on")),
|
||||||
|
((x, "False") for x in ("0", "false", "f", "no", "n", "off")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_boolean_conversion(runner, value, expect):
|
||||||
|
@click.command()
|
||||||
|
@click.option("--flag", type=bool)
|
||||||
|
def cli(flag):
|
||||||
|
click.echo(flag, nl=False)
|
||||||
|
|
||||||
@click.command()
|
result = runner.invoke(cli, ["--flag", value])
|
||||||
@click.option("--flag", default=default, type=bool)
|
assert result.output == expect
|
||||||
def cli(flag):
|
|
||||||
click.echo(flag)
|
|
||||||
|
|
||||||
for value in "true", "t", "1", "yes", "y":
|
result = runner.invoke(cli, ["--flag", value.title()])
|
||||||
result = runner.invoke(cli, ["--flag", value])
|
assert result.output == expect
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "True\n"
|
|
||||||
|
|
||||||
for value in "false", "f", "0", "no", "n":
|
|
||||||
result = runner.invoke(cli, ["--flag", value])
|
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "False\n"
|
|
||||||
|
|
||||||
result = runner.invoke(cli, [])
|
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "{}\n".format(default)
|
|
||||||
|
|
||||||
|
|
||||||
def test_file_option(runner):
|
def test_file_option(runner):
|
||||||
|
@ -280,10 +303,7 @@ def test_file_lazy_mode(runner):
|
||||||
os.mkdir("example.txt")
|
os.mkdir("example.txt")
|
||||||
result_in = runner.invoke(input_non_lazy, ["--file=example.txt"])
|
result_in = runner.invoke(input_non_lazy, ["--file=example.txt"])
|
||||||
assert result_in.exit_code == 2
|
assert result_in.exit_code == 2
|
||||||
assert (
|
assert "Invalid value for '--file': 'example.txt'" in result_in.output
|
||||||
"Invalid value for '--file': Could not open file: example.txt"
|
|
||||||
in result_in.output
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_path_option(runner):
|
def test_path_option(runner):
|
||||||
|
@ -308,8 +328,8 @@ def test_path_option(runner):
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("-f", type=click.Path(exists=True))
|
@click.option("-f", type=click.Path(exists=True))
|
||||||
def showtype(f):
|
def showtype(f):
|
||||||
click.echo("is_file={}".format(os.path.isfile(f)))
|
click.echo(f"is_file={os.path.isfile(f)}")
|
||||||
click.echo("is_dir={}".format(os.path.isdir(f)))
|
click.echo(f"is_dir={os.path.isdir(f)}")
|
||||||
|
|
||||||
with runner.isolated_filesystem():
|
with runner.isolated_filesystem():
|
||||||
result = runner.invoke(showtype, ["-f", "xxx"])
|
result = runner.invoke(showtype, ["-f", "xxx"])
|
||||||
|
@ -322,7 +342,7 @@ def test_path_option(runner):
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("-f", type=click.Path())
|
@click.option("-f", type=click.Path())
|
||||||
def exists(f):
|
def exists(f):
|
||||||
click.echo("exists={}".format(os.path.exists(f)))
|
click.echo(f"exists={os.path.exists(f)}")
|
||||||
|
|
||||||
with runner.isolated_filesystem():
|
with runner.isolated_filesystem():
|
||||||
result = runner.invoke(exists, ["-f", "xxx"])
|
result = runner.invoke(exists, ["-f", "xxx"])
|
||||||
|
@ -345,14 +365,35 @@ def test_choice_option(runner):
|
||||||
result = runner.invoke(cli, ["--method=meh"])
|
result = runner.invoke(cli, ["--method=meh"])
|
||||||
assert result.exit_code == 2
|
assert result.exit_code == 2
|
||||||
assert (
|
assert (
|
||||||
"Invalid value for '--method': invalid choice: meh."
|
"Invalid value for '--method': 'meh' is not one of 'foo', 'bar', 'baz'."
|
||||||
" (choose from foo, bar, baz)" in result.output
|
in result.output
|
||||||
)
|
)
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--help"])
|
result = runner.invoke(cli, ["--help"])
|
||||||
assert "--method [foo|bar|baz]" in result.output
|
assert "--method [foo|bar|baz]" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_choice_argument(runner):
|
||||||
|
@click.command()
|
||||||
|
@click.argument("method", type=click.Choice(["foo", "bar", "baz"]))
|
||||||
|
def cli(method):
|
||||||
|
click.echo(method)
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["foo"])
|
||||||
|
assert not result.exception
|
||||||
|
assert result.output == "foo\n"
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["meh"])
|
||||||
|
assert result.exit_code == 2
|
||||||
|
assert (
|
||||||
|
"Invalid value for '{foo|bar|baz}': 'meh' is not one of 'foo',"
|
||||||
|
" 'bar', 'baz'." in result.output
|
||||||
|
)
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["--help"])
|
||||||
|
assert "{foo|bar|baz}" in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_datetime_option_default(runner):
|
def test_datetime_option_default(runner):
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--start_date", type=click.DateTime())
|
@click.option("--start_date", type=click.DateTime())
|
||||||
|
@ -370,9 +411,8 @@ def test_datetime_option_default(runner):
|
||||||
result = runner.invoke(cli, ["--start_date=2015-09"])
|
result = runner.invoke(cli, ["--start_date=2015-09"])
|
||||||
assert result.exit_code == 2
|
assert result.exit_code == 2
|
||||||
assert (
|
assert (
|
||||||
"Invalid value for '--start_date':"
|
"Invalid value for '--start_date': '2015-09' does not match the formats"
|
||||||
" invalid datetime format: 2015-09."
|
" '%Y-%m-%d', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S'."
|
||||||
" (choose from %Y-%m-%d, %Y-%m-%dT%H:%M:%S, %Y-%m-%d %H:%M:%S)"
|
|
||||||
) in result.output
|
) in result.output
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--help"])
|
result = runner.invoke(cli, ["--help"])
|
||||||
|
@ -392,76 +432,6 @@ def test_datetime_option_custom(runner):
|
||||||
assert result.output == "2010-06-05T00:00:00\n"
|
assert result.output == "2010-06-05T00:00:00\n"
|
||||||
|
|
||||||
|
|
||||||
def test_int_range_option(runner):
|
|
||||||
@click.command()
|
|
||||||
@click.option("--x", type=click.IntRange(0, 5))
|
|
||||||
def cli(x):
|
|
||||||
click.echo(x)
|
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--x=5"])
|
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "5\n"
|
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--x=6"])
|
|
||||||
assert result.exit_code == 2
|
|
||||||
assert (
|
|
||||||
"Invalid value for '--x': 6 is not in the valid range of 0 to 5.\n"
|
|
||||||
in result.output
|
|
||||||
)
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option("--x", type=click.IntRange(0, 5, clamp=True))
|
|
||||||
def clamp(x):
|
|
||||||
click.echo(x)
|
|
||||||
|
|
||||||
result = runner.invoke(clamp, ["--x=5"])
|
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "5\n"
|
|
||||||
|
|
||||||
result = runner.invoke(clamp, ["--x=6"])
|
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "5\n"
|
|
||||||
|
|
||||||
result = runner.invoke(clamp, ["--x=-1"])
|
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "0\n"
|
|
||||||
|
|
||||||
|
|
||||||
def test_float_range_option(runner):
|
|
||||||
@click.command()
|
|
||||||
@click.option("--x", type=click.FloatRange(0, 5))
|
|
||||||
def cli(x):
|
|
||||||
click.echo(x)
|
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--x=5.0"])
|
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "5.0\n"
|
|
||||||
|
|
||||||
result = runner.invoke(cli, ["--x=6.0"])
|
|
||||||
assert result.exit_code == 2
|
|
||||||
assert (
|
|
||||||
"Invalid value for '--x': 6.0 is not in the valid range of 0 to 5.\n"
|
|
||||||
in result.output
|
|
||||||
)
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option("--x", type=click.FloatRange(0, 5, clamp=True))
|
|
||||||
def clamp(x):
|
|
||||||
click.echo(x)
|
|
||||||
|
|
||||||
result = runner.invoke(clamp, ["--x=5.0"])
|
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "5.0\n"
|
|
||||||
|
|
||||||
result = runner.invoke(clamp, ["--x=6.0"])
|
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "5\n"
|
|
||||||
|
|
||||||
result = runner.invoke(clamp, ["--x=-1.0"])
|
|
||||||
assert not result.exception
|
|
||||||
assert result.output == "0\n"
|
|
||||||
|
|
||||||
|
|
||||||
def test_required_option(runner):
|
def test_required_option(runner):
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--foo", required=True)
|
@click.option("--foo", required=True)
|
||||||
|
@ -558,3 +528,39 @@ def test_hidden_group(runner):
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "subgroup" not in result.output
|
assert "subgroup" not in result.output
|
||||||
assert "nope" not in result.output
|
assert "nope" not in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_summary_line(runner):
|
||||||
|
@click.group()
|
||||||
|
def cli():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def cmd():
|
||||||
|
"""
|
||||||
|
Summary line without period
|
||||||
|
|
||||||
|
Here is a sentence. And here too.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["--help"])
|
||||||
|
assert "Summary line without period" in result.output
|
||||||
|
assert "Here is a sentence." not in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_help_invalid_default(runner):
|
||||||
|
cli = click.Command(
|
||||||
|
"cli",
|
||||||
|
params=[
|
||||||
|
click.Option(
|
||||||
|
["-a"],
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
default="not found",
|
||||||
|
show_default=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
result = runner.invoke(cli, ["--help"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "default: not found" in result.output
|
||||||
|
|
|
@ -7,9 +7,8 @@ import click
|
||||||
|
|
||||||
def debug():
|
def debug():
|
||||||
click.echo(
|
click.echo(
|
||||||
"{}={}".format(
|
f"{sys._getframe(1).f_code.co_name}"
|
||||||
sys._getframe(1).f_code.co_name, "|".join(click.get_current_context().args)
|
f"={'|'.join(click.get_current_context().args)}"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,18 +76,37 @@ def test_chaining_with_options(runner):
|
||||||
@cli.command("sdist")
|
@cli.command("sdist")
|
||||||
@click.option("--format")
|
@click.option("--format")
|
||||||
def sdist(format):
|
def sdist(format):
|
||||||
click.echo("sdist called {}".format(format))
|
click.echo(f"sdist called {format}")
|
||||||
|
|
||||||
@cli.command("bdist")
|
@cli.command("bdist")
|
||||||
@click.option("--format")
|
@click.option("--format")
|
||||||
def bdist(format):
|
def bdist(format):
|
||||||
click.echo("bdist called {}".format(format))
|
click.echo(f"bdist called {format}")
|
||||||
|
|
||||||
result = runner.invoke(cli, ["bdist", "--format=1", "sdist", "--format=2"])
|
result = runner.invoke(cli, ["bdist", "--format=1", "sdist", "--format=2"])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
assert result.output.splitlines() == ["bdist called 1", "sdist called 2"]
|
assert result.output.splitlines() == ["bdist called 1", "sdist called 2"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("chain", "expect"), [(False, "None"), (True, "[]")])
|
||||||
|
def test_no_command_result_callback(runner, chain, expect):
|
||||||
|
"""When a group has ``invoke_without_command=True``, the result
|
||||||
|
callback is always invoked. A regular group invokes it with
|
||||||
|
``None``, a chained group with ``[]``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@click.group(invoke_without_command=True, chain=chain)
|
||||||
|
def cli():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@cli.result_callback()
|
||||||
|
def process_result(result):
|
||||||
|
click.echo(str(result), nl=False)
|
||||||
|
|
||||||
|
result = runner.invoke(cli, [])
|
||||||
|
assert result.output == expect
|
||||||
|
|
||||||
|
|
||||||
def test_chaining_with_arguments(runner):
|
def test_chaining_with_arguments(runner):
|
||||||
@click.group(chain=True)
|
@click.group(chain=True)
|
||||||
def cli():
|
def cli():
|
||||||
|
@ -97,12 +115,12 @@ def test_chaining_with_arguments(runner):
|
||||||
@cli.command("sdist")
|
@cli.command("sdist")
|
||||||
@click.argument("format")
|
@click.argument("format")
|
||||||
def sdist(format):
|
def sdist(format):
|
||||||
click.echo("sdist called {}".format(format))
|
click.echo(f"sdist called {format}")
|
||||||
|
|
||||||
@cli.command("bdist")
|
@cli.command("bdist")
|
||||||
@click.argument("format")
|
@click.argument("format")
|
||||||
def bdist(format):
|
def bdist(format):
|
||||||
click.echo("bdist called {}".format(format))
|
click.echo(f"bdist called {format}")
|
||||||
|
|
||||||
result = runner.invoke(cli, ["bdist", "1", "sdist", "2"])
|
result = runner.invoke(cli, ["bdist", "1", "sdist", "2"])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
|
@ -115,7 +133,7 @@ def test_pipeline(runner):
|
||||||
def cli(input):
|
def cli(input):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@cli.resultcallback()
|
@cli.result_callback()
|
||||||
def process_pipeline(processors, input):
|
def process_pipeline(processors, input):
|
||||||
iterator = (x.rstrip("\r\n") for x in input)
|
iterator = (x.rstrip("\r\n") for x in input)
|
||||||
for processor in processors:
|
for processor in processors:
|
||||||
|
@ -192,7 +210,7 @@ def test_multicommand_arg_behavior(runner):
|
||||||
@click.group(chain=True)
|
@click.group(chain=True)
|
||||||
@click.argument("arg")
|
@click.argument("arg")
|
||||||
def cli(arg):
|
def cli(arg):
|
||||||
click.echo("cli:{}".format(arg))
|
click.echo(f"cli:{arg}")
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def a():
|
def a():
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
@ -26,7 +25,7 @@ def test_other_command_forward(runner):
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option("--count", default=1)
|
@click.option("--count", default=1)
|
||||||
def test(count):
|
def test(count):
|
||||||
click.echo("Count: {:d}".format(count))
|
click.echo(f"Count: {count:d}")
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option("--count", default=1)
|
@click.option("--count", default=1)
|
||||||
|
@ -40,6 +39,28 @@ def test_other_command_forward(runner):
|
||||||
assert result.output == "Count: 1\nCount: 42\n"
|
assert result.output == "Count: 1\nCount: 42\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_forwarded_params_consistency(runner):
|
||||||
|
cli = click.Group()
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("-a")
|
||||||
|
@click.pass_context
|
||||||
|
def first(ctx, **kwargs):
|
||||||
|
click.echo(f"{ctx.params}")
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("-a")
|
||||||
|
@click.option("-b")
|
||||||
|
@click.pass_context
|
||||||
|
def second(ctx, **kwargs):
|
||||||
|
click.echo(f"{ctx.params}")
|
||||||
|
ctx.forward(first)
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["second", "-a", "foo", "-b", "bar"])
|
||||||
|
assert not result.exception
|
||||||
|
assert result.output == "{'a': 'foo', 'b': 'bar'}\n{'a': 'foo', 'b': 'bar'}\n"
|
||||||
|
|
||||||
|
|
||||||
def test_auto_shorthelp(runner):
|
def test_auto_shorthelp(runner):
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():
|
def cli():
|
||||||
|
@ -102,7 +123,7 @@ def test_group_with_args(runner):
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.argument("obj")
|
@click.argument("obj")
|
||||||
def cli(obj):
|
def cli(obj):
|
||||||
click.echo("obj={}".format(obj))
|
click.echo(f"obj={obj}")
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def move():
|
def move():
|
||||||
|
@ -134,7 +155,7 @@ def test_base_command(runner):
|
||||||
|
|
||||||
class OptParseCommand(click.BaseCommand):
|
class OptParseCommand(click.BaseCommand):
|
||||||
def __init__(self, name, parser, callback):
|
def __init__(self, name, parser, callback):
|
||||||
click.BaseCommand.__init__(self, name)
|
super().__init__(name)
|
||||||
self.parser = parser
|
self.parser = parser
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
|
|
||||||
|
@ -211,7 +232,7 @@ def test_object_propagation(runner):
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def sync(ctx):
|
def sync(ctx):
|
||||||
click.echo("Debug is {}".format("on" if ctx.obj["DEBUG"] else "off"))
|
click.echo(f"Debug is {'on' if ctx.obj['DEBUG'] else 'off'}")
|
||||||
|
|
||||||
result = runner.invoke(cli, ["sync"])
|
result = runner.invoke(cli, ["sync"])
|
||||||
assert result.exception is None
|
assert result.exception is None
|
||||||
|
@ -259,13 +280,43 @@ def test_invoked_subcommand(runner):
|
||||||
assert result.output == "no subcommand, use default\nin subcommand\n"
|
assert result.output == "no subcommand, use default\nin subcommand\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_aliased_command_canonical_name(runner):
|
||||||
|
class AliasedGroup(click.Group):
|
||||||
|
def get_command(self, ctx, cmd_name):
|
||||||
|
return push
|
||||||
|
|
||||||
|
def resolve_command(self, ctx, args):
|
||||||
|
_, command, args = super().resolve_command(ctx, args)
|
||||||
|
return command.name, command, args
|
||||||
|
|
||||||
|
cli = AliasedGroup()
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def push():
|
||||||
|
click.echo("push command")
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["pu", "--help"])
|
||||||
|
assert not result.exception
|
||||||
|
assert result.output.startswith("Usage: root push [OPTIONS]")
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_add_command_name(runner):
|
||||||
|
cli = click.Group("cli")
|
||||||
|
cmd = click.Command("a", params=[click.Option(["-x"], required=True)])
|
||||||
|
cli.add_command(cmd, "b")
|
||||||
|
# Check that the command is accessed through the registered name,
|
||||||
|
# not the original name.
|
||||||
|
result = runner.invoke(cli, ["b"], default_map={"b": {"x": 3}})
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
def test_unprocessed_options(runner):
|
def test_unprocessed_options(runner):
|
||||||
@click.command(context_settings=dict(ignore_unknown_options=True))
|
@click.command(context_settings=dict(ignore_unknown_options=True))
|
||||||
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
||||||
@click.option("--verbose", "-v", count=True)
|
@click.option("--verbose", "-v", count=True)
|
||||||
def cli(verbose, args):
|
def cli(verbose, args):
|
||||||
click.echo("Verbosity: {}".format(verbose))
|
click.echo(f"Verbosity: {verbose}")
|
||||||
click.echo("Args: {}".format("|".join(args)))
|
click.echo(f"Args: {'|'.join(args)}")
|
||||||
|
|
||||||
result = runner.invoke(cli, ["-foo", "-vvvvx", "--muhaha", "x", "y", "-x"])
|
result = runner.invoke(cli, ["-foo", "-vvvvx", "--muhaha", "x", "y", "-x"])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
|
@ -282,14 +333,14 @@ def test_deprecated_in_help_messages(runner):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
result = runner.invoke(cmd_with_help, ["--help"])
|
result = runner.invoke(cmd_with_help, ["--help"])
|
||||||
assert "(DEPRECATED)" in result.output
|
assert "(Deprecated)" in result.output
|
||||||
|
|
||||||
@click.command(deprecated=True)
|
@click.command(deprecated=True)
|
||||||
def cmd_without_help():
|
def cmd_without_help():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
result = runner.invoke(cmd_without_help, ["--help"])
|
result = runner.invoke(cmd_without_help, ["--help"])
|
||||||
assert "(DEPRECATED)" in result.output
|
assert "(Deprecated)" in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_deprecated_in_invocation(runner):
|
def test_deprecated_in_invocation(runner):
|
||||||
|
|
|
@ -1,45 +1,8 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
import click
|
|
||||||
from click._compat import should_strip_ansi
|
from click._compat import should_strip_ansi
|
||||||
from click._compat import WIN
|
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_callbacks(runner):
|
|
||||||
def legacy_callback(ctx, value):
|
|
||||||
return value.upper()
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option("--foo", callback=legacy_callback)
|
|
||||||
def cli(foo):
|
|
||||||
click.echo(foo)
|
|
||||||
|
|
||||||
with pytest.warns(DeprecationWarning, match="2-arg style"):
|
|
||||||
result = runner.invoke(cli, ["--foo", "wat"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "WAT" in result.output
|
|
||||||
|
|
||||||
|
|
||||||
def test_bash_func_name():
|
|
||||||
from click._bashcomplete import get_completion_script
|
|
||||||
|
|
||||||
script = get_completion_script("foo-bar baz_blah", "_COMPLETE_VAR", "bash").strip()
|
|
||||||
assert script.startswith("_foo_barbaz_blah_completion()")
|
|
||||||
assert "_COMPLETE_VAR=complete $1" in script
|
|
||||||
|
|
||||||
|
|
||||||
def test_zsh_func_name():
|
|
||||||
from click._bashcomplete import get_completion_script
|
|
||||||
|
|
||||||
script = get_completion_script("foo-bar", "_COMPLETE_VAR", "zsh").strip()
|
|
||||||
assert script.startswith("#compdef foo-bar")
|
|
||||||
assert "compdef _foo_bar_completion foo-bar;" in script
|
|
||||||
assert "(( ! $+commands[foo-bar] )) && return 1" in script
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(WIN, reason="Jupyter not tested/supported on Windows")
|
|
||||||
def test_is_jupyter_kernel_output():
|
def test_is_jupyter_kernel_output():
|
||||||
class JupyterKernelFakeStream(object):
|
class JupyterKernelFakeStream:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# implementation detail, aka cheapskate test
|
# implementation detail, aka cheapskate test
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
# -*- coding: utf-8 -*-
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
from click.core import ParameterSource
|
||||||
|
from click.decorators import pass_meta_key
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_context_objects(runner):
|
def test_ensure_context_objects(runner):
|
||||||
class Foo(object):
|
class Foo:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.title = "default"
|
self.title = "default"
|
||||||
|
|
||||||
|
@ -25,7 +30,7 @@ def test_ensure_context_objects(runner):
|
||||||
|
|
||||||
|
|
||||||
def test_get_context_objects(runner):
|
def test_get_context_objects(runner):
|
||||||
class Foo(object):
|
class Foo:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.title = "default"
|
self.title = "default"
|
||||||
|
|
||||||
|
@ -48,7 +53,7 @@ def test_get_context_objects(runner):
|
||||||
|
|
||||||
|
|
||||||
def test_get_context_objects_no_ensuring(runner):
|
def test_get_context_objects_no_ensuring(runner):
|
||||||
class Foo(object):
|
class Foo:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.title = "default"
|
self.title = "default"
|
||||||
|
|
||||||
|
@ -71,7 +76,7 @@ def test_get_context_objects_no_ensuring(runner):
|
||||||
|
|
||||||
|
|
||||||
def test_get_context_objects_missing(runner):
|
def test_get_context_objects_missing(runner):
|
||||||
class Foo(object):
|
class Foo:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
pass_foo = click.make_pass_decorator(Foo)
|
pass_foo = click.make_pass_decorator(Foo)
|
||||||
|
@ -129,7 +134,7 @@ def test_global_context_object(runner):
|
||||||
|
|
||||||
|
|
||||||
def test_context_meta(runner):
|
def test_context_meta(runner):
|
||||||
LANG_KEY = "{}.lang".format(__name__)
|
LANG_KEY = f"{__name__}.lang"
|
||||||
|
|
||||||
def set_language(value):
|
def set_language(value):
|
||||||
click.get_current_context().meta[LANG_KEY] = value
|
click.get_current_context().meta[LANG_KEY] = value
|
||||||
|
@ -147,6 +152,28 @@ def test_context_meta(runner):
|
||||||
runner.invoke(cli, [], catch_exceptions=False)
|
runner.invoke(cli, [], catch_exceptions=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_pass_meta_decorator(runner):
|
||||||
|
@click.group()
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx):
|
||||||
|
ctx.meta["value"] = "good"
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@pass_meta_key("value")
|
||||||
|
def show(value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["show"], standalone_mode=False)
|
||||||
|
assert result.return_value == "good"
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_pass_meta_decorator_doc():
|
||||||
|
pass_value = pass_meta_key("value")
|
||||||
|
assert "the 'value' key from :attr:`click.Context.meta`" in pass_value.__doc__
|
||||||
|
pass_value = pass_meta_key("value", doc_description="the test value")
|
||||||
|
assert "passes the test value" in pass_value.__doc__
|
||||||
|
|
||||||
|
|
||||||
def test_context_pushing():
|
def test_context_pushing():
|
||||||
rv = []
|
rv = []
|
||||||
|
|
||||||
|
@ -210,13 +237,29 @@ def test_close_before_pop(runner):
|
||||||
assert called == [True]
|
assert called == [True]
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_resource():
|
||||||
|
@contextmanager
|
||||||
|
def manager():
|
||||||
|
val = [1]
|
||||||
|
yield val
|
||||||
|
val[0] = 0
|
||||||
|
|
||||||
|
ctx = click.Context(click.Command("test"))
|
||||||
|
|
||||||
|
with ctx.scope():
|
||||||
|
rv = ctx.with_resource(manager())
|
||||||
|
assert rv[0] == 1
|
||||||
|
|
||||||
|
assert rv == [0]
|
||||||
|
|
||||||
|
|
||||||
def test_make_pass_decorator_args(runner):
|
def test_make_pass_decorator_args(runner):
|
||||||
"""
|
"""
|
||||||
Test to check that make_pass_decorator doesn't consume arguments based on
|
Test to check that make_pass_decorator doesn't consume arguments based on
|
||||||
invocation order.
|
invocation order.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Foo(object):
|
class Foo:
|
||||||
title = "foocmd"
|
title = "foocmd"
|
||||||
|
|
||||||
pass_foo = click.make_pass_decorator(Foo)
|
pass_foo = click.make_pass_decorator(Foo)
|
||||||
|
@ -247,6 +290,20 @@ def test_make_pass_decorator_args(runner):
|
||||||
assert result.output == "foocmd\n"
|
assert result.output == "foocmd\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_propagate_show_default_setting(runner):
|
||||||
|
"""A context's ``show_default`` setting defaults to the value from
|
||||||
|
the parent context.
|
||||||
|
"""
|
||||||
|
group = click.Group(
|
||||||
|
commands={
|
||||||
|
"sub": click.Command("sub", params=[click.Option(["-a"], default="a")]),
|
||||||
|
},
|
||||||
|
context_settings={"show_default": True},
|
||||||
|
)
|
||||||
|
result = runner.invoke(group, ["sub", "--help"])
|
||||||
|
assert "[default: a]" in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_exit_not_standalone():
|
def test_exit_not_standalone():
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
|
@ -261,3 +318,50 @@ def test_exit_not_standalone():
|
||||||
ctx.exit(0)
|
ctx.exit(0)
|
||||||
|
|
||||||
assert cli.main([], "test_exit_not_standalone", standalone_mode=False) == 0
|
assert cli.main([], "test_exit_not_standalone", standalone_mode=False) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("option_args", "invoke_args", "expect"),
|
||||||
|
[
|
||||||
|
pytest.param({}, {}, ParameterSource.DEFAULT, id="default"),
|
||||||
|
pytest.param(
|
||||||
|
{},
|
||||||
|
{"default_map": {"option": 1}},
|
||||||
|
ParameterSource.DEFAULT_MAP,
|
||||||
|
id="default_map",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{},
|
||||||
|
{"args": ["-o", "1"]},
|
||||||
|
ParameterSource.COMMANDLINE,
|
||||||
|
id="commandline short",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{},
|
||||||
|
{"args": ["--option", "1"]},
|
||||||
|
ParameterSource.COMMANDLINE,
|
||||||
|
id="commandline long",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{},
|
||||||
|
{"auto_envvar_prefix": "TEST", "env": {"TEST_OPTION": "1"}},
|
||||||
|
ParameterSource.ENVIRONMENT,
|
||||||
|
id="environment auto",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"envvar": "NAME"},
|
||||||
|
{"env": {"NAME": "1"}},
|
||||||
|
ParameterSource.ENVIRONMENT,
|
||||||
|
id="environment manual",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parameter_source(runner, option_args, invoke_args, expect):
|
||||||
|
@click.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.option("-o", "--option", default=1, **option_args)
|
||||||
|
def cli(ctx, option):
|
||||||
|
return ctx.get_parameter_source("option")
|
||||||
|
|
||||||
|
rv = runner.invoke(cli, standalone_mode=False, **invoke_args)
|
||||||
|
assert rv.return_value == expect
|
||||||
|
|
112
tests/test_custom_classes.py
Normal file
112
tests/test_custom_classes.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_context_class():
|
||||||
|
"""A command with a custom ``context_class`` should produce a
|
||||||
|
context using that type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class CustomContext(click.Context):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CustomCommand(click.Command):
|
||||||
|
context_class = CustomContext
|
||||||
|
|
||||||
|
command = CustomCommand("test")
|
||||||
|
context = command.make_context("test", [])
|
||||||
|
assert isinstance(context, CustomContext)
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_invoke_type(runner):
|
||||||
|
"""A command invoked from a custom context should have a new
|
||||||
|
context with the same type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class CustomContext(click.Context):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CustomCommand(click.Command):
|
||||||
|
context_class = CustomContext
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument("first_id", type=int)
|
||||||
|
@click.pass_context
|
||||||
|
def second(ctx, first_id):
|
||||||
|
assert isinstance(ctx, CustomContext)
|
||||||
|
assert id(ctx) != first_id
|
||||||
|
|
||||||
|
@click.command(cls=CustomCommand)
|
||||||
|
@click.pass_context
|
||||||
|
def first(ctx):
|
||||||
|
assert isinstance(ctx, CustomContext)
|
||||||
|
ctx.invoke(second, first_id=id(ctx))
|
||||||
|
|
||||||
|
assert not runner.invoke(first).exception
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_formatter_class():
|
||||||
|
"""A context with a custom ``formatter_class`` should format help
|
||||||
|
using that type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class CustomFormatter(click.HelpFormatter):
|
||||||
|
def write_heading(self, heading):
|
||||||
|
heading = click.style(heading, fg="yellow")
|
||||||
|
return super().write_heading(heading)
|
||||||
|
|
||||||
|
class CustomContext(click.Context):
|
||||||
|
formatter_class = CustomFormatter
|
||||||
|
|
||||||
|
context = CustomContext(
|
||||||
|
click.Command("test", params=[click.Option(["--value"])]), color=True
|
||||||
|
)
|
||||||
|
assert "\x1b[33mOptions\x1b[0m:" in context.get_help()
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_command_class(runner):
|
||||||
|
"""A group with a custom ``command_class`` should create subcommands
|
||||||
|
of that type by default.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class CustomCommand(click.Command):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CustomGroup(click.Group):
|
||||||
|
command_class = CustomCommand
|
||||||
|
|
||||||
|
group = CustomGroup()
|
||||||
|
subcommand = group.command()(lambda: None)
|
||||||
|
assert type(subcommand) is CustomCommand
|
||||||
|
subcommand = group.command(cls=click.Command)(lambda: None)
|
||||||
|
assert type(subcommand) is click.Command
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_group_class(runner):
|
||||||
|
"""A group with a custom ``group_class`` should create subgroups
|
||||||
|
of that type by default.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class CustomSubGroup(click.Group):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CustomGroup(click.Group):
|
||||||
|
group_class = CustomSubGroup
|
||||||
|
|
||||||
|
group = CustomGroup()
|
||||||
|
subgroup = group.group()(lambda: None)
|
||||||
|
assert type(subgroup) is CustomSubGroup
|
||||||
|
subgroup = group.command(cls=click.Group)(lambda: None)
|
||||||
|
assert type(subgroup) is click.Group
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_group_class_self(runner):
|
||||||
|
"""A group with ``group_class = type`` should create subgroups of
|
||||||
|
the same type as itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class CustomGroup(click.Group):
|
||||||
|
group_class = type
|
||||||
|
|
||||||
|
group = CustomGroup()
|
||||||
|
subgroup = group.group()(lambda: None)
|
||||||
|
assert type(subgroup) is CustomGroup
|
|
@ -6,7 +6,7 @@ def test_basic_defaults(runner):
|
||||||
@click.option("--foo", default=42, type=click.FLOAT)
|
@click.option("--foo", default=42, type=click.FLOAT)
|
||||||
def cli(foo):
|
def cli(foo):
|
||||||
assert type(foo) is float
|
assert type(foo) is float
|
||||||
click.echo("FOO:[{}]".format(foo))
|
click.echo(f"FOO:[{foo}]")
|
||||||
|
|
||||||
result = runner.invoke(cli, [])
|
result = runner.invoke(cli, [])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
|
@ -32,8 +32,8 @@ def test_nargs_plus_multiple(runner):
|
||||||
"--arg", default=((1, 2), (3, 4)), nargs=2, multiple=True, type=click.INT
|
"--arg", default=((1, 2), (3, 4)), nargs=2, multiple=True, type=click.INT
|
||||||
)
|
)
|
||||||
def cli(arg):
|
def cli(arg):
|
||||||
for item in arg:
|
for a, b in arg:
|
||||||
click.echo("<{0[0]:d}|{0[1]:d}>".format(item))
|
click.echo(f"<{a:d}|{b:d}>")
|
||||||
|
|
||||||
result = runner.invoke(cli, [])
|
result = runner.invoke(cli, [])
|
||||||
assert not result.exception
|
assert not result.exception
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue