New upstream version 8.0.2
This commit is contained in:
parent
4e974d1c0d
commit
46bc5bd117
|
@ -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
|
|
@ -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:
|
|
@ -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.
|
|
@ -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?
|
||||
-->
|
|
@ -0,0 +1,8 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
time: "08:00"
|
||||
open-pull-requests-limit: 99
|
|
@ -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.
|
|
@ -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
|
|
@ -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 }}
|
|
@ -0,0 +1,15 @@
|
|||
/.idea/
|
||||
/.vscode/
|
||||
/env/
|
||||
/venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.egg-info/
|
||||
/build/
|
||||
/dist/
|
||||
/.pytest_cache/
|
||||
/.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
/htmlcov/
|
||||
/docs/_build/
|
|
@ -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
|
|
@ -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
|
||||
|
||||
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
|
||||
-------------
|
||||
|
||||
|
@ -46,7 +328,7 @@ Released 2020-03-09
|
|||
:issue:`1277`, :pr:`1318`
|
||||
- Add ``no_args_is_help`` option to ``click.Command``, defaults to
|
||||
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`
|
||||
- Handle ``env MYPATH=''`` as though the option were not passed.
|
||||
:issue:`1196`
|
||||
|
@ -90,6 +372,8 @@ Released 2020-03-09
|
|||
- 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
|
||||
2.0. :pr:`1492`
|
||||
- Adjust error messages to standardize the types of quotes used so
|
||||
they match error messages from Python.
|
||||
|
||||
|
||||
Version 7.0
|
||||
|
@ -480,15 +764,16 @@ Version 3.0
|
|||
|
||||
Released 2014-08-12, codename "clonk clonk"
|
||||
|
||||
- Formatter now no longer attempts to accomodate for terminals smaller
|
||||
than 50 characters. If that happens it just assumes a minimal width.
|
||||
- Formatter now no longer attempts to accommodate for terminals
|
||||
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 better support for colors with pagers and ways to override the
|
||||
autodetection.
|
||||
- The CLI runner's result object now has a traceback attached.
|
||||
- Improved automatic short help detection to work better with dots
|
||||
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
|
||||
should catch situations where users wanted to created arguments
|
||||
instead of options.
|
||||
|
|
|
@ -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
|
|
@ -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 tox.ini
|
||||
include requirements/*.txt
|
||||
graft artwork
|
||||
graft docs
|
||||
prune docs/_build
|
||||
graft examples
|
||||
graft tests
|
||||
include src/click/py.typed
|
||||
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: https://pip.pypa.io/en/stable/quickstart/
|
||||
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||
|
||||
|
||||
A Simple Example
|
||||
|
@ -70,10 +70,11 @@ donate today`_.
|
|||
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
|
||||
- Changes: https://click.palletsprojects.com/changes/
|
||||
- PyPI Releases: https://pypi.org/project/click/
|
||||
- Source Code: https://github.com/pallets/click
|
||||
- Issue Tracker: https://github.com/pallets/click/issues
|
||||
- Website: https://palletsprojects.com/p/click
|
||||
- Twitter: https://twitter.com/PalletsTeam
|
||||
- Chat: https://discord.gg/pallets
|
||||
|
|
Binary file not shown.
|
@ -13,7 +13,7 @@ Command Aliases
|
|||
---------------
|
||||
|
||||
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
|
||||
``git commit``. Other tools also support auto-discovery for aliases by
|
||||
automatically shortening them.
|
||||
|
@ -35,7 +35,6 @@ it would accept ``pus`` as an alias (so long as it was unique):
|
|||
.. click:example::
|
||||
|
||||
class AliasedGroup(click.Group):
|
||||
|
||||
def get_command(self, ctx, cmd_name):
|
||||
rv = click.Group.get_command(self, ctx, cmd_name)
|
||||
if rv is not None:
|
||||
|
@ -46,7 +45,12 @@ it would accept ``pus`` as an alias (so long as it was unique):
|
|||
return None
|
||||
elif len(matches) == 1:
|
||||
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:
|
||||
|
||||
|
@ -92,7 +96,7 @@ it's good to know that the system works this way.
|
|||
@click.option('--url', callback=open_url)
|
||||
def cli(url, fp=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
|
||||
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)
|
||||
def cli(url):
|
||||
if url is not None:
|
||||
click.echo('%s: %s' % (url.url, url.fp.code))
|
||||
click.echo(f"{url.url}: {url.fp.code}")
|
||||
|
||||
|
||||
Token Normalization
|
||||
|
@ -140,7 +144,7 @@ function that converts the token to lowercase:
|
|||
@click.command(context_settings=CONTEXT_SETTINGS)
|
||||
@click.option('--name', default='Pete')
|
||||
def cli(name):
|
||||
click.echo('Name: %s' % name)
|
||||
click.echo(f"Name: {name}")
|
||||
|
||||
And how it works on the command line:
|
||||
|
||||
|
@ -171,7 +175,7 @@ Example:
|
|||
@cli.command()
|
||||
@click.option('--count', default=1)
|
||||
def test(count):
|
||||
click.echo('Count: %d' % count)
|
||||
click.echo(f'Count: {count}')
|
||||
|
||||
@cli.command()
|
||||
@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."""
|
||||
cmdline = ['echo', 'python', '-mtimeit'] + list(timeit_args)
|
||||
if verbose:
|
||||
click.echo('Invoking: %s' % ' '.join(cmdline))
|
||||
click.echo(f"Invoking: {' '.join(cmdline)}")
|
||||
call(cmdline)
|
||||
|
||||
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
|
||||
allowed to read from the context, but not to perform any modifications on
|
||||
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:: click.decorators.pass_meta_key
|
||||
|
||||
|
||||
Utilities
|
||||
---------
|
||||
|
||||
|
@ -108,6 +111,11 @@ Context
|
|||
|
||||
.. autofunction:: get_current_context
|
||||
|
||||
.. autoclass:: click.core.ParameterSource
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
|
@ -131,6 +139,10 @@ Types
|
|||
|
||||
.. autoclass:: IntRange
|
||||
|
||||
.. autoclass:: FloatRange
|
||||
|
||||
.. autoclass:: DateTime
|
||||
|
||||
.. autoclass:: Tuple
|
||||
|
||||
.. autoclass:: ParamType
|
||||
|
@ -169,6 +181,24 @@ Parsing
|
|||
.. autoclass:: OptionParser
|
||||
: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
|
||||
-------
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ Example:
|
|||
def copy(src, dst):
|
||||
"""Move file SRC to DST."""
|
||||
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:
|
||||
|
||||
|
|
|
@ -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.option('--debug/--no-debug', default=False)
|
||||
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!
|
||||
def sync():
|
||||
|
@ -96,7 +96,7 @@ script like this:
|
|||
@cli.command()
|
||||
@click.pass_context
|
||||
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__':
|
||||
cli(obj={})
|
||||
|
@ -166,7 +166,7 @@ Example:
|
|||
if ctx.invoked_subcommand is None:
|
||||
click.echo('I was invoked without subcommand')
|
||||
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()
|
||||
def sync():
|
||||
|
@ -202,7 +202,7 @@ A custom multi command just needs to implement a list and load method:
|
|||
def list_commands(self, ctx):
|
||||
rv = []
|
||||
for filename in os.listdir(plugin_folder):
|
||||
if filename.endswith('.py'):
|
||||
if filename.endswith('.py') and filename != '__init__.py':
|
||||
rv.append(filename[:-3])
|
||||
rv.sort()
|
||||
return rv
|
||||
|
@ -287,7 +287,7 @@ Multi Command Chaining
|
|||
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
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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):
|
||||
pass
|
||||
|
||||
@cli.resultcallback()
|
||||
@cli.result_callback()
|
||||
def process_pipeline(processors, input):
|
||||
iterator = (x.rstrip('\r\n') for x in input)
|
||||
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
|
||||
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
|
||||
that has a nice internal structure for the pipelines.
|
||||
|
||||
|
@ -466,7 +466,7 @@ Example usage:
|
|||
@cli.command()
|
||||
@click.option('--port', default=8000)
|
||||
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__':
|
||||
cli(default_map={
|
||||
|
@ -512,7 +512,7 @@ This example does the same as the previous example:
|
|||
@cli.command()
|
||||
@click.option('--port', default=8000)
|
||||
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__':
|
||||
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 ProjectLink
|
||||
|
||||
import click._compat
|
||||
|
||||
# compat until pallets-sphinx-themes is updated
|
||||
click._compat.text_type = str
|
||||
|
||||
# Project --------------------------------------------------------------
|
||||
|
||||
project = "Click"
|
||||
copyright = "2014 Pallets"
|
||||
author = "Pallets"
|
||||
release, version = get_version("Click", version_length=1)
|
||||
release, version = get_version("Click")
|
||||
|
||||
# General --------------------------------------------------------------
|
||||
|
||||
|
@ -17,7 +22,9 @@ extensions = [
|
|||
"sphinxcontrib.log_cabinet",
|
||||
"pallets_sphinx_themes",
|
||||
"sphinx_issues",
|
||||
"sphinx_tabs.tabs",
|
||||
]
|
||||
autodoc_typehints = "description"
|
||||
intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
|
||||
issues_github_path = "pallets/click"
|
||||
|
||||
|
@ -27,18 +34,20 @@ html_theme = "click"
|
|||
html_theme_options = {"index_sidebar_logo": False}
|
||||
html_context = {
|
||||
"project_links": [
|
||||
ProjectLink("Donate to Pallets", "https://palletsprojects.com/donate"),
|
||||
ProjectLink("Click Website", "https://palletsprojects.com/p/click/"),
|
||||
ProjectLink("PyPI releases", "https://pypi.org/project/click/"),
|
||||
ProjectLink("Donate", "https://palletsprojects.com/donate"),
|
||||
ProjectLink("PyPI Releases", "https://pypi.org/project/click/"),
|
||||
ProjectLink("Source Code", "https://github.com/pallets/click/"),
|
||||
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 = {
|
||||
"index": ["project.html", "localtoc.html", "searchbox.html"],
|
||||
"**": ["localtoc.html", "relations.html", "searchbox.html"],
|
||||
"index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.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_favicon = "_static/click-icon.png"
|
||||
html_logo = "_static/click-logo-sidebar.png"
|
||||
|
|
|
@ -24,7 +24,7 @@ Simple example:
|
|||
def hello(count, name):
|
||||
"""This script prints hello NAME COUNT times."""
|
||||
for x in range(count):
|
||||
click.echo('Hello %s!' % name)
|
||||
click.echo(f"Hello {name}!")
|
||||
|
||||
And what it looks like:
|
||||
|
||||
|
@ -173,7 +173,7 @@ desired. This can be customized at all levels:
|
|||
def hello(count, name):
|
||||
"""This script prints hello <name> <int> times."""
|
||||
for x in range(count):
|
||||
click.echo('Hello %s!' % name)
|
||||
click.echo(f"Hello {name}!")
|
||||
|
||||
Example:
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ What does it look like? Here is an example of a simple Click program:
|
|||
def hello(count, name):
|
||||
"""Simple program that greets NAME for a total of COUNT times."""
|
||||
for x in range(count):
|
||||
click.echo('Hello %s!' % name)
|
||||
click.echo(f"Hello {name}!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
hello()
|
||||
|
@ -79,9 +79,9 @@ usage patterns.
|
|||
advanced
|
||||
testing
|
||||
utils
|
||||
bashcomplete
|
||||
shell-completion
|
||||
exceptions
|
||||
python3
|
||||
unicode-support
|
||||
wincmd
|
||||
|
||||
API Reference
|
||||
|
@ -102,6 +102,6 @@ Miscellaneous Pages
|
|||
:maxdepth: 2
|
||||
|
||||
contrib
|
||||
changelog
|
||||
upgrading
|
||||
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('--to', '-t')
|
||||
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:
|
||||
|
||||
|
@ -121,7 +121,8 @@ the ``nargs`` parameter. The values are then stored as a tuple.
|
|||
@click.command()
|
||||
@click.option('--pos', nargs=2, type=float)
|
||||
def findme(pos):
|
||||
click.echo('%s / %s' % pos)
|
||||
a, b = pos
|
||||
click.echo(f"{a} / {b}")
|
||||
|
||||
And on the command line:
|
||||
|
||||
|
@ -146,7 +147,8 @@ the tuple. For this you can directly specify a tuple as type:
|
|||
@click.command()
|
||||
@click.option('--item', type=(str, int))
|
||||
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:
|
||||
|
||||
|
@ -163,7 +165,8 @@ used. The above example is thus equivalent to this:
|
|||
@click.command()
|
||||
@click.option('--item', nargs=2, type=click.Tuple([str, int]))
|
||||
def putitem(item):
|
||||
click.echo('name=%s id=%d' % item)
|
||||
name, id = item
|
||||
click.echo(f"name={name} id={id}")
|
||||
|
||||
.. _multiple-options:
|
||||
|
||||
|
@ -212,7 +215,7 @@ for instance:
|
|||
@click.command()
|
||||
@click.option('-v', '--verbose', count=True)
|
||||
def log(verbose):
|
||||
click.echo('Verbosity: %s' % verbose)
|
||||
click.echo(f"Verbosity: {verbose}")
|
||||
|
||||
And on the command line:
|
||||
|
||||
|
@ -250,6 +253,7 @@ And on the command line:
|
|||
|
||||
invoke(info, args=['--shout'])
|
||||
invoke(info, args=['--no-shout'])
|
||||
invoke(info)
|
||||
|
||||
If you really don't want an off-switch, you can just define one and
|
||||
manually inform Click that something is a flag:
|
||||
|
@ -271,6 +275,7 @@ And on the command line:
|
|||
.. click:run::
|
||||
|
||||
invoke(info, args=['--shout'])
|
||||
invoke(info)
|
||||
|
||||
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
|
||||
|
@ -281,7 +286,7 @@ can alternatively split the parameters through ``;`` instead:
|
|||
@click.command()
|
||||
@click.option('/debug;/no-debug')
|
||||
def log(debug):
|
||||
click.echo('debug=%s' % debug)
|
||||
click.echo(f"debug={debug}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
log()
|
||||
|
@ -402,7 +407,7 @@ Example:
|
|||
@click.command()
|
||||
@click.option('--name', prompt=True)
|
||||
def hello(name):
|
||||
click.echo('Hello %s!' % name)
|
||||
click.echo(f"Hello {name}!")
|
||||
|
||||
And what it looks like:
|
||||
|
||||
|
@ -419,7 +424,7 @@ a different one:
|
|||
@click.command()
|
||||
@click.option('--name', prompt='Your name please')
|
||||
def hello(name):
|
||||
click.echo('Hello %s!' % name)
|
||||
click.echo(f"Hello {name}!")
|
||||
|
||||
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
|
||||
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
|
||||
----------------
|
||||
|
@ -439,27 +448,30 @@ useful for password input:
|
|||
|
||||
.. click:example::
|
||||
|
||||
@click.command()
|
||||
@click.option('--password', prompt=True, hide_input=True,
|
||||
confirmation_prompt=True)
|
||||
def encrypt(password):
|
||||
click.echo('Encrypting password to %s' % password.encode('rot13'))
|
||||
import codecs
|
||||
|
||||
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::
|
||||
|
||||
invoke(encrypt, input=['secret', 'secret'])
|
||||
invoke(encode, input=['secret', 'secret'])
|
||||
|
||||
Because this combination of parameters is quite common, this can also be
|
||||
replaced with the :func:`password_option` decorator:
|
||||
|
||||
.. click:example::
|
||||
.. code-block:: python
|
||||
|
||||
@click.command()
|
||||
@click.password_option()
|
||||
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
|
||||
----------------------------
|
||||
|
@ -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
|
||||
from the environment:
|
||||
|
||||
.. click:example::
|
||||
.. code-block:: python
|
||||
|
||||
import os
|
||||
|
||||
@click.command()
|
||||
@click.option('--username', prompt=True,
|
||||
default=lambda: os.environ.get('USER', ''))
|
||||
@click.option(
|
||||
"--username", prompt=True,
|
||||
default=lambda: os.environ.get("USER", "")
|
||||
)
|
||||
def hello(username):
|
||||
print("Hello,", username)
|
||||
click.echo(f"Hello, {username}!")
|
||||
|
||||
To describe what the default value will be, set it in ``show_default``.
|
||||
|
||||
.. click:example::
|
||||
|
||||
import os
|
||||
|
||||
@click.command()
|
||||
@click.option('--username', prompt=True,
|
||||
default=lambda: os.environ.get('USER', ''),
|
||||
show_default='current user')
|
||||
@click.option(
|
||||
"--username", prompt=True,
|
||||
default=lambda: os.environ.get("USER", ""),
|
||||
show_default="current user"
|
||||
)
|
||||
def hello(username):
|
||||
print("Hello,", username)
|
||||
click.echo(f"Hello, {username}!")
|
||||
|
||||
.. click:run::
|
||||
|
||||
invoke(hello, args=['--help'])
|
||||
invoke(hello, args=["--help"])
|
||||
|
||||
|
||||
Callbacks and Eager Options
|
||||
---------------------------
|
||||
|
@ -625,7 +646,7 @@ Example usage:
|
|||
@click.command()
|
||||
@click.option('--username')
|
||||
def greet(username):
|
||||
click.echo('Hello %s!' % username)
|
||||
click.echo(f'Hello {username}!')
|
||||
|
||||
if __name__ == '__main__':
|
||||
greet(auto_envvar_prefix='GREETER')
|
||||
|
@ -650,12 +671,12 @@ Example:
|
|||
@click.group()
|
||||
@click.option('--debug/--no-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()
|
||||
@click.option('--username')
|
||||
def greet(username):
|
||||
click.echo('Hello %s!' % username)
|
||||
click.echo(f"Hello {username}!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli(auto_envvar_prefix='GREETER')
|
||||
|
@ -677,7 +698,7 @@ Example usage:
|
|||
@click.command()
|
||||
@click.option('--username', envvar='USERNAME')
|
||||
def greet(username):
|
||||
click.echo('Hello %s!' % username)
|
||||
click.echo(f"Hello {username}!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
greet()
|
||||
|
@ -726,7 +747,7 @@ And from the command line:
|
|||
.. click:run::
|
||||
|
||||
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
|
||||
-----------------------
|
||||
|
@ -742,7 +763,7 @@ POSIX semantics. However in certain situations this can be useful:
|
|||
@click.command()
|
||||
@click.option('+w/-w')
|
||||
def chmod(w):
|
||||
click.echo('writable=%s' % w)
|
||||
click.echo(f"writable={w}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
chmod()
|
||||
|
@ -762,7 +783,7 @@ boolean flag you need to separate it with ``;`` instead of ``/``:
|
|||
@click.command()
|
||||
@click.option('/debug;/no-debug')
|
||||
def log(debug):
|
||||
click.echo('debug=%s' % debug)
|
||||
click.echo(f"debug={debug}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
log()
|
||||
|
@ -772,39 +793,34 @@ boolean flag you need to separate it with ``;`` instead of ``/``:
|
|||
Range Options
|
||||
-------------
|
||||
|
||||
A special mention should go to the :class:`IntRange` type, which works very
|
||||
similarly to the :data:`INT` type, but restricts the value to fall into a
|
||||
specific range (inclusive on both edges). It has two modes:
|
||||
The :class:`IntRange` type extends the :data:`INT` type to ensure the
|
||||
value is contained in the given range. The :class:`FloatRange` type does
|
||||
the same for :data:`FLOAT`.
|
||||
|
||||
- the default mode (non-clamping mode) where a value that falls outside
|
||||
of the range will cause an error.
|
||||
- an optional clamping mode where a value that falls outside of the
|
||||
range will be clamped. This means that a range of ``0-5`` would
|
||||
return ``5`` for the value ``10`` or ``0`` for the value ``-1`` (for
|
||||
example).
|
||||
If ``min`` or ``max`` is omitted, that side is *unbounded*. Any value in
|
||||
that direction is accepted. By default, both bounds are *closed*, which
|
||||
means the boundary value is included in the accepted range. ``min_open``
|
||||
and ``max_open`` can be used to exclude that boundary from the range.
|
||||
|
||||
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.command()
|
||||
@click.option('--count', type=click.IntRange(0, 20, clamp=True))
|
||||
@click.option('--digit', type=click.IntRange(0, 10))
|
||||
@click.option("--count", type=click.IntRange(0, 20, clamp=True))
|
||||
@click.option("--digit", type=click.IntRange(0, 9))
|
||||
def repeat(count, digit):
|
||||
click.echo(str(digit) * count)
|
||||
|
||||
if __name__ == '__main__':
|
||||
repeat()
|
||||
|
||||
And from the command line:
|
||||
|
||||
.. click:run::
|
||||
|
||||
invoke(repeat, args=['--count=1000', '--digit=5'])
|
||||
invoke(repeat, args=['--count=1000', '--digit=12'])
|
||||
invoke(repeat, args=['--count=100', '--digit=5'])
|
||||
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
|
||||
------------------------
|
||||
|
@ -812,37 +828,87 @@ Callbacks for Validation
|
|||
.. versionchanged:: 2.0
|
||||
|
||||
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
|
||||
raise errors if the validation does not work.
|
||||
parameter callbacks. These callbacks can both modify values as well as
|
||||
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
|
||||
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
|
||||
also contain the parameter name.
|
||||
|
||||
Example:
|
||||
|
||||
.. click:example::
|
||||
|
||||
def validate_rolls(ctx, param, value):
|
||||
if isinstance(value, tuple):
|
||||
return value
|
||||
|
||||
try:
|
||||
rolls, dice = map(int, value.split('d', 2))
|
||||
return (dice, rolls)
|
||||
rolls, _, dice = value.partition("d")
|
||||
return int(dice), int(rolls)
|
||||
except ValueError:
|
||||
raise click.BadParameter('rolls need to be in format NdM')
|
||||
raise click.BadParameter("format must be 'NdM'")
|
||||
|
||||
@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):
|
||||
click.echo('Rolling a %d-sided dice %d time(s)' % rolls)
|
||||
|
||||
if __name__ == '__main__':
|
||||
roll()
|
||||
|
||||
And what it looks like:
|
||||
sides, times = rolls
|
||||
click.echo(f"Rolling a {sides}-sided dice {times} time(s)")
|
||||
|
||||
.. click:run::
|
||||
|
||||
invoke(roll, args=['--rolls=42'])
|
||||
invoke(roll, args=["--rolls=42"])
|
||||
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.
|
||||
|
||||
``bool`` / :data:`click.BOOL`:
|
||||
A parameter that accepts boolean values. This is automatically used
|
||||
for boolean flags. If used with string values ``1``, ``yes``, ``y``, ``t``
|
||||
and ``true`` convert to `True` and ``0``, ``no``, ``n``, ``f`` and ``false``
|
||||
convert to `False`.
|
||||
A parameter that accepts boolean values. This is automatically used
|
||||
for boolean flags. The string values "1", "true", "t", "yes", "y",
|
||||
and "on" convert to ``True``. "0", "false", "f", "no", "n", and
|
||||
"off" convert to ``False``.
|
||||
|
||||
:data:`click.UUID`:
|
||||
A parameter that accepts UUID values. This is not automatically
|
||||
|
@ -118,19 +118,15 @@ integers.
|
|||
name = "integer"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
|
||||
try:
|
||||
if value[:2].lower() == "0x":
|
||||
return int(value[2:], 16)
|
||||
elif value[:1] == "0":
|
||||
return int(value, 8)
|
||||
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:
|
||||
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
|
||||
``param`` and ``ctx`` arguments may be ``None`` in some cases such as
|
||||
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
|
||||
works.
|
||||
|
||||
If you are on Mac OS X or Linux, chances are that one of the following two
|
||||
commands will work for you::
|
||||
|
||||
$ sudo easy_install virtualenv
|
||||
|
||||
or even better::
|
||||
If you are on Mac OS X or Linux::
|
||||
|
||||
$ 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
|
||||
virtualenv::
|
||||
|
||||
$ pip install Click
|
||||
$ pip install click
|
||||
|
||||
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:
|
||||
|
||||
* ``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
|
||||
<https://github.com/pallets/click/tree/master/examples/naval>`_
|
||||
<https://github.com/pallets/click/tree/main/examples/naval>`_
|
||||
* ``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
|
||||
<https://github.com/pallets/click/tree/master/examples/repo>`_
|
||||
<https://github.com/pallets/click/tree/main/examples/repo>`_
|
||||
* ``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
|
||||
<https://github.com/pallets/click/tree/master/examples/validation>`_
|
||||
* ``colors``: `Colorama ANSI color support
|
||||
<https://github.com/pallets/click/tree/master/examples/colors>`_
|
||||
<https://github.com/pallets/click/tree/main/examples/validation>`_
|
||||
* ``colors``: `Color support demo
|
||||
<https://github.com/pallets/click/tree/main/examples/colors>`_
|
||||
* ``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
|
||||
<https://github.com/pallets/click/tree/master/examples/imagepipe>`_
|
||||
<https://github.com/pallets/click/tree/main/examples/imagepipe>`_
|
||||
|
||||
Basic Concepts - Creating a Command
|
||||
-----------------------------------
|
||||
|
@ -162,7 +157,7 @@ Echoing
|
|||
|
||||
Why does this example use :func:`echo` instead of the regular
|
||||
: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
|
||||
functional at least on a basic level even if everything is completely
|
||||
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
|
||||
:exc:`UnicodeError`.
|
||||
|
||||
As an added benefit, starting with Click 2.0, the echo function also
|
||||
has good support for ANSI colors. It will automatically strip ANSI codes
|
||||
if the output stream is a file and if colorama is supported, ANSI colors
|
||||
will also work on Windows. Note that in Python 2, the :func:`echo` function
|
||||
does not parse color code information from bytearrays. See :ref:`ansi-colors`
|
||||
for more information.
|
||||
The echo function also supports color and other styles in output. It
|
||||
will automatically remove styles if the output stream is a file. On
|
||||
Windows, colorama is automatically installed and used. See
|
||||
:ref:`ansi-colors`.
|
||||
|
||||
If you don't need this, you can also use the `print()` construct /
|
||||
function.
|
||||
|
@ -233,6 +226,30 @@ other invocations::
|
|||
if __name__ == '__main__':
|
||||
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
|
||||
-----------------
|
||||
|
||||
|
@ -245,7 +262,7 @@ To add parameters, use the :func:`option` and :func:`argument` decorators:
|
|||
@click.argument('name')
|
||||
def hello(count, name):
|
||||
for x in range(count):
|
||||
click.echo('Hello %s!' % name)
|
||||
click.echo(f"Hello {name}!")
|
||||
|
||||
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
|
||||
Python package and a ``setup.py`` file.
|
||||
|
||||
Imagine this directory structure::
|
||||
Imagine this directory structure:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
yourscript.py
|
||||
setup.py
|
||||
|
@ -59,21 +61,24 @@ Contents of ``yourscript.py``:
|
|||
"""Example script."""
|
||||
click.echo('Hello World!')
|
||||
|
||||
Contents of ``setup.py``::
|
||||
Contents of ``setup.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='yourscript',
|
||||
version='0.1',
|
||||
version='0.1.0',
|
||||
py_modules=['yourscript'],
|
||||
install_requires=[
|
||||
'Click',
|
||||
],
|
||||
entry_points='''
|
||||
[console_scripts]
|
||||
yourscript=yourscript:cli
|
||||
''',
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'yourscript = yourscript:cli',
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
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
|
||||
package::
|
||||
package:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ virtualenv venv
|
||||
$ . 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
|
||||
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/
|
||||
__init__.py
|
||||
main.py
|
||||
utils.py
|
||||
scripts/
|
||||
.. code-block:: text
|
||||
|
||||
project/
|
||||
yourpackage/
|
||||
__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
|
||||
can use ``packages`` and the automatic package finding support of
|
||||
setuptools. In addition to that it's also recommended to include other
|
||||
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
|
||||
|
||||
setup(
|
||||
name='yourpackage',
|
||||
version='0.1',
|
||||
version='0.1.0',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
'Click',
|
||||
],
|
||||
entry_points='''
|
||||
[console_scripts]
|
||||
yourscript=yourpackage.scripts.yourscript:cli
|
||||
''',
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'yourscript = yourpackage.scripts.yourscript:cli',
|
||||
],
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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.argument('name')
|
||||
def hello(name):
|
||||
click.echo('Hello %s!' % name)
|
||||
click.echo(f'Hello {name}!')
|
||||
|
||||
.. code-block:: python
|
||||
:caption: test_hello.py
|
||||
|
@ -54,7 +54,7 @@ For subcommand testing, a subcommand name must be specified in the `args` parame
|
|||
@click.group()
|
||||
@click.option('--debug/--no-debug', default=False)
|
||||
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()
|
||||
def sync():
|
||||
|
@ -112,6 +112,19 @@ current working directory to a new, empty folder.
|
|||
assert result.exit_code == 0
|
||||
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
|
||||
-------------
|
||||
|
||||
|
@ -126,7 +139,7 @@ stream (stdin). This is very useful for testing prompts, for instance:
|
|||
@click.command()
|
||||
@click.option('--foo', prompt=True)
|
||||
def prompt(foo):
|
||||
click.echo('foo=%s' % foo)
|
||||
click.echo(f"foo={foo}")
|
||||
|
||||
.. code-block:: python
|
||||
:caption: test_prompt.py
|
||||
|
|
|
@ -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
|
||||
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
|
||||
:meth:`Context.resultcallback`.
|
||||
:meth:`Context.result_callback`.
|
||||
|
||||
|
||||
.. _upgrade-to-2.0:
|
||||
|
|
|
@ -13,9 +13,7 @@ Printing to Stdout
|
|||
|
||||
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
|
||||
that it works the same in Python 2 and 3, it intelligently detects
|
||||
misconfigured output streams, and it will never fail (except in Python 3; for
|
||||
more information see :ref:`python3-limitations`).
|
||||
that it works the same in many different terminal environments.
|
||||
|
||||
Example::
|
||||
|
||||
|
@ -23,10 +21,8 @@ Example::
|
|||
|
||||
click.echo('Hello World!')
|
||||
|
||||
Most importantly, it can print both Unicode and binary data, unlike the
|
||||
built-in ``print`` function in Python 3, which cannot output any bytes. It
|
||||
will, however, emit a trailing newline by default, which needs to be
|
||||
suppressed by passing ``nl=False``::
|
||||
It can output both text and binary data. It will emit a trailing newline
|
||||
by default, which needs to be suppressed by passing ``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
|
||||
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
|
||||
with regards to which characters can be displayed). This functionality is
|
||||
new in Click 6.0.
|
||||
with regards to which characters can be displayed).
|
||||
|
||||
.. 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
|
||||
:doc:`wincmd`.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
Starting with Click 3.0 you can also easily print to standard error by
|
||||
passing ``err=True``::
|
||||
You can also easily print to standard error by passing ``err=True``::
|
||||
|
||||
click.echo('Hello World!', err=True)
|
||||
|
||||
|
@ -58,11 +52,8 @@ ANSI Colors
|
|||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Starting with Click 2.0, the :func:`echo` function gained extra
|
||||
functionality to deal with ANSI colors and styles. Note that on Windows,
|
||||
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.
|
||||
The :func:`echo` function supports ANSI colors and styles. On Windows
|
||||
this uses `colorama`_.
|
||||
|
||||
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
|
||||
operating systems.
|
||||
|
||||
Note for `colorama` support: Click will automatically detect when `colorama`
|
||||
is available and use it. Do *not* call ``colorama.init()``!
|
||||
|
||||
To install `colorama`, run this command::
|
||||
|
||||
$ pip install colorama
|
||||
On Windows, Click uses colorama without calling ``colorama.init()``. You
|
||||
can still call that in your code, but it's not required for Click.
|
||||
|
||||
For styling a string, the :func:`style` function can be used::
|
||||
|
||||
|
@ -112,15 +99,14 @@ Example:
|
|||
|
||||
@click.command()
|
||||
def less():
|
||||
click.echo_via_pager('\n'.join('Line %d' % idx
|
||||
for idx in range(200)))
|
||||
click.echo_via_pager("\n".join(f"Line {idx}" 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:
|
||||
|
||||
.. click:example::
|
||||
def _generate_output():
|
||||
for idx in range(50000):
|
||||
yield "Line %d\n" % idx
|
||||
yield f"Line {idx}\n"
|
||||
|
||||
@click.command()
|
||||
def less():
|
||||
|
@ -253,9 +239,7 @@ Printing Filenames
|
|||
------------------
|
||||
|
||||
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
|
||||
write the bytes to stdout with the ``print`` function, but in Python 3, you
|
||||
will always need to operate in Unicode.
|
||||
tricky.
|
||||
|
||||
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
|
||||
|
@ -264,7 +248,7 @@ context of a full Unicode string.
|
|||
|
||||
Example::
|
||||
|
||||
click.echo('Path: %s' % click.format_filename(b'foo.txt'))
|
||||
click.echo(f"Path: {click.format_filename(b'foo.txt')}")
|
||||
|
||||
|
||||
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.
|
||||
|
||||
The end result is that these functions will always return a functional
|
||||
stream object (except in very odd cases in Python 3; see
|
||||
:ref:`python3-limitations`).
|
||||
stream object (except in very odd cases; see :doc:`/unicode-support`).
|
||||
|
||||
Example::
|
||||
|
||||
|
@ -349,20 +332,25 @@ Example usage::
|
|||
rv = {}
|
||||
for section in parser.sections():
|
||||
for key, value in parser.items(section):
|
||||
rv['%s.%s' % (section, key)] = value
|
||||
rv[f"{section}.{key}"] = value
|
||||
return rv
|
||||
|
||||
|
||||
Showing Progress Bars
|
||||
---------------------
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
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
|
||||
will take. Click supports simple progress bar rendering for that through
|
||||
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
|
||||
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::
|
||||
|
@ -396,7 +384,7 @@ loop. So code like this will render correctly::
|
|||
|
||||
with click.progressbar([1, 2, 3]) as bar:
|
||||
for x in bar:
|
||||
print('sleep({})...'.format(x))
|
||||
print(f"sleep({x})...")
|
||||
time.sleep(x)
|
||||
|
||||
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
|
||||
line utility for Python out there which ticks the following boxes:
|
||||
|
||||
* is lazily composable without restrictions
|
||||
* supports implementation of Unix/POSIX command line conventions
|
||||
* supports loading values from environment variables out of the box
|
||||
* support for prompting of custom values
|
||||
* is fully nestable and composable
|
||||
* works the same in Python 2 and 3
|
||||
* supports file handling out of the box
|
||||
* comes with useful common helpers (getting terminal dimensions,
|
||||
* Is lazily composable without restrictions.
|
||||
* Supports implementation of Unix/POSIX command line conventions.
|
||||
* Supports loading values from environment variables out of the box.
|
||||
* Support for prompting of custom values.
|
||||
* Is fully nestable and composable.
|
||||
* Supports file handling out of the box.
|
||||
* Comes with useful common helpers (getting terminal dimensions,
|
||||
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``
|
||||
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
|
||||
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:
|
||||
|
||||
* 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
|
||||
incomplete command lines; the behaviour becomes unpredictable
|
||||
without full knowledge of a command line. This goes against Click's
|
||||
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
|
||||
parsing.
|
||||
|
||||
|
@ -135,7 +134,7 @@ Why No Auto Correction?
|
|||
-----------------------
|
||||
|
||||
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.
|
||||
If people start relying on automatically modified parameters and someone
|
||||
adds a new parameter in the future, the script might stop working. These
|
||||
|
|
|
@ -3,11 +3,7 @@ Windows Console Notes
|
|||
|
||||
.. versionadded:: 6.0
|
||||
|
||||
Until Click 6.0 there are various bugs and limitations with using Click on
|
||||
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
|
||||
Click emulates output streams on Windows to support unicode to the
|
||||
Windows console through separate APIs and we perform different decoding of
|
||||
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
|
||||
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
|
||||
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`.
|
||||
|
||||
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
|
||||
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
|
||||
|
@ -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
|
||||
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
|
||||
well as ``click.get_text_stream``.
|
||||
* Depending on if unicode values or byte strings are passed the control
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import configparser
|
||||
import os
|
||||
|
||||
import click
|
||||
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
|
||||
|
||||
class Config(object):
|
||||
class Config:
|
||||
"""The config in this example only holds aliases."""
|
||||
|
||||
def __init__(self):
|
||||
|
@ -53,7 +49,7 @@ class AliasedGroup(click.Group):
|
|||
# will create the config object is missing.
|
||||
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:
|
||||
actual_cmd = cfg.aliases[cmd_name]
|
||||
return click.Group.get_command(self, ctx, actual_cmd)
|
||||
|
@ -69,7 +65,12 @@ class AliasedGroup(click.Group):
|
|||
return None
|
||||
elif len(matches) == 1:
|
||||
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):
|
||||
|
@ -125,7 +126,7 @@ def commit():
|
|||
@pass_config
|
||||
def status(config):
|
||||
"""Shows the status."""
|
||||
click.echo("Status for {}".format(config.path))
|
||||
click.echo(f"Status for {config.path}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
@ -139,4 +140,4 @@ def alias(config, alias_, cmd, config_file):
|
|||
"""Adds an alias to the specified configuration file."""
|
||||
config.add_alias(alias_, cmd)
|
||||
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
|
||||
colorize text.
|
||||
|
||||
For this to work on Windows, colorama is required.
|
||||
Uses colorama on Windows.
|
||||
|
||||
Usage:
|
||||
|
||||
|
|
|
@ -23,22 +23,17 @@ all_colors = (
|
|||
|
||||
@click.command()
|
||||
def cli():
|
||||
"""This script prints some colors. If colorama is installed this will
|
||||
also work on Windows. It will also automatically remove all ANSI
|
||||
styles if data is piped into a file.
|
||||
"""This script prints some colors. It will also automatically remove
|
||||
all ANSI styles if data is piped into a file.
|
||||
|
||||
Give it a try!
|
||||
"""
|
||||
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:
|
||||
click.echo(
|
||||
click.style("I am colored {} and bold".format(color), fg=color, bold=True)
|
||||
)
|
||||
click.echo(click.style(f"I am colored {color} and bold", fg=color, bold=True))
|
||||
for color in all_colors:
|
||||
click.echo(
|
||||
click.style("I am reverse colored {}".format(color), fg=color, reverse=True)
|
||||
)
|
||||
click.echo(click.style(f"I am reverse colored {color}", fg=color, reverse=True))
|
||||
|
||||
click.echo(click.style("I am blinking", blink=True))
|
||||
click.echo(click.style("I am underlined", underline=True))
|
||||
|
|
|
@ -5,11 +5,7 @@ setup(
|
|||
version="1.0",
|
||||
py_modules=["colors"],
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
"click",
|
||||
# Colorama is only required for Windows.
|
||||
"colorama",
|
||||
],
|
||||
install_requires=["click"],
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
colors=colors:cli
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
|
||||
setup(
|
||||
name="click-example-bashcompletion",
|
||||
name="click-example-completion",
|
||||
version="1.0",
|
||||
py_modules=["bashcompletion"],
|
||||
py_modules=["completion"],
|
||||
include_package_data=True,
|
||||
install_requires=["click"],
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
bashcompletion=bashcompletion:cli
|
||||
completion=completion:cli
|
||||
""",
|
||||
)
|
|
@ -7,7 +7,7 @@ import click
|
|||
CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX")
|
||||
|
||||
|
||||
class Environment(object):
|
||||
class Environment:
|
||||
def __init__(self):
|
||||
self.verbose = False
|
||||
self.home = os.getcwd()
|
||||
|
@ -39,11 +39,7 @@ class ComplexCLI(click.MultiCommand):
|
|||
|
||||
def get_command(self, ctx, name):
|
||||
try:
|
||||
if sys.version_info[0] == 2:
|
||||
name = name.encode("ascii", "replace")
|
||||
mod = __import__(
|
||||
"complex.commands.cmd_{}".format(name), None, None, ["cli"]
|
||||
)
|
||||
mod = __import__(f"complex.commands.cmd_{name}", None, None, ["cli"])
|
||||
except ImportError:
|
||||
return
|
||||
return mod.cli
|
||||
|
|
|
@ -10,4 +10,4 @@ def cli(ctx, path):
|
|||
"""Initializes a repository."""
|
||||
if path is None:
|
||||
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):
|
||||
"""This result callback is invoked with an iterable of all the chained
|
||||
subcommands. As in this example each subcommand returns a function
|
||||
|
@ -60,10 +60,8 @@ def generator(f):
|
|||
|
||||
@processor
|
||||
def new_func(stream, *args, **kwargs):
|
||||
for item in stream:
|
||||
yield item
|
||||
for item in f(*args, **kwargs):
|
||||
yield item
|
||||
yield from stream
|
||||
yield from f(*args, **kwargs)
|
||||
|
||||
return update_wrapper(new_func, f)
|
||||
|
||||
|
@ -89,7 +87,7 @@ def open_cmd(images):
|
|||
"""
|
||||
for image in images:
|
||||
try:
|
||||
click.echo("Opening '{}'".format(image))
|
||||
click.echo(f"Opening '{image}'")
|
||||
if image == "-":
|
||||
img = Image.open(click.get_binary_stdin())
|
||||
img.filename = "-"
|
||||
|
@ -97,7 +95,7 @@ def open_cmd(images):
|
|||
img = Image.open(image)
|
||||
yield img
|
||||
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")
|
||||
|
@ -114,12 +112,10 @@ def save_cmd(images, filename):
|
|||
for idx, image in enumerate(images):
|
||||
try:
|
||||
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)
|
||||
except Exception as e:
|
||||
click.echo(
|
||||
"Could not save image '{}': {}".format(image.filename, e), err=True
|
||||
)
|
||||
click.echo(f"Could not save image '{image.filename}': {e}", err=True)
|
||||
|
||||
|
||||
@cli.command("display")
|
||||
|
@ -127,7 +123,7 @@ def save_cmd(images, filename):
|
|||
def display_cmd(images):
|
||||
"""Opens all images in an image viewer."""
|
||||
for image in images:
|
||||
click.echo("Displaying '{}'".format(image.filename))
|
||||
click.echo(f"Displaying '{image.filename}'")
|
||||
image.show()
|
||||
yield image
|
||||
|
||||
|
@ -142,7 +138,7 @@ def resize_cmd(images, width, height):
|
|||
"""
|
||||
for image in images:
|
||||
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))
|
||||
yield image
|
||||
|
||||
|
@ -160,7 +156,7 @@ def crop_cmd(images, border):
|
|||
if border is not None:
|
||||
for idx, val in enumerate(box):
|
||||
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)
|
||||
else:
|
||||
yield image
|
||||
|
@ -176,7 +172,7 @@ def convert_rotation(ctx, param, value):
|
|||
return (Image.ROTATE_180, 180)
|
||||
if value in ("-90", "270", "l", "left"):
|
||||
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):
|
||||
|
@ -187,7 +183,7 @@ def convert_flip(ctx, param, value):
|
|||
return (Image.FLIP_LEFT_RIGHT, "left to right")
|
||||
if value in ("tb", "topbottom", "upsidedown", "ud"):
|
||||
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")
|
||||
|
@ -201,11 +197,11 @@ def transpose_cmd(images, rotate, flip):
|
|||
for image in images:
|
||||
if rotate is not None:
|
||||
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)
|
||||
if flip is not None:
|
||||
mode, direction = flip
|
||||
click.echo("Flip '{}' {}".format(image.filename, direction))
|
||||
click.echo(f"Flip '{image.filename}' {direction}")
|
||||
image = copy_filename(image.transpose(mode), image)
|
||||
yield image
|
||||
|
||||
|
@ -217,7 +213,7 @@ def blur_cmd(images, radius):
|
|||
"""Applies gaussian blur."""
|
||||
blur = ImageFilter.GaussianBlur(radius)
|
||||
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)
|
||||
|
||||
|
||||
|
@ -234,9 +230,8 @@ def smoothen_cmd(images, iterations):
|
|||
"""Applies a smoothening filter."""
|
||||
for image in images:
|
||||
click.echo(
|
||||
"Smoothening '{}' {} time{}".format(
|
||||
image.filename, iterations, "s" if iterations != 1 else ""
|
||||
)
|
||||
f"Smoothening {image.filename!r} {iterations}"
|
||||
f" time{'s' if iterations != 1 else ''}"
|
||||
)
|
||||
for _ in range(iterations):
|
||||
image = copy_filename(image.filter(ImageFilter.BLUR), image)
|
||||
|
@ -248,7 +243,7 @@ def smoothen_cmd(images, iterations):
|
|||
def emboss_cmd(images):
|
||||
"""Embosses an image."""
|
||||
for image in images:
|
||||
click.echo("Embossing '{}'".format(image.filename))
|
||||
click.echo(f"Embossing '{image.filename}'")
|
||||
yield copy_filename(image.filter(ImageFilter.EMBOSS), image)
|
||||
|
||||
|
||||
|
@ -260,7 +255,7 @@ def emboss_cmd(images):
|
|||
def sharpen_cmd(images, factor):
|
||||
"""Sharpens an image."""
|
||||
for image in images:
|
||||
click.echo("Sharpen '{}' by {}".format(image.filename, factor))
|
||||
click.echo(f"Sharpen '{image.filename}' by {factor}")
|
||||
enhancer = ImageEnhance.Sharpness(image)
|
||||
yield copy_filename(enhancer.enhance(max(1.0, factor)), image)
|
||||
|
||||
|
@ -282,13 +277,12 @@ def paste_cmd(images, left, right):
|
|||
yield image
|
||||
return
|
||||
|
||||
click.echo("Paste '{}' on '{}'".format(to_paste.filename, image.filename))
|
||||
click.echo(f"Paste '{to_paste.filename}' on '{image.filename}'")
|
||||
mask = None
|
||||
if to_paste.mode == "RGBA" or "transparency" in to_paste.info:
|
||||
mask = to_paste
|
||||
image.paste(to_paste, (left, right), mask)
|
||||
image.filename += "+{}".format(to_paste.filename)
|
||||
image.filename += f"+{to_paste.filename}"
|
||||
yield image
|
||||
|
||||
for image in imageiter:
|
||||
yield image
|
||||
yield from imageiter
|
||||
|
|
|
@ -21,7 +21,7 @@ def ship():
|
|||
@click.argument("name")
|
||||
def ship_new(name):
|
||||
"""Creates a new ship."""
|
||||
click.echo("Created ship {}".format(name))
|
||||
click.echo(f"Created ship {name}")
|
||||
|
||||
|
||||
@ship.command("move")
|
||||
|
@ -31,7 +31,7 @@ def ship_new(name):
|
|||
@click.option("--speed", metavar="KN", default=10, help="Speed in knots.")
|
||||
def ship_move(ship, x, y, speed):
|
||||
"""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")
|
||||
|
@ -40,7 +40,7 @@ def ship_move(ship, x, y, speed):
|
|||
@click.argument("y", type=float)
|
||||
def ship_shoot(ship, 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")
|
||||
|
@ -61,7 +61,7 @@ def mine():
|
|||
@click.option("ty", "--drifting", flag_value="drifting", help="Drifting mine.")
|
||||
def mine_set(x, y, ty):
|
||||
"""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")
|
||||
|
@ -69,4 +69,4 @@ def mine_set(x, y, ty):
|
|||
@click.argument("y", type=float)
|
||||
def mine_remove(x, y):
|
||||
"""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
|
||||
|
||||
|
||||
class Repo(object):
|
||||
class Repo:
|
||||
def __init__(self, home):
|
||||
self.home = home
|
||||
self.config = {}
|
||||
|
@ -14,10 +14,10 @@ class Repo(object):
|
|||
def set_config(self, key, value):
|
||||
self.config[key] = value
|
||||
if self.verbose:
|
||||
click.echo(" config[{}] = {}".format(key, value), file=sys.stderr)
|
||||
click.echo(f" config[{key}] = {value}", file=sys.stderr)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Repo {}>".format(self.home)
|
||||
return f"<Repo {self.home}>"
|
||||
|
||||
|
||||
pass_repo = click.make_pass_decorator(Repo)
|
||||
|
@ -78,11 +78,11 @@ def clone(repo, src, dest, shallow, rev):
|
|||
"""
|
||||
if dest is None:
|
||||
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
|
||||
if shallow:
|
||||
click.echo("Making shallow checkout")
|
||||
click.echo("Checking out revision {}".format(rev))
|
||||
click.echo(f"Checking out revision {rev}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
@ -93,7 +93,7 @@ def delete(repo):
|
|||
|
||||
This will throw away the current repository.
|
||||
"""
|
||||
click.echo("Destroying repo {}".format(repo.home))
|
||||
click.echo(f"Destroying repo {repo.home}")
|
||||
click.echo("Deleted!")
|
||||
|
||||
|
||||
|
@ -136,7 +136,7 @@ def commit(repo, files, message):
|
|||
marker = "# Files to be committed:"
|
||||
hint = ["", "", marker, "#"]
|
||||
for file in files:
|
||||
hint.append("# U {}".format(file))
|
||||
hint.append(f"# U {file}")
|
||||
message = click.edit("\n".join(hint))
|
||||
if message is None:
|
||||
click.echo("Aborted!")
|
||||
|
@ -147,8 +147,8 @@ def commit(repo, files, message):
|
|||
return
|
||||
else:
|
||||
msg = "\n".join(message)
|
||||
click.echo("Files to be committed: {}".format(files))
|
||||
click.echo("Commit message:\n{}".format(msg))
|
||||
click.echo(f"Files to be committed: {files}")
|
||||
click.echo(f"Commit message:\n{msg}")
|
||||
|
||||
|
||||
@cli.command(short_help="Copies files.")
|
||||
|
@ -163,4 +163,4 @@ def copy(repo, src, dst, force):
|
|||
files from SRC to DST.
|
||||
"""
|
||||
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",
|
||||
py_modules=["termui"],
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
"click",
|
||||
# Colorama is only required for Windows.
|
||||
"colorama",
|
||||
],
|
||||
install_requires=["click"],
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
termui=termui:cli
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# coding: utf-8
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
|
@ -16,8 +15,8 @@ def cli():
|
|||
def colordemo():
|
||||
"""Demonstrates ANSI color support."""
|
||||
for color in "red", "green", "blue":
|
||||
click.echo(click.style("I am colored {}".format(color), fg=color))
|
||||
click.echo(click.style("I am background colored {}".format(color), bg=color))
|
||||
click.echo(click.style(f"I am colored {color}", fg=color))
|
||||
click.echo(click.style(f"I am background colored {color}", bg=color))
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
@ -25,7 +24,7 @@ def pager():
|
|||
"""Demonstrates using the pager."""
|
||||
lines = []
|
||||
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))
|
||||
|
||||
|
||||
|
@ -56,7 +55,7 @@ def progress(count):
|
|||
|
||||
def show_item(item):
|
||||
if item is not None:
|
||||
return "Item #{}".format(item)
|
||||
return f"Item #{item}"
|
||||
|
||||
with click.progressbar(
|
||||
filter(items),
|
||||
|
@ -71,7 +70,7 @@ def progress(count):
|
|||
length=count,
|
||||
label="Counting",
|
||||
bar_template="%(label)s %(bar)s | %(info)s",
|
||||
fill_char=click.style(u"█", fg="cyan"),
|
||||
fill_char=click.style("█", fg="cyan"),
|
||||
empty_char=" ",
|
||||
) as bar:
|
||||
for item in bar:
|
||||
|
@ -94,7 +93,7 @@ def progress(count):
|
|||
length=count,
|
||||
show_percent=False,
|
||||
label="Slowing progress bar",
|
||||
fill_char=click.style(u"█", fg="green"),
|
||||
fill_char=click.style("█", fg="green"),
|
||||
) as bar:
|
||||
for item in steps:
|
||||
time.sleep(item)
|
||||
|
@ -119,13 +118,13 @@ def locate(url):
|
|||
def edit():
|
||||
"""Opens an editor with some text in it."""
|
||||
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:
|
||||
msg = message.split(MARKER, 1)[0].rstrip("\n")
|
||||
if not msg:
|
||||
click.echo("Empty message!")
|
||||
else:
|
||||
click.echo("Message:\n{}".format(msg))
|
||||
click.echo(f"Message:\n{msg}")
|
||||
else:
|
||||
click.echo("You did not enter anything!")
|
||||
|
||||
|
@ -146,7 +145,7 @@ def pause():
|
|||
def menu():
|
||||
"""Shows a simple menu."""
|
||||
menu = "main"
|
||||
while 1:
|
||||
while True:
|
||||
if menu == "main":
|
||||
click.echo("Main menu:")
|
||||
click.echo(" d: debug menu")
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import click
|
||||
from urllib import parse as urlparse
|
||||
|
||||
try:
|
||||
from urllib import parse as urlparse
|
||||
except ImportError:
|
||||
import urlparse
|
||||
import click
|
||||
|
||||
|
||||
def validate_count(ctx, param, value):
|
||||
|
@ -20,8 +17,7 @@ class URL(click.ParamType):
|
|||
value = urlparse.urlparse(value)
|
||||
if value.scheme not in ("http", "https"):
|
||||
self.fail(
|
||||
"invalid URL scheme ({}). Only HTTP URLs are"
|
||||
" allowed".format(value.scheme),
|
||||
f"invalid URL scheme ({value.scheme}). Only HTTP URLs are allowed",
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
|
@ -47,6 +43,6 @@ def cli(count, foo, url):
|
|||
'If a value is provided it needs to be the value "wat".',
|
||||
param_hint=["--foo"],
|
||||
)
|
||||
click.echo("count: {}".format(count))
|
||||
click.echo("foo: {}".format(foo))
|
||||
click.echo("url: {!r}".format(url))
|
||||
click.echo(f"count: {count}")
|
||||
click.echo(f"foo: {foo}")
|
||||
click.echo(f"url: {url!r}")
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
-r docs.in
|
||||
-r tests.in
|
||||
-r typing.in
|
||||
pip-tools
|
||||
pre-commit
|
||||
tox
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
Pallets-Sphinx-Themes
|
||||
Sphinx
|
||||
sphinx-issues
|
||||
sphinxcontrib-log-cabinet
|
||||
sphinx-tabs
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
pytest
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
mypy
|
|
@ -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
|
107
setup.cfg
107
setup.cfg
|
@ -1,37 +1,100 @@
|
|||
[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]
|
||||
universal = 1
|
||||
[options]
|
||||
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]
|
||||
testpaths = tests
|
||||
filterwarnings =
|
||||
error
|
||||
filterwarnings =
|
||||
error
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
source =
|
||||
src
|
||||
tests
|
||||
branch = true
|
||||
source =
|
||||
click
|
||||
tests
|
||||
|
||||
[coverage:paths]
|
||||
source =
|
||||
click
|
||||
*/site-packages
|
||||
source =
|
||||
click
|
||||
*/site-packages
|
||||
|
||||
[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
|
||||
ignore =
|
||||
E203
|
||||
E501
|
||||
E722
|
||||
W503
|
||||
ignore =
|
||||
# slice notation whitespace, invalid
|
||||
E203
|
||||
# line length, handled by bugbear B950
|
||||
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
|
||||
per-file-ignores =
|
||||
src/click/__init__.py: F401
|
||||
per-file-ignores =
|
||||
# __init__ module exports names
|
||||
src/click/__init__.py: F401
|
||||
|
||||
[egg_info]
|
||||
tag_build =
|
||||
tag_date = 0
|
||||
[mypy]
|
||||
files = src/click
|
||||
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
|
||||
|
||||
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(
|
||||
name="click",
|
||||
version=version,
|
||||
url="https://palletsprojects.com/p/click/",
|
||||
project_urls={
|
||||
"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",
|
||||
install_requires=[
|
||||
"colorama; platform_system == 'Windows'",
|
||||
"importlib-metadata; python_version < '3.8'",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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
|
||||
composable.
|
||||
"""
|
||||
from .core import Argument
|
||||
from .core import BaseCommand
|
||||
from .core import Command
|
||||
from .core import CommandCollection
|
||||
from .core import Context
|
||||
from .core import Group
|
||||
from .core import MultiCommand
|
||||
from .core import Option
|
||||
from .core import Parameter
|
||||
from .decorators import argument
|
||||
from .decorators import command
|
||||
from .decorators import confirmation_option
|
||||
from .decorators import group
|
||||
from .decorators import help_option
|
||||
from .decorators import make_pass_decorator
|
||||
from .decorators import option
|
||||
from .decorators import pass_context
|
||||
from .decorators import pass_obj
|
||||
from .decorators import password_option
|
||||
from .decorators import version_option
|
||||
from .exceptions import Abort
|
||||
from .exceptions import BadArgumentUsage
|
||||
from .exceptions import BadOptionUsage
|
||||
from .exceptions import BadParameter
|
||||
from .exceptions import ClickException
|
||||
from .exceptions import FileError
|
||||
from .exceptions import MissingParameter
|
||||
from .exceptions import NoSuchOption
|
||||
from .exceptions import UsageError
|
||||
from .formatting import HelpFormatter
|
||||
from .formatting import wrap_text
|
||||
from .globals import get_current_context
|
||||
from .parser import OptionParser
|
||||
from .termui import clear
|
||||
from .termui import confirm
|
||||
from .termui import echo_via_pager
|
||||
from .termui import edit
|
||||
from .termui import get_terminal_size
|
||||
from .termui import getchar
|
||||
from .termui import launch
|
||||
from .termui import pause
|
||||
from .termui import progressbar
|
||||
from .termui import prompt
|
||||
from .termui import secho
|
||||
from .termui import style
|
||||
from .termui import unstyle
|
||||
from .types import BOOL
|
||||
from .types import Choice
|
||||
from .types import DateTime
|
||||
from .types import File
|
||||
from .types import FLOAT
|
||||
from .types import FloatRange
|
||||
from .types import INT
|
||||
from .types import IntRange
|
||||
from .types import ParamType
|
||||
from .types import Path
|
||||
from .types import STRING
|
||||
from .types import Tuple
|
||||
from .types import UNPROCESSED
|
||||
from .types import UUID
|
||||
from .utils import echo
|
||||
from .utils import format_filename
|
||||
from .utils import get_app_dir
|
||||
from .utils import get_binary_stream
|
||||
from .utils import get_os_args
|
||||
from .utils import get_text_stream
|
||||
from .utils import open_file
|
||||
from .core import Argument as Argument
|
||||
from .core import BaseCommand as BaseCommand
|
||||
from .core import Command as Command
|
||||
from .core import CommandCollection as CommandCollection
|
||||
from .core import Context as Context
|
||||
from .core import Group as Group
|
||||
from .core import MultiCommand as MultiCommand
|
||||
from .core import Option as Option
|
||||
from .core import Parameter as Parameter
|
||||
from .decorators import argument as argument
|
||||
from .decorators import command as command
|
||||
from .decorators import confirmation_option as confirmation_option
|
||||
from .decorators import group as group
|
||||
from .decorators import help_option as help_option
|
||||
from .decorators import make_pass_decorator as make_pass_decorator
|
||||
from .decorators import option as option
|
||||
from .decorators import pass_context as pass_context
|
||||
from .decorators import pass_obj as pass_obj
|
||||
from .decorators import password_option as password_option
|
||||
from .decorators import version_option as version_option
|
||||
from .exceptions import Abort as Abort
|
||||
from .exceptions import BadArgumentUsage as BadArgumentUsage
|
||||
from .exceptions import BadOptionUsage as BadOptionUsage
|
||||
from .exceptions import BadParameter as BadParameter
|
||||
from .exceptions import ClickException as ClickException
|
||||
from .exceptions import FileError as FileError
|
||||
from .exceptions import MissingParameter as MissingParameter
|
||||
from .exceptions import NoSuchOption as NoSuchOption
|
||||
from .exceptions import UsageError as UsageError
|
||||
from .formatting import HelpFormatter as HelpFormatter
|
||||
from .formatting import wrap_text as wrap_text
|
||||
from .globals import get_current_context as get_current_context
|
||||
from .parser import OptionParser as OptionParser
|
||||
from .termui import clear as clear
|
||||
from .termui import confirm as confirm
|
||||
from .termui import echo_via_pager as echo_via_pager
|
||||
from .termui import edit as edit
|
||||
from .termui import get_terminal_size as get_terminal_size
|
||||
from .termui import getchar as getchar
|
||||
from .termui import launch as launch
|
||||
from .termui import pause as pause
|
||||
from .termui import progressbar as progressbar
|
||||
from .termui import prompt as prompt
|
||||
from .termui import secho as secho
|
||||
from .termui import style as style
|
||||
from .termui import unstyle as unstyle
|
||||
from .types import BOOL as BOOL
|
||||
from .types import Choice as Choice
|
||||
from .types import DateTime as DateTime
|
||||
from .types import File as File
|
||||
from .types import FLOAT as FLOAT
|
||||
from .types import FloatRange as FloatRange
|
||||
from .types import INT as INT
|
||||
from .types import IntRange as IntRange
|
||||
from .types import ParamType as ParamType
|
||||
from .types import Path as Path
|
||||
from .types import STRING as STRING
|
||||
from .types import Tuple as Tuple
|
||||
from .types import UNPROCESSED as UNPROCESSED
|
||||
from .types import UUID as UUID
|
||||
from .utils import echo as echo
|
||||
from .utils import format_filename as format_filename
|
||||
from .utils import get_app_dir as get_app_dir
|
||||
from .utils import get_binary_stream as get_binary_stream
|
||||
from .utils import get_os_args as get_os_args
|
||||
from .utils import get_text_stream as get_text_stream
|
||||
from .utils import open_file as open_file
|
||||
|
||||
# Controls if click should emit the warning about the use of unicode
|
||||
# literals.
|
||||
disable_unicode_literals_warning = False
|
||||
|
||||
__version__ = "7.1.2"
|
||||
__version__ = "8.0.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 io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import typing as t
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
CYGWIN = sys.platform.startswith("cygwin")
|
||||
MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version)
|
||||
# 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", ""
|
||||
)
|
||||
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]")
|
||||
|
||||
|
||||
def get_filesystem_encoding():
|
||||
def get_filesystem_encoding() -> str:
|
||||
return sys.getfilesystemencoding() or sys.getdefaultencoding()
|
||||
|
||||
|
||||
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:
|
||||
encoding = get_best_encoding(stream)
|
||||
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."""
|
||||
try:
|
||||
return codecs.lookup(encoding).name == "ascii"
|
||||
|
@ -49,7 +50,7 @@ def is_ascii_encoding(encoding):
|
|||
return False
|
||||
|
||||
|
||||
def get_best_encoding(stream):
|
||||
def get_best_encoding(stream: t.IO) -> str:
|
||||
"""Returns the default stream encoding if not found."""
|
||||
rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
|
||||
if is_ascii_encoding(rv):
|
||||
|
@ -60,46 +61,30 @@ def get_best_encoding(stream):
|
|||
class _NonClosingTextIOWrapper(io.TextIOWrapper):
|
||||
def __init__(
|
||||
self,
|
||||
stream,
|
||||
encoding,
|
||||
errors,
|
||||
force_readable=False,
|
||||
force_writable=False,
|
||||
**extra
|
||||
):
|
||||
self._stream = stream = _FixupStream(stream, force_readable, force_writable)
|
||||
io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra)
|
||||
stream: t.BinaryIO,
|
||||
encoding: t.Optional[str],
|
||||
errors: t.Optional[str],
|
||||
force_readable: bool = False,
|
||||
force_writable: bool = False,
|
||||
**extra: t.Any,
|
||||
) -> None:
|
||||
self._stream = stream = t.cast(
|
||||
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
|
||||
# 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):
|
||||
def __del__(self) -> None:
|
||||
try:
|
||||
self.detach()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def isatty(self):
|
||||
def isatty(self) -> bool:
|
||||
# https://bitbucket.org/pypy/pypy/issue/1803
|
||||
return self._stream.isatty()
|
||||
|
||||
|
||||
class _FixupStream(object):
|
||||
class _FixupStream:
|
||||
"""The new io interface needs more from streams than streams
|
||||
traditionally implement. As such, this fix-up code is necessary in
|
||||
some circumstances.
|
||||
|
@ -109,45 +94,47 @@ class _FixupStream(object):
|
|||
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._force_readable = force_readable
|
||||
self._force_writable = force_writable
|
||||
|
||||
def __getattr__(self, name):
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
return getattr(self._stream, name)
|
||||
|
||||
def read1(self, size):
|
||||
def read1(self, size: int) -> bytes:
|
||||
f = getattr(self._stream, "read1", None)
|
||||
|
||||
if f is not None:
|
||||
return 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 t.cast(bytes, f(size))
|
||||
|
||||
return self._stream.read(size)
|
||||
|
||||
def readable(self):
|
||||
def readable(self) -> bool:
|
||||
if self._force_readable:
|
||||
return True
|
||||
x = getattr(self._stream, "readable", None)
|
||||
if x is not None:
|
||||
return x()
|
||||
return t.cast(bool, x())
|
||||
try:
|
||||
self._stream.read(0)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
def writable(self):
|
||||
def writable(self) -> bool:
|
||||
if self._force_writable:
|
||||
return True
|
||||
x = getattr(self._stream, "writable", None)
|
||||
if x is not None:
|
||||
return x()
|
||||
return t.cast(bool, x())
|
||||
try:
|
||||
self._stream.write("")
|
||||
self._stream.write("") # type: ignore
|
||||
except Exception:
|
||||
try:
|
||||
self._stream.write(b"")
|
||||
|
@ -155,10 +142,10 @@ class _FixupStream(object):
|
|||
return False
|
||||
return True
|
||||
|
||||
def seekable(self):
|
||||
def seekable(self) -> bool:
|
||||
x = getattr(self._stream, "seekable", None)
|
||||
if x is not None:
|
||||
return x()
|
||||
return t.cast(bool, x())
|
||||
try:
|
||||
self._stream.seek(self._stream.tell())
|
||||
except Exception:
|
||||
|
@ -166,351 +153,239 @@ class _FixupStream(object):
|
|||
return True
|
||||
|
||||
|
||||
if PY2:
|
||||
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
|
||||
|
||||
def _is_binary_reader(stream: t.IO, default: bool = False) -> bool:
|
||||
try:
|
||||
import msvcrt
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
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 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:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
pass
|
||||
stream.write(b"")
|
||||
except Exception:
|
||||
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:
|
||||
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):
|
||||
try:
|
||||
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
|
||||
# Otherwise, get the underlying binary reader.
|
||||
possible_binary_reader = find_binary(text_stream)
|
||||
|
||||
def isidentifier(x):
|
||||
return _identifier_re.search(x) is not None
|
||||
# If that's not possible, silently use the original reader
|
||||
# and get mojibake instead of exceptions.
|
||||
if possible_binary_reader is None:
|
||||
return text_stream
|
||||
|
||||
def get_binary_stdin():
|
||||
return set_binary_mode(sys.stdin)
|
||||
binary_reader = possible_binary_reader
|
||||
|
||||
def get_binary_stdout():
|
||||
_wrap_std_stream("stdout")
|
||||
return set_binary_mode(sys.stdout)
|
||||
# Default errors to replace instead of strict in order to get
|
||||
# something that works.
|
||||
if errors is None:
|
||||
errors = "replace"
|
||||
|
||||
def get_binary_stderr():
|
||||
_wrap_std_stream("stderr")
|
||||
return set_binary_mode(sys.stderr)
|
||||
|
||||
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,
|
||||
# Wrap the binary stream in a text stream with the correct
|
||||
# encoding parameters.
|
||||
return _make_text_stream(
|
||||
binary_reader,
|
||||
encoding,
|
||||
errors,
|
||||
is_binary,
|
||||
find_binary,
|
||||
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
|
||||
force_readable=force_readable,
|
||||
force_writable=force_writable,
|
||||
)
|
||||
|
||||
|
||||
def get_streerror(e, default=None):
|
||||
if hasattr(e, "strerror"):
|
||||
msg = e.strerror
|
||||
else:
|
||||
if default is not None:
|
||||
msg = default
|
||||
else:
|
||||
msg = str(e)
|
||||
if isinstance(msg, bytes):
|
||||
msg = msg.decode("utf-8", "replace")
|
||||
return msg
|
||||
def _force_correct_text_reader(
|
||||
text_reader: t.IO,
|
||||
encoding: t.Optional[str],
|
||||
errors: t.Optional[str],
|
||||
force_readable: bool = False,
|
||||
) -> t.TextIO:
|
||||
return _force_correct_text_stream(
|
||||
text_reader,
|
||||
encoding,
|
||||
errors,
|
||||
_is_binary_reader,
|
||||
_find_binary_reader,
|
||||
force_readable=force_readable,
|
||||
)
|
||||
|
||||
|
||||
def _wrap_io_open(file, mode, encoding, errors):
|
||||
"""On Python 2, :func:`io.open` returns a text file wrapper that
|
||||
requires passing ``unicode`` to ``write``. Need to open the file in
|
||||
binary mode then wrap it in a subclass that can write ``str`` and
|
||||
``unicode``.
|
||||
|
||||
Also handles not passing ``encoding`` and ``errors`` in binary mode.
|
||||
"""
|
||||
binary = "b" in mode
|
||||
|
||||
if binary:
|
||||
kwargs = {}
|
||||
else:
|
||||
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 _force_correct_text_writer(
|
||||
text_writer: t.IO,
|
||||
encoding: t.Optional[str],
|
||||
errors: t.Optional[str],
|
||||
force_writable: bool = False,
|
||||
) -> t.TextIO:
|
||||
return _force_correct_text_stream(
|
||||
text_writer,
|
||||
encoding,
|
||||
errors,
|
||||
_is_binary_writer,
|
||||
_find_binary_writer,
|
||||
force_writable=force_writable,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
try:
|
||||
perm = os.stat(filename).st_mode
|
||||
perm: t.Optional[int] = os.stat(filename).st_mode
|
||||
except OSError:
|
||||
perm = None
|
||||
|
||||
|
@ -561,7 +436,7 @@ def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False
|
|||
while True:
|
||||
tmp_filename = os.path.join(
|
||||
os.path.dirname(filename),
|
||||
".__atomic-write{:08x}".format(random.randrange(1 << 32)),
|
||||
f".__atomic-write{random.randrange(1 << 32):08x}",
|
||||
)
|
||||
try:
|
||||
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
|
||||
|
||||
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.
|
||||
if hasattr(os, "replace"):
|
||||
_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):
|
||||
class _AtomicFile:
|
||||
def __init__(self, f: t.IO, tmp_filename: str, real_filename: str) -> None:
|
||||
self._f = f
|
||||
self._tmp_filename = tmp_filename
|
||||
self._real_filename = real_filename
|
||||
self.closed = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
return self._real_filename
|
||||
|
||||
def close(self, delete=False):
|
||||
def close(self, delete: bool = False) -> None:
|
||||
if self.closed:
|
||||
return
|
||||
self._f.close()
|
||||
if not _can_replace:
|
||||
try:
|
||||
os.remove(self._real_filename)
|
||||
except OSError:
|
||||
pass
|
||||
_replace(self._tmp_filename, self._real_filename)
|
||||
os.replace(self._tmp_filename, self._real_filename)
|
||||
self.closed = True
|
||||
|
||||
def __getattr__(self, name):
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
return getattr(self._f, name)
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> "_AtomicFile":
|
||||
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)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return repr(self._f)
|
||||
|
||||
|
||||
auto_wrap_for_ansi = None
|
||||
colorama = None
|
||||
get_winterm_size = None
|
||||
|
||||
|
||||
def strip_ansi(value):
|
||||
def strip_ansi(value: str) -> str:
|
||||
return _ansi_re.sub("", value)
|
||||
|
||||
|
||||
def _is_jupyter_kernel_output(stream):
|
||||
if WIN:
|
||||
# TODO: Couldn't test on Windows, should't try to support until
|
||||
# someone tests the details wrt colorama.
|
||||
return
|
||||
|
||||
def _is_jupyter_kernel_output(stream: t.IO) -> bool:
|
||||
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
|
||||
stream = stream._stream
|
||||
|
||||
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 stream is None:
|
||||
stream = sys.stdin
|
||||
|
@ -657,99 +511,85 @@ def should_strip_ansi(stream=None, color=None):
|
|||
return not color
|
||||
|
||||
|
||||
# If we're on Windows, we provide transparent integration through
|
||||
# colorama. This will make ANSI colors through the echo function
|
||||
# work automatically.
|
||||
if WIN:
|
||||
# Windows has a smaller terminal
|
||||
DEFAULT_COLUMNS = 79
|
||||
# On Windows, wrap the output streams with colorama to support ANSI
|
||||
# color codes.
|
||||
# NOTE: double check is needed so mypy does not analyze this on Linux
|
||||
if sys.platform.startswith("win") and WIN:
|
||||
from ._winconsole import _get_windows_console_stream
|
||||
|
||||
from ._winconsole import _get_windows_console_stream, _wrap_std_stream
|
||||
|
||||
def _get_argv_encoding():
|
||||
def _get_argv_encoding() -> str:
|
||||
import locale
|
||||
|
||||
return locale.getpreferredencoding()
|
||||
|
||||
if PY2:
|
||||
_ansi_stream_wrappers: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
|
||||
|
||||
def raw_input(prompt=""):
|
||||
sys.stderr.flush()
|
||||
if prompt:
|
||||
stdout = _default_text_stdout()
|
||||
stdout.write(prompt)
|
||||
stdin = _default_text_stdin()
|
||||
return stdin.readline().rstrip("\r\n")
|
||||
def auto_wrap_for_ansi(
|
||||
stream: t.TextIO, color: t.Optional[bool] = None
|
||||
) -> t.TextIO:
|
||||
"""Support ANSI color and style codes on Windows by wrapping a
|
||||
stream with colorama.
|
||||
"""
|
||||
try:
|
||||
cached = _ansi_stream_wrappers.get(stream)
|
||||
except Exception:
|
||||
cached = None
|
||||
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
try:
|
||||
import colorama
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
_ansi_stream_wrappers = WeakKeyDictionary()
|
||||
|
||||
def auto_wrap_for_ansi(stream, color=None):
|
||||
"""This function wraps a stream so that calls through colorama
|
||||
are issued to the win32 console API to recolor on demand. It
|
||||
also ensures to reset the colors if a write call is interrupted
|
||||
to not destroy the console afterwards.
|
||||
"""
|
||||
strip = should_strip_ansi(stream, color)
|
||||
ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
|
||||
rv = t.cast(t.TextIO, ansi_wrapper.stream)
|
||||
_write = rv.write
|
||||
|
||||
def _safe_write(s):
|
||||
try:
|
||||
cached = _ansi_stream_wrappers.get(stream)
|
||||
except Exception:
|
||||
cached = None
|
||||
if cached is not None:
|
||||
return cached
|
||||
strip = should_strip_ansi(stream, color)
|
||||
ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
|
||||
rv = ansi_wrapper.stream
|
||||
_write = rv.write
|
||||
return _write(s)
|
||||
except BaseException:
|
||||
ansi_wrapper.reset_all()
|
||||
raise
|
||||
|
||||
def _safe_write(s):
|
||||
try:
|
||||
return _write(s)
|
||||
except:
|
||||
ansi_wrapper.reset_all()
|
||||
raise
|
||||
rv.write = _safe_write
|
||||
|
||||
rv.write = _safe_write
|
||||
try:
|
||||
_ansi_stream_wrappers[stream] = rv
|
||||
except Exception:
|
||||
pass
|
||||
return rv
|
||||
try:
|
||||
_ansi_stream_wrappers[stream] = rv
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_winterm_size():
|
||||
win = colorama.win32.GetConsoleScreenBufferInfo(
|
||||
colorama.win32.STDOUT
|
||||
).srWindow
|
||||
return win.Right - win.Left, win.Bottom - win.Top
|
||||
return rv
|
||||
|
||||
|
||||
else:
|
||||
|
||||
def _get_argv_encoding():
|
||||
def _get_argv_encoding() -> str:
|
||||
return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding()
|
||||
|
||||
_get_windows_console_stream = lambda *x: None
|
||||
_wrap_std_stream = lambda *x: None
|
||||
def _get_windows_console_stream(
|
||||
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))
|
||||
|
||||
|
||||
def isatty(stream):
|
||||
def isatty(stream: t.IO) -> bool:
|
||||
try:
|
||||
return stream.isatty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _make_cached_stream_func(src_func, wrapper_func):
|
||||
cache = WeakKeyDictionary()
|
||||
def _make_cached_stream_func(
|
||||
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()
|
||||
try:
|
||||
rv = cache.get(stream)
|
||||
|
@ -759,7 +599,6 @@ def _make_cached_stream_func(src_func, wrapper_func):
|
|||
return rv
|
||||
rv = wrapper_func()
|
||||
try:
|
||||
stream = src_func() # In case wrapper_func() modified the stream
|
||||
cache[stream] = rv
|
||||
except Exception:
|
||||
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)
|
||||
|
||||
|
||||
binary_streams = {
|
||||
binary_streams: t.Mapping[str, t.Callable[[], t.BinaryIO]] = {
|
||||
"stdin": get_binary_stdin,
|
||||
"stdout": get_binary_stdout,
|
||||
"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,
|
||||
"stdout": get_text_stdout,
|
||||
"stderr": get_text_stderr,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module contains implementations for the termui module. To keep the
|
||||
import time of Click down, some infrequently used functionality is
|
||||
|
@ -9,20 +8,22 @@ import math
|
|||
import os
|
||||
import sys
|
||||
import time
|
||||
import typing as t
|
||||
from gettext import gettext as _
|
||||
|
||||
from ._compat import _default_text_stdout
|
||||
from ._compat import CYGWIN
|
||||
from ._compat import get_best_encoding
|
||||
from ._compat import int_types
|
||||
from ._compat import isatty
|
||||
from ._compat import open_stream
|
||||
from ._compat import range_type
|
||||
from ._compat import strip_ansi
|
||||
from ._compat import term_len
|
||||
from ._compat import WIN
|
||||
from .exceptions import ClickException
|
||||
from .utils import echo
|
||||
|
||||
V = t.TypeVar("V")
|
||||
|
||||
if os.name == "nt":
|
||||
BEFORE_BAR = "\r"
|
||||
AFTER_BAR = "\n"
|
||||
|
@ -31,42 +32,25 @@ else:
|
|||
AFTER_BAR = "\033[?25h\n"
|
||||
|
||||
|
||||
def _length_hint(obj):
|
||||
"""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):
|
||||
class ProgressBar(t.Generic[V]):
|
||||
def __init__(
|
||||
self,
|
||||
iterable,
|
||||
length=None,
|
||||
fill_char="#",
|
||||
empty_char=" ",
|
||||
bar_template="%(bar)s",
|
||||
info_sep=" ",
|
||||
show_eta=True,
|
||||
show_percent=None,
|
||||
show_pos=False,
|
||||
item_show_func=None,
|
||||
label=None,
|
||||
file=None,
|
||||
color=None,
|
||||
width=30,
|
||||
):
|
||||
iterable: t.Optional[t.Iterable[V]],
|
||||
length: t.Optional[int] = None,
|
||||
fill_char: str = "#",
|
||||
empty_char: str = " ",
|
||||
bar_template: str = "%(bar)s",
|
||||
info_sep: str = " ",
|
||||
show_eta: bool = True,
|
||||
show_percent: t.Optional[bool] = None,
|
||||
show_pos: bool = False,
|
||||
item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None,
|
||||
label: t.Optional[str] = None,
|
||||
file: t.Optional[t.TextIO] = None,
|
||||
color: t.Optional[bool] = None,
|
||||
update_min_steps: int = 1,
|
||||
width: int = 30,
|
||||
) -> None:
|
||||
self.fill_char = fill_char
|
||||
self.empty_char = empty_char
|
||||
self.bar_template = bar_template
|
||||
|
@ -80,45 +64,50 @@ class ProgressBar(object):
|
|||
file = _default_text_stdout()
|
||||
self.file = file
|
||||
self.color = color
|
||||
self.update_min_steps = update_min_steps
|
||||
self._completed_intervals = 0
|
||||
self.width = width
|
||||
self.autowidth = width == 0
|
||||
|
||||
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 length is None:
|
||||
raise TypeError("iterable or length is required")
|
||||
iterable = range_type(length)
|
||||
iterable = t.cast(t.Iterable[V], range(length))
|
||||
self.iter = iter(iterable)
|
||||
self.length = length
|
||||
self.length_known = length is not None
|
||||
self.pos = 0
|
||||
self.avg = []
|
||||
self.avg: t.List[float] = []
|
||||
self.start = self.last_eta = time.time()
|
||||
self.eta_known = False
|
||||
self.finished = False
|
||||
self.max_width = None
|
||||
self.max_width: t.Optional[int] = None
|
||||
self.entered = False
|
||||
self.current_item = None
|
||||
self.current_item: t.Optional[V] = None
|
||||
self.is_hidden = not isatty(self.file)
|
||||
self._last_line = None
|
||||
self.short_limit = 0.5
|
||||
self._last_line: t.Optional[str] = None
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> "ProgressBar":
|
||||
self.entered = True
|
||||
self.render_progress()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||
self.render_finish()
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> t.Iterator[V]:
|
||||
if not self.entered:
|
||||
raise RuntimeError("You need to use progress bars in a with block.")
|
||||
self.render_progress()
|
||||
return self.generator()
|
||||
|
||||
def __next__(self):
|
||||
def __next__(self) -> V:
|
||||
# Iteration is defined in terms of a generator function,
|
||||
# returned by iter(self); use that to define next(). This works
|
||||
# because `self.iter` is an iterable consumed by that generator,
|
||||
|
@ -126,37 +115,31 @@ class ProgressBar(object):
|
|||
# twice works and does "what you want".
|
||||
return next(iter(self))
|
||||
|
||||
# Python 2 compat
|
||||
next = __next__
|
||||
|
||||
def is_fast(self):
|
||||
return time.time() - self.start <= self.short_limit
|
||||
|
||||
def render_finish(self):
|
||||
if self.is_hidden or self.is_fast():
|
||||
def render_finish(self) -> None:
|
||||
if self.is_hidden:
|
||||
return
|
||||
self.file.write(AFTER_BAR)
|
||||
self.file.flush()
|
||||
|
||||
@property
|
||||
def pct(self):
|
||||
def pct(self) -> float:
|
||||
if self.finished:
|
||||
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
|
||||
def time_per_iteration(self):
|
||||
def time_per_iteration(self) -> float:
|
||||
if not self.avg:
|
||||
return 0.0
|
||||
return sum(self.avg) / float(len(self.avg))
|
||||
|
||||
@property
|
||||
def eta(self):
|
||||
if self.length_known and not self.finished:
|
||||
def eta(self) -> float:
|
||||
if self.length is not None and not self.finished:
|
||||
return self.time_per_iteration * (self.length - self.pos)
|
||||
return 0.0
|
||||
|
||||
def format_eta(self):
|
||||
def format_eta(self) -> str:
|
||||
if self.eta_known:
|
||||
t = int(self.eta)
|
||||
seconds = t % 60
|
||||
|
@ -166,44 +149,44 @@ class ProgressBar(object):
|
|||
hours = t % 24
|
||||
t //= 24
|
||||
if t > 0:
|
||||
return "{}d {:02}:{:02}:{:02}".format(t, hours, minutes, seconds)
|
||||
return f"{t}d {hours:02}:{minutes:02}:{seconds:02}"
|
||||
else:
|
||||
return "{:02}:{:02}:{:02}".format(hours, minutes, seconds)
|
||||
return f"{hours:02}:{minutes:02}:{seconds:02}"
|
||||
return ""
|
||||
|
||||
def format_pos(self):
|
||||
def format_pos(self) -> str:
|
||||
pos = str(self.pos)
|
||||
if self.length_known:
|
||||
pos += "/{}".format(self.length)
|
||||
if self.length is not None:
|
||||
pos += f"/{self.length}"
|
||||
return pos
|
||||
|
||||
def format_pct(self):
|
||||
return "{: 4}%".format(int(self.pct * 100))[1:]
|
||||
def format_pct(self) -> str:
|
||||
return f"{int(self.pct * 100): 4}%"[1:]
|
||||
|
||||
def format_bar(self):
|
||||
if self.length_known:
|
||||
def format_bar(self) -> str:
|
||||
if self.length is not None:
|
||||
bar_length = int(self.pct * self.width)
|
||||
bar = self.fill_char * bar_length
|
||||
bar += self.empty_char * (self.width - bar_length)
|
||||
elif self.finished:
|
||||
bar = self.fill_char * self.width
|
||||
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:
|
||||
bar[
|
||||
chars[
|
||||
int(
|
||||
(math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
|
||||
* self.width
|
||||
)
|
||||
] = self.fill_char
|
||||
bar = "".join(bar)
|
||||
bar = "".join(chars)
|
||||
return bar
|
||||
|
||||
def format_progress_line(self):
|
||||
def format_progress_line(self) -> str:
|
||||
show_percent = self.show_percent
|
||||
|
||||
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
|
||||
|
||||
if self.show_pos:
|
||||
|
@ -226,10 +209,16 @@ class ProgressBar(object):
|
|||
}
|
||||
).rstrip()
|
||||
|
||||
def render_progress(self):
|
||||
from .termui import get_terminal_size
|
||||
def render_progress(self) -> None:
|
||||
import shutil
|
||||
|
||||
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
|
||||
|
||||
buf = []
|
||||
|
@ -238,10 +227,10 @@ class ProgressBar(object):
|
|||
old_width = self.width
|
||||
self.width = 0
|
||||
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:
|
||||
buf.append(BEFORE_BAR)
|
||||
buf.append(" " * self.max_width)
|
||||
buf.append(" " * self.max_width) # type: ignore
|
||||
self.max_width = new_width
|
||||
self.width = new_width
|
||||
|
||||
|
@ -260,14 +249,14 @@ class ProgressBar(object):
|
|||
line = "".join(buf)
|
||||
# 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
|
||||
echo(line, file=self.file, color=self.color, nl=False)
|
||||
self.file.flush()
|
||||
|
||||
def make_step(self, n_steps):
|
||||
def make_step(self, n_steps: int) -> None:
|
||||
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
|
||||
|
||||
if (time.time() - self.last_eta) < 1.0:
|
||||
|
@ -285,18 +274,40 @@ class ProgressBar(object):
|
|||
|
||||
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):
|
||||
self.make_step(n_steps)
|
||||
self.render_progress()
|
||||
def update(self, n_steps: int, current_item: t.Optional[V] = None) -> None:
|
||||
"""Update the progress bar by advancing a specified number of
|
||||
steps, and optionally set the ``current_item`` for this new
|
||||
position.
|
||||
|
||||
def finish(self):
|
||||
self.eta_known = 0
|
||||
:param n_steps: Number of steps to advance.
|
||||
: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.finished = True
|
||||
|
||||
def generator(self):
|
||||
def generator(self) -> t.Iterator[V]:
|
||||
"""Return a generator which yields the items added to the bar
|
||||
during construction, and updates the progress bar *after* the
|
||||
yielded block returns.
|
||||
|
@ -312,18 +323,25 @@ class ProgressBar(object):
|
|||
raise RuntimeError("You need to use progress bars in a with block.")
|
||||
|
||||
if self.is_hidden:
|
||||
for rv in self.iter:
|
||||
yield rv
|
||||
yield from self.iter
|
||||
else:
|
||||
for rv in self.iter:
|
||||
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
|
||||
self.update(1)
|
||||
|
||||
self.finish()
|
||||
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."""
|
||||
stdout = _default_text_stdout()
|
||||
if not isatty(sys.stdin) or not isatty(stdout):
|
||||
|
@ -345,14 +363,14 @@ def pager(generator, color=None):
|
|||
fd, filename = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
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 _nullpager(stdout, generator, color)
|
||||
finally:
|
||||
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
|
||||
pager through this might support colors.
|
||||
"""
|
||||
|
@ -364,7 +382,7 @@ def _pipepager(generator, cmd, color):
|
|||
# condition that
|
||||
cmd_detail = cmd.rsplit("/", 1)[-1].split()
|
||||
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:
|
||||
env["LESS"] = "-R"
|
||||
color = True
|
||||
|
@ -372,17 +390,18 @@ def _pipepager(generator, cmd, color):
|
|||
color = True
|
||||
|
||||
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:
|
||||
for text in generator:
|
||||
if not color:
|
||||
text = strip_ansi(text)
|
||||
|
||||
c.stdin.write(text.encode(encoding, "replace"))
|
||||
except (IOError, KeyboardInterrupt):
|
||||
stdin.write(text.encode(encoding, "replace"))
|
||||
except (OSError, KeyboardInterrupt):
|
||||
pass
|
||||
else:
|
||||
c.stdin.close()
|
||||
stdin.close()
|
||||
|
||||
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting
|
||||
# search or other commands inside less).
|
||||
|
@ -401,11 +420,13 @@ def _pipepager(generator, cmd, color):
|
|||
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."""
|
||||
import tempfile
|
||||
|
||||
filename = tempfile.mktemp()
|
||||
fd, filename = tempfile.mkstemp()
|
||||
# TODO: This never terminates if the passed generator never terminates.
|
||||
text = "".join(generator)
|
||||
if not color:
|
||||
|
@ -414,12 +435,15 @@ def _tempfilepager(generator, cmd, color):
|
|||
with open_stream(filename, "wb")[0] as f:
|
||||
f.write(text.encode(encoding))
|
||||
try:
|
||||
os.system('{} "{}"'.format(cmd, filename))
|
||||
os.system(f'{cmd} "{filename}"')
|
||||
finally:
|
||||
os.close(fd)
|
||||
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."""
|
||||
for text in generator:
|
||||
if not color:
|
||||
|
@ -427,14 +451,20 @@ def _nullpager(stream, generator, color):
|
|||
stream.write(text)
|
||||
|
||||
|
||||
class Editor(object):
|
||||
def __init__(self, editor=None, env=None, require_save=True, extension=".txt"):
|
||||
class Editor:
|
||||
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.env = env
|
||||
self.require_save = require_save
|
||||
self.extension = extension
|
||||
|
||||
def get_editor(self):
|
||||
def get_editor(self) -> str:
|
||||
if self.editor is not None:
|
||||
return self.editor
|
||||
for key in "VISUAL", "EDITOR":
|
||||
|
@ -444,48 +474,62 @@ class Editor(object):
|
|||
if WIN:
|
||||
return "notepad"
|
||||
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 "vi"
|
||||
|
||||
def edit_file(self, filename):
|
||||
def edit_file(self, filename: str) -> None:
|
||||
import subprocess
|
||||
|
||||
editor = self.get_editor()
|
||||
environ: t.Optional[t.Dict[str, str]] = None
|
||||
|
||||
if self.env:
|
||||
environ = os.environ.copy()
|
||||
environ.update(self.env)
|
||||
else:
|
||||
environ = None
|
||||
|
||||
try:
|
||||
c = subprocess.Popen(
|
||||
'{} "{}"'.format(editor, filename), env=environ, shell=True,
|
||||
)
|
||||
c = subprocess.Popen(f'{editor} "{filename}"', env=environ, shell=True)
|
||||
exit_code = c.wait()
|
||||
if exit_code != 0:
|
||||
raise ClickException("{}: Editing failed!".format(editor))
|
||||
raise ClickException(
|
||||
_("{editor}: Editing failed").format(editor=editor)
|
||||
)
|
||||
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
|
||||
|
||||
text = text or ""
|
||||
if text and not text.endswith("\n"):
|
||||
text += "\n"
|
||||
if not text:
|
||||
data = b""
|
||||
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)
|
||||
try:
|
||||
if WIN:
|
||||
encoding = "utf-8-sig"
|
||||
text = text.replace("\n", "\r\n")
|
||||
else:
|
||||
encoding = "utf-8"
|
||||
text = text.encode(encoding)
|
||||
f: t.BinaryIO
|
||||
|
||||
f = os.fdopen(fd, "wb")
|
||||
f.write(text)
|
||||
f.close()
|
||||
try:
|
||||
with os.fdopen(fd, "wb") as f:
|
||||
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)
|
||||
|
||||
self.edit_file(name)
|
||||
|
@ -493,26 +537,26 @@ class Editor(object):
|
|||
if self.require_save and os.path.getmtime(name) == timestamp:
|
||||
return None
|
||||
|
||||
f = open(name, "rb")
|
||||
try:
|
||||
with open(name, "rb") as f:
|
||||
rv = f.read()
|
||||
finally:
|
||||
f.close()
|
||||
return rv.decode("utf-8-sig").replace("\r\n", "\n")
|
||||
|
||||
if isinstance(text, (bytes, bytearray)):
|
||||
return rv
|
||||
|
||||
return rv.decode("utf-8-sig").replace("\r\n", "\n") # type: ignore
|
||||
finally:
|
||||
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
|
||||
|
||||
def _unquote_file(url):
|
||||
try:
|
||||
import urllib
|
||||
except ImportError:
|
||||
import urllib
|
||||
def _unquote_file(url: str) -> str:
|
||||
from urllib.parse import unquote
|
||||
|
||||
if url.startswith("file://"):
|
||||
url = urllib.unquote(url[7:])
|
||||
url = unquote(url[7:])
|
||||
|
||||
return url
|
||||
|
||||
if sys.platform == "darwin":
|
||||
|
@ -529,19 +573,21 @@ def open_url(url, wait=False, locate=False):
|
|||
null.close()
|
||||
elif WIN:
|
||||
if locate:
|
||||
url = _unquote_file(url)
|
||||
args = 'explorer /select,"{}"'.format(_unquote_file(url.replace('"', "")))
|
||||
url = _unquote_file(url.replace('"', ""))
|
||||
args = f'explorer /select,"{url}"'
|
||||
else:
|
||||
args = 'start {} "" "{}"'.format(
|
||||
"/WAIT" if wait else "", url.replace('"', "")
|
||||
)
|
||||
url = url.replace('"', "")
|
||||
wait_str = "/WAIT" if wait else ""
|
||||
args = f'start {wait_str} "" "{url}"'
|
||||
return os.system(args)
|
||||
elif CYGWIN:
|
||||
if locate:
|
||||
url = _unquote_file(url)
|
||||
args = 'cygstart "{}"'.format(os.path.dirname(url).replace('"', ""))
|
||||
url = os.path.dirname(_unquote_file(url).replace('"', ""))
|
||||
args = f'cygstart "{url}"'
|
||||
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)
|
||||
|
||||
try:
|
||||
|
@ -562,23 +608,27 @@ def open_url(url, wait=False, locate=False):
|
|||
return 1
|
||||
|
||||
|
||||
def _translate_ch_to_exc(ch):
|
||||
if ch == u"\x03":
|
||||
def _translate_ch_to_exc(ch: str) -> t.Optional[BaseException]:
|
||||
if ch == "\x03":
|
||||
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()
|
||||
if ch == u"\x1a" and WIN: # Windows, Ctrl+Z
|
||||
|
||||
if ch == "\x1a" and WIN: # Windows, Ctrl+Z
|
||||
raise EOFError()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
if WIN:
|
||||
import msvcrt
|
||||
|
||||
@contextlib.contextmanager
|
||||
def raw_terminal():
|
||||
yield
|
||||
def raw_terminal() -> t.Iterator[int]:
|
||||
yield -1
|
||||
|
||||
def getchar(echo):
|
||||
def getchar(echo: bool) -> str:
|
||||
# The function `getch` will return a bytes object corresponding to
|
||||
# the pressed character. Since Windows 10 build 1803, it will also
|
||||
# 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`
|
||||
# is doing the right thing in more situations than with `getch`.
|
||||
func: t.Callable[[], str]
|
||||
|
||||
if echo:
|
||||
func = msvcrt.getwche
|
||||
func = msvcrt.getwche # type: ignore
|
||||
else:
|
||||
func = msvcrt.getwch
|
||||
func = msvcrt.getwch # type: ignore
|
||||
|
||||
rv = func()
|
||||
if rv in (u"\x00", u"\xe0"):
|
||||
|
||||
if rv in ("\x00", "\xe0"):
|
||||
# \x00 and \xe0 are control characters that indicate special key,
|
||||
# see above.
|
||||
rv += func()
|
||||
|
||||
_translate_ch_to_exc(rv)
|
||||
return rv
|
||||
|
||||
|
@ -627,31 +681,38 @@ else:
|
|||
import termios
|
||||
|
||||
@contextlib.contextmanager
|
||||
def raw_terminal():
|
||||
def raw_terminal() -> t.Iterator[int]:
|
||||
f: t.Optional[t.TextIO]
|
||||
fd: int
|
||||
|
||||
if not isatty(sys.stdin):
|
||||
f = open("/dev/tty")
|
||||
fd = f.fileno()
|
||||
else:
|
||||
fd = sys.stdin.fileno()
|
||||
f = None
|
||||
|
||||
try:
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
yield fd
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
sys.stdout.flush()
|
||||
|
||||
if f is not None:
|
||||
f.close()
|
||||
except termios.error:
|
||||
pass
|
||||
|
||||
def getchar(echo):
|
||||
def getchar(echo: bool) -> str:
|
||||
with raw_terminal() as fd:
|
||||
ch = os.read(fd, 32)
|
||||
ch = ch.decode(get_best_encoding(sys.stdin), "replace")
|
||||
ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace")
|
||||
|
||||
if echo and isatty(sys.stdout):
|
||||
sys.stdout.write(ch)
|
||||
|
||||
_translate_ch_to_exc(ch)
|
||||
return ch
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import textwrap
|
||||
import typing as t
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if self.break_long_words:
|
||||
|
@ -16,22 +23,27 @@ class TextWrapper(textwrap.TextWrapper):
|
|||
cur_line.append(reversed_chunks.pop())
|
||||
|
||||
@contextmanager
|
||||
def extra_indent(self, indent):
|
||||
def extra_indent(self, indent: str) -> t.Iterator[None]:
|
||||
old_initial_indent = self.initial_indent
|
||||
old_subsequent_indent = self.subsequent_indent
|
||||
self.initial_indent += indent
|
||||
self.subsequent_indent += indent
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.initial_indent = old_initial_indent
|
||||
self.subsequent_indent = old_subsequent_indent
|
||||
|
||||
def indent_only(self, text):
|
||||
def indent_only(self, text: str) -> str:
|
||||
rv = []
|
||||
|
||||
for idx, line in enumerate(text.splitlines()):
|
||||
indent = self.initial_indent
|
||||
|
||||
if idx > 0:
|
||||
indent = self.subsequent_indent
|
||||
rv.append(indent + line)
|
||||
|
||||
rv.append(f"{indent}{line}")
|
||||
|
||||
return "\n".join(rv)
|
||||
|
|
|
@ -1,131 +1,100 @@
|
|||
import codecs
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ._compat import PY2
|
||||
from gettext import gettext as _
|
||||
|
||||
|
||||
def _find_unicode_literals_frame():
|
||||
import __future__
|
||||
|
||||
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
|
||||
def _verify_python_env() -> None:
|
||||
"""Ensures that the environment is good for Unicode."""
|
||||
try:
|
||||
import locale
|
||||
from locale import getpreferredencoding
|
||||
|
||||
fs_enc = codecs.lookup(locale.getpreferredencoding()).name
|
||||
fs_enc = codecs.lookup(getpreferredencoding()).name
|
||||
except Exception:
|
||||
fs_enc = "ascii"
|
||||
|
||||
if fs_enc != "ascii":
|
||||
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":
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
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]
|
||||
except OSError:
|
||||
rv = b""
|
||||
rv = ""
|
||||
|
||||
good_locales = set()
|
||||
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():
|
||||
locale = line.strip()
|
||||
|
||||
if locale.lower().endswith((".utf-8", ".utf8")):
|
||||
good_locales.add(locale)
|
||||
|
||||
if locale.lower() in ("c.utf8", "c.utf-8"):
|
||||
has_c_utf8 = True
|
||||
|
||||
extra += "\n\n"
|
||||
if not good_locales:
|
||||
extra += (
|
||||
"Additional information: on this system no suitable"
|
||||
" UTF-8 locales were discovered. This most likely"
|
||||
" requires resolving by reconfiguring the locale"
|
||||
" system."
|
||||
extra.append(
|
||||
_(
|
||||
"Additional information: on this system no suitable"
|
||||
" UTF-8 locales were discovered. This most likely"
|
||||
" requires resolving by reconfiguring the locale"
|
||||
" system."
|
||||
)
|
||||
)
|
||||
elif has_c_utf8:
|
||||
extra += (
|
||||
"This system supports the C.UTF-8 locale which is"
|
||||
" recommended. You might be able to resolve your issue"
|
||||
" by exporting the following environment variables:\n\n"
|
||||
" export LC_ALL=C.UTF-8\n"
|
||||
" export LANG=C.UTF-8"
|
||||
extra.append(
|
||||
_(
|
||||
"This system supports the C.UTF-8 locale which is"
|
||||
" recommended. You might be able to resolve your"
|
||||
" issue by exporting the following environment"
|
||||
" variables:"
|
||||
)
|
||||
)
|
||||
extra.append(" export LC_ALL=C.UTF-8\n export LANG=C.UTF-8")
|
||||
else:
|
||||
extra += (
|
||||
"This system lists a couple of UTF-8 supporting locales"
|
||||
" that you can pick from. The following suitable"
|
||||
" locales were discovered: {}".format(", ".join(sorted(good_locales)))
|
||||
extra.append(
|
||||
_(
|
||||
"This system lists some UTF-8 supporting locales"
|
||||
" that you can pick from. The following suitable"
|
||||
" locales were discovered: {locales}"
|
||||
).format(locales=", ".join(sorted(good_locales)))
|
||||
)
|
||||
|
||||
bad_locale = None
|
||||
for locale in os.environ.get("LC_ALL"), os.environ.get("LANG"):
|
||||
if locale and locale.lower().endswith((".utf-8", ".utf8")):
|
||||
bad_locale = locale
|
||||
if locale is not None:
|
||||
|
||||
for env_locale in os.environ.get("LC_ALL"), os.environ.get("LANG"):
|
||||
if env_locale and env_locale.lower().endswith((".utf-8", ".utf8")):
|
||||
bad_locale = env_locale
|
||||
|
||||
if env_locale is not None:
|
||||
break
|
||||
|
||||
if bad_locale is not None:
|
||||
extra += (
|
||||
"\n\nClick discovered that you exported a UTF-8 locale"
|
||||
" but the locale system could not pick up from it"
|
||||
" because it does not exist. The exported locale is"
|
||||
" '{}' but it is not supported".format(bad_locale)
|
||||
extra.append(
|
||||
_(
|
||||
"Click discovered that you exported a UTF-8 locale"
|
||||
" but the locale system could not pick up from it"
|
||||
" because it does not exist. The exported locale is"
|
||||
" {locale!r} but it is not supported."
|
||||
).format(locale=bad_locale)
|
||||
)
|
||||
|
||||
raise RuntimeError(
|
||||
"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)
|
||||
)
|
||||
raise RuntimeError("\n\n".join(extra))
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# This module is based on the excellent work by Adam Bartoš who
|
||||
# provided a lot of what went into the implementation here in
|
||||
# the discussion to issue1602 in the Python bug tracker.
|
||||
|
@ -6,13 +5,11 @@
|
|||
# There are some general differences in regards to how this works
|
||||
# compared to the original patches as we do not need to patch
|
||||
# the entire interpreter but just work in our little world of
|
||||
# echo and prmopt.
|
||||
import ctypes
|
||||
# echo and prompt.
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import zlib
|
||||
import typing as t
|
||||
from ctypes import byref
|
||||
from ctypes import c_char
|
||||
from ctypes import c_char_p
|
||||
|
@ -22,28 +19,18 @@ from ctypes import c_ulong
|
|||
from ctypes import c_void_p
|
||||
from ctypes import POINTER
|
||||
from ctypes import py_object
|
||||
from ctypes import windll
|
||||
from ctypes import WinError
|
||||
from ctypes import WINFUNCTYPE
|
||||
from ctypes import Structure
|
||||
from ctypes.wintypes import DWORD
|
||||
from ctypes.wintypes import HANDLE
|
||||
from ctypes.wintypes import LPCWSTR
|
||||
from ctypes.wintypes import LPWSTR
|
||||
|
||||
import msvcrt
|
||||
|
||||
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)
|
||||
|
||||
|
@ -57,16 +44,12 @@ GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
|
|||
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
|
||||
("CommandLineToArgvW", windll.shell32)
|
||||
)
|
||||
LocalFree = WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)(
|
||||
("LocalFree", windll.kernel32)
|
||||
)
|
||||
|
||||
LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32))
|
||||
|
||||
STDIN_HANDLE = GetStdHandle(-10)
|
||||
STDOUT_HANDLE = GetStdHandle(-11)
|
||||
STDERR_HANDLE = GetStdHandle(-12)
|
||||
|
||||
|
||||
PyBUF_SIMPLE = 0
|
||||
PyBUF_WRITABLE = 1
|
||||
|
||||
|
@ -81,36 +64,37 @@ STDERR_FILENO = 2
|
|||
EOF = b"\x1a"
|
||||
MAX_BYTES_WRITTEN = 32767
|
||||
|
||||
|
||||
class Py_buffer(ctypes.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),
|
||||
]
|
||||
|
||||
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:
|
||||
try:
|
||||
from ctypes import pythonapi
|
||||
except ImportError:
|
||||
# On PyPy we cannot get buffers so our ability to operate here is
|
||||
# severely limited.
|
||||
get_buffer = None
|
||||
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):
|
||||
buf = Py_buffer()
|
||||
flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
|
||||
PyObject_GetBuffer(py_object(obj), byref(buf), flags)
|
||||
|
||||
try:
|
||||
buffer_type = c_char * buf.len
|
||||
return buffer_type.from_address(buf.buf)
|
||||
|
@ -123,7 +107,7 @@ class _WindowsConsoleRawIOBase(io.RawIOBase):
|
|||
self.handle = handle
|
||||
|
||||
def isatty(self):
|
||||
io.RawIOBase.isatty(self)
|
||||
super().isatty()
|
||||
return True
|
||||
|
||||
|
||||
|
@ -155,7 +139,7 @@ class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
|
|||
# wait for KeyboardInterrupt
|
||||
time.sleep(0.1)
|
||||
if not rv:
|
||||
raise OSError("Windows error: {}".format(GetLastError()))
|
||||
raise OSError(f"Windows error: {GetLastError()}")
|
||||
|
||||
if buffer[0] == EOF:
|
||||
return 0
|
||||
|
@ -172,7 +156,7 @@ class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
|
|||
return "ERROR_SUCCESS"
|
||||
elif errno == ERROR_NOT_ENOUGH_MEMORY:
|
||||
return "ERROR_NOT_ENOUGH_MEMORY"
|
||||
return "Windows error {}".format(errno)
|
||||
return f"Windows error {errno}"
|
||||
|
||||
def write(self, b):
|
||||
bytes_to_be_written = len(b)
|
||||
|
@ -194,17 +178,17 @@ class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
|
|||
return bytes_written
|
||||
|
||||
|
||||
class ConsoleStream(object):
|
||||
def __init__(self, text_stream, byte_stream):
|
||||
class ConsoleStream:
|
||||
def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None:
|
||||
self._text_stream = text_stream
|
||||
self.buffer = byte_stream
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
return self.buffer.name
|
||||
|
||||
def write(self, x):
|
||||
if isinstance(x, text_type):
|
||||
def write(self, x: t.AnyStr) -> int:
|
||||
if isinstance(x, str):
|
||||
return self._text_stream.write(x)
|
||||
try:
|
||||
self.flush()
|
||||
|
@ -212,159 +196,84 @@ class ConsoleStream(object):
|
|||
pass
|
||||
return self.buffer.write(x)
|
||||
|
||||
def writelines(self, lines):
|
||||
def writelines(self, lines: t.Iterable[t.AnyStr]) -> None:
|
||||
for line in lines:
|
||||
self.write(line)
|
||||
|
||||
def __getattr__(self, name):
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
return getattr(self._text_stream, name)
|
||||
|
||||
def isatty(self):
|
||||
def isatty(self) -> bool:
|
||||
return self.buffer.isatty()
|
||||
|
||||
def __repr__(self):
|
||||
return "<ConsoleStream name={!r} encoding={!r}>".format(
|
||||
self.name, self.encoding
|
||||
)
|
||||
return f"<ConsoleStream name={self.name!r} encoding={self.encoding!r}>"
|
||||
|
||||
|
||||
class WindowsChunkedWriter(object):
|
||||
"""
|
||||
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):
|
||||
def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO:
|
||||
text_stream = _NonClosingTextIOWrapper(
|
||||
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
|
||||
"utf-16-le",
|
||||
"strict",
|
||||
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(
|
||||
io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
|
||||
"utf-16-le",
|
||||
"strict",
|
||||
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(
|
||||
io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
|
||||
"utf-16-le",
|
||||
"strict",
|
||||
line_buffering=True,
|
||||
)
|
||||
return ConsoleStream(text_stream, buffer_stream)
|
||||
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
||||
|
||||
|
||||
if PY2:
|
||||
|
||||
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 = {
|
||||
_stream_factories: t.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = {
|
||||
0: _get_text_stdin,
|
||||
1: _get_text_stdout,
|
||||
2: _get_text_stderr,
|
||||
}
|
||||
|
||||
|
||||
def _is_console(f):
|
||||
def _is_console(f: t.TextIO) -> bool:
|
||||
if not hasattr(f, "fileno"):
|
||||
return False
|
||||
|
||||
try:
|
||||
fileno = f.fileno()
|
||||
except OSError:
|
||||
except (OSError, io.UnsupportedOperation):
|
||||
return False
|
||||
|
||||
handle = msvcrt.get_osfhandle(fileno)
|
||||
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 (
|
||||
get_buffer is not None
|
||||
and encoding in ("utf-16-le", None)
|
||||
and errors in ("strict", None)
|
||||
and encoding in {"utf-16-le", None}
|
||||
and errors in {"strict", None}
|
||||
and _is_console(f)
|
||||
):
|
||||
func = _stream_factories.get(f.fileno())
|
||||
if func is not None:
|
||||
if not PY2:
|
||||
f = getattr(f, "buffer", None)
|
||||
if f is None:
|
||||
return None
|
||||
else:
|
||||
# If we are on Python 2 we need to set the stream that we
|
||||
# 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)
|
||||
b = getattr(f, "buffer", None)
|
||||
|
||||
if b is None:
|
||||
return None
|
||||
|
||||
return func(b)
|
||||
|
|
2171
src/click/core.py
2171
src/click/core.py
|
@ -1,17 +1,20 @@
|
|||
import enum
|
||||
import errno
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import typing
|
||||
import typing as t
|
||||
from collections import abc
|
||||
from contextlib import contextmanager
|
||||
from contextlib import ExitStack
|
||||
from functools import partial
|
||||
from functools import update_wrapper
|
||||
from gettext import gettext as _
|
||||
from gettext import ngettext
|
||||
from itertools import repeat
|
||||
|
||||
from ._compat import isidentifier
|
||||
from ._compat import iteritems
|
||||
from ._compat import PY2
|
||||
from ._compat import string_types
|
||||
from ._unicodefun import _check_for_unicode_literals
|
||||
from ._unicodefun import _verify_python3_env
|
||||
from . import types
|
||||
from ._unicodefun import _verify_python_env
|
||||
from .exceptions import Abort
|
||||
from .exceptions import BadParameter
|
||||
from .exceptions import ClickException
|
||||
|
@ -22,58 +25,49 @@ from .formatting import HelpFormatter
|
|||
from .formatting import join_options
|
||||
from .globals import pop_context
|
||||
from .globals import push_context
|
||||
from .parser import _flag_needs_value
|
||||
from .parser import OptionParser
|
||||
from .parser import split_opt
|
||||
from .termui import confirm
|
||||
from .termui import prompt
|
||||
from .termui import style
|
||||
from .types import BOOL
|
||||
from .types import convert_type
|
||||
from .types import IntRange
|
||||
from .utils import _detect_program_name
|
||||
from .utils import _expand_args
|
||||
from .utils import echo
|
||||
from .utils import get_os_args
|
||||
from .utils import make_default_short_help
|
||||
from .utils import make_str
|
||||
from .utils import PacifyFlushWrapper
|
||||
|
||||
_missing = object()
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
from .shell_completion import CompletionItem
|
||||
|
||||
SUBCOMMAND_METAVAR = "COMMAND [ARGS]..."
|
||||
SUBCOMMANDS_METAVAR = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
|
||||
|
||||
DEPRECATED_HELP_NOTICE = " (DEPRECATED)"
|
||||
DEPRECATED_INVOKE_NOTICE = "DeprecationWarning: The command %(name)s is deprecated."
|
||||
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||
V = t.TypeVar("V")
|
||||
|
||||
|
||||
def _maybe_show_deprecated_notice(cmd):
|
||||
if cmd.deprecated:
|
||||
echo(style(DEPRECATED_INVOKE_NOTICE % {"name": cmd.name}, fg="red"), err=True)
|
||||
def _complete_visible_commands(
|
||||
ctx: "Context", incomplete: str
|
||||
) -> t.Iterator[t.Tuple[str, "Command"]]:
|
||||
"""List all the subcommands of a group that start with the
|
||||
incomplete value and aren't hidden.
|
||||
|
||||
|
||||
def fast_exit(code):
|
||||
"""Exit without garbage collection, this speeds up exit by about 10ms for
|
||||
things like bash completion.
|
||||
:param ctx: Invocation context for the group.
|
||||
:param incomplete: Value being completed. May be empty.
|
||||
"""
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(code)
|
||||
multi = t.cast(MultiCommand, ctx.command)
|
||||
|
||||
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):
|
||||
"""Internal handler for the bash completion support."""
|
||||
if complete_var is 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):
|
||||
def _check_multicommand(
|
||||
base_command: "MultiCommand", cmd_name: str, cmd: "Command", register: bool = False
|
||||
) -> None:
|
||||
if not base_command.chain or not isinstance(cmd, MultiCommand):
|
||||
return
|
||||
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."
|
||||
)
|
||||
raise RuntimeError(
|
||||
"{}. Command '{}' is set to chain and '{}' was added as"
|
||||
" subcommand but it in itself is a multi command. ('{}' is a {}"
|
||||
" within a chained {} named '{}').".format(
|
||||
hint,
|
||||
base_command.name,
|
||||
cmd_name,
|
||||
cmd_name,
|
||||
cmd.__class__.__name__,
|
||||
base_command.__class__.__name__,
|
||||
base_command.name,
|
||||
)
|
||||
f"{hint}. Command {base_command.name!r} is set to chain and"
|
||||
f" {cmd_name!r} was added as a subcommand but it in itself is a"
|
||||
f" multi command. ({cmd_name!r} is a {type(cmd).__name__}"
|
||||
f" within a chained {type(base_command).__name__} named"
|
||||
f" {base_command.name!r})."
|
||||
)
|
||||
|
||||
|
||||
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)))
|
||||
|
||||
|
||||
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
|
||||
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."""
|
||||
try:
|
||||
yield
|
||||
|
@ -140,23 +112,53 @@ def augment_usage_errors(ctx, param=None):
|
|||
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
|
||||
for processing and an iterable of parameters that exist, this returns
|
||||
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:
|
||||
idx = invocation_order.index(item)
|
||||
idx: float = invocation_order.index(item)
|
||||
except ValueError:
|
||||
idx = float("inf")
|
||||
return (not item.is_eager, idx)
|
||||
|
||||
return not item.is_eager, idx
|
||||
|
||||
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
|
||||
for the script execution at every single level. It's normally invisible
|
||||
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
|
||||
: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 parent: the parent context.
|
||||
: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
|
||||
default not the case. This for instance would affect
|
||||
help output.
|
||||
:param show_default: if True, shows defaults for all options.
|
||||
Even if an option is later created with show_default=False,
|
||||
this command-level setting overrides it.
|
||||
:param show_default: Show defaults for all options. If not set,
|
||||
defaults to the value from a parent context. Overrides an
|
||||
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__(
|
||||
self,
|
||||
command,
|
||||
parent=None,
|
||||
info_name=None,
|
||||
obj=None,
|
||||
auto_envvar_prefix=None,
|
||||
default_map=None,
|
||||
terminal_width=None,
|
||||
max_content_width=None,
|
||||
resilient_parsing=False,
|
||||
allow_extra_args=None,
|
||||
allow_interspersed_args=None,
|
||||
ignore_unknown_options=None,
|
||||
help_option_names=None,
|
||||
token_normalize_func=None,
|
||||
color=None,
|
||||
show_default=None,
|
||||
):
|
||||
command: "Command",
|
||||
parent: t.Optional["Context"] = None,
|
||||
info_name: t.Optional[str] = None,
|
||||
obj: t.Optional[t.Any] = None,
|
||||
auto_envvar_prefix: t.Optional[str] = None,
|
||||
default_map: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
terminal_width: t.Optional[int] = None,
|
||||
max_content_width: t.Optional[int] = None,
|
||||
resilient_parsing: bool = False,
|
||||
allow_extra_args: t.Optional[bool] = None,
|
||||
allow_interspersed_args: t.Optional[bool] = None,
|
||||
ignore_unknown_options: t.Optional[bool] = None,
|
||||
help_option_names: t.Optional[t.List[str]] = None,
|
||||
token_normalize_func: t.Optional[t.Callable[[str], str]] = None,
|
||||
color: t.Optional[bool] = None,
|
||||
show_default: t.Optional[bool] = None,
|
||||
) -> None:
|
||||
#: the parent context or `None` if none exists.
|
||||
self.parent = parent
|
||||
#: the :class:`Command` for this context.
|
||||
self.command = command
|
||||
#: the descriptive information name
|
||||
self.info_name = info_name
|
||||
#: the parsed parameters except if the value is hidden in which
|
||||
#: case it's not remembered.
|
||||
self.params = {}
|
||||
#: Map of parameter names to their parsed values. Parameters
|
||||
#: with ``expose_value=False`` are not stored.
|
||||
self.params: t.Dict[str, t.Any] = {}
|
||||
#: the leftover arguments.
|
||||
self.args = []
|
||||
self.args: t.List[str] = []
|
||||
#: protected arguments. These are arguments that are prepended
|
||||
#: to `args` when certain parsing scenarios are encountered but
|
||||
#: must be never propagated to another arguments. This is used
|
||||
#: to implement nested parsing.
|
||||
self.protected_args = []
|
||||
self.protected_args: t.List[str] = []
|
||||
|
||||
if obj is None and parent is not None:
|
||||
obj = parent.obj
|
||||
|
||||
#: the user object stored.
|
||||
self.obj = obj
|
||||
self._meta = getattr(parent, "meta", {})
|
||||
self.obj: t.Any = obj
|
||||
self._meta: t.Dict[str, t.Any] = getattr(parent, "meta", {})
|
||||
|
||||
#: A dictionary (-like object) with defaults for parameters.
|
||||
if (
|
||||
default_map is None
|
||||
and info_name is not None
|
||||
and parent is not None
|
||||
and parent.default_map is not None
|
||||
):
|
||||
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
|
||||
#: 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
|
||||
#: any commands are executed. It is however not possible to
|
||||
#: figure out which ones. If you require this knowledge you
|
||||
#: should use a :func:`resultcallback`.
|
||||
self.invoked_subcommand = None
|
||||
#: should use a :func:`result_callback`.
|
||||
self.invoked_subcommand: t.Optional[str] = None
|
||||
|
||||
if terminal_width is None and parent is not None:
|
||||
terminal_width = parent.terminal_width
|
||||
|
||||
#: 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:
|
||||
max_content_width = parent.max_content_width
|
||||
|
||||
#: The maximum width of formatted content (None implies a sensible
|
||||
#: 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:
|
||||
allow_extra_args = command.allow_extra_args
|
||||
|
||||
#: Indicates if the context allows extra args or if it should
|
||||
#: fail on parsing.
|
||||
#:
|
||||
|
@ -325,14 +343,16 @@ class Context(object):
|
|||
|
||||
if allow_interspersed_args is None:
|
||||
allow_interspersed_args = command.allow_interspersed_args
|
||||
|
||||
#: Indicates if the context allows mixing of arguments and
|
||||
#: options or not.
|
||||
#:
|
||||
#: .. versionadded:: 3.0
|
||||
self.allow_interspersed_args = allow_interspersed_args
|
||||
self.allow_interspersed_args: bool = allow_interspersed_args
|
||||
|
||||
if ignore_unknown_options is None:
|
||||
ignore_unknown_options = command.ignore_unknown_options
|
||||
|
||||
#: Instructs click to ignore options that a command does not
|
||||
#: understand and will store it on the context for later
|
||||
#: processing. This is primarily useful for situations where you
|
||||
|
@ -341,7 +361,7 @@ class Context(object):
|
|||
#: forward all arguments.
|
||||
#:
|
||||
#: .. 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 parent is not None:
|
||||
|
@ -350,19 +370,21 @@ class Context(object):
|
|||
help_option_names = ["--help"]
|
||||
|
||||
#: 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:
|
||||
token_normalize_func = parent.token_normalize_func
|
||||
|
||||
#: An optional normalization function for tokens. This is
|
||||
#: 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
|
||||
#: will do its best to not cause any failures and default values
|
||||
#: 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
|
||||
# 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 self.info_name is not None
|
||||
):
|
||||
auto_envvar_prefix = "{}_{}".format(
|
||||
parent.auto_envvar_prefix, self.info_name.upper()
|
||||
auto_envvar_prefix = (
|
||||
f"{parent.auto_envvar_prefix}_{self.info_name.upper()}"
|
||||
)
|
||||
else:
|
||||
auto_envvar_prefix = auto_envvar_prefix.upper()
|
||||
|
||||
if auto_envvar_prefix is not None:
|
||||
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:
|
||||
color = parent.color
|
||||
|
||||
#: 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._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
|
||||
push_context(self)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||
self._depth -= 1
|
||||
if self._depth == 0:
|
||||
self.close()
|
||||
pop_context()
|
||||
|
||||
@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
|
||||
it to the current thread local (see :func:`get_current_context`).
|
||||
The default behavior of this is to invoke the cleanup functions which
|
||||
|
@ -443,7 +494,7 @@ class Context(object):
|
|||
self._depth -= 1
|
||||
|
||||
@property
|
||||
def meta(self):
|
||||
def meta(self) -> t.Dict[str, t.Any]:
|
||||
"""This is a dictionary which is shared with all the contexts
|
||||
that are nested. It exists so that click utilities can store some
|
||||
state here if they need to. It is however the responsibility of
|
||||
|
@ -470,32 +521,72 @@ class Context(object):
|
|||
"""
|
||||
return self._meta
|
||||
|
||||
def make_formatter(self):
|
||||
"""Creates the formatter for the help and usage output."""
|
||||
return HelpFormatter(
|
||||
def make_formatter(self) -> HelpFormatter:
|
||||
"""Creates the :class:`~click.HelpFormatter` for the help and
|
||||
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
|
||||
)
|
||||
|
||||
def call_on_close(self, f):
|
||||
"""This decorator remembers a function as callback that should be
|
||||
executed when the context tears down. This is most useful to bind
|
||||
resource handling to the script execution. For instance, file objects
|
||||
opened by the :class:`File` type will register their close callbacks
|
||||
here.
|
||||
def with_resource(self, context_manager: t.ContextManager[V]) -> V:
|
||||
"""Register a resource as if it were used in a ``with``
|
||||
statement. The resource will be cleaned up when the context is
|
||||
popped.
|
||||
|
||||
: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 f
|
||||
return self._exit_stack.enter_context(context_manager)
|
||||
|
||||
def close(self):
|
||||
"""Invokes all close callbacks."""
|
||||
for cb in self._close_callbacks:
|
||||
cb()
|
||||
self._close_callbacks = []
|
||||
def call_on_close(self, f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
||||
"""Register a function to be called when the context tears down.
|
||||
|
||||
This can be used to close resources opened during the script
|
||||
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
|
||||
def command_path(self):
|
||||
def command_path(self) -> str:
|
||||
"""The computed command path. This is used for the ``usage``
|
||||
information on the help page. It's automatically created by
|
||||
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:
|
||||
rv = self.info_name
|
||||
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()
|
||||
|
||||
def find_root(self):
|
||||
def find_root(self) -> "Context":
|
||||
"""Finds the outermost context."""
|
||||
node = self
|
||||
while node.parent is not None:
|
||||
node = node.parent
|
||||
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."""
|
||||
node = self
|
||||
node: t.Optional["Context"] = self
|
||||
|
||||
while node is not None:
|
||||
if isinstance(node.obj, object_type):
|
||||
return node.obj
|
||||
|
||||
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
|
||||
new instance of `object_type` if it does not exist.
|
||||
"""
|
||||
|
@ -531,17 +632,39 @@ class Context(object):
|
|||
self.obj = rv = object_type()
|
||||
return rv
|
||||
|
||||
def lookup_default(self, name):
|
||||
"""Looks up the default for a parameter name. This by default
|
||||
looks into the :attr:`default_map` if available.
|
||||
@typing.overload
|
||||
def lookup_default(
|
||||
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:
|
||||
rv = self.default_map.get(name)
|
||||
if callable(rv):
|
||||
rv = rv()
|
||||
return rv
|
||||
value = self.default_map.get(name)
|
||||
|
||||
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
|
||||
message.
|
||||
|
||||
|
@ -549,27 +672,40 @@ class Context(object):
|
|||
"""
|
||||
raise UsageError(message, self)
|
||||
|
||||
def abort(self):
|
||||
def abort(self) -> "te.NoReturn":
|
||||
"""Aborts the script."""
|
||||
raise Abort()
|
||||
|
||||
def exit(self, code=0):
|
||||
def exit(self, code: int = 0) -> "te.NoReturn":
|
||||
"""Exits the application with a given exit code."""
|
||||
raise Exit(code)
|
||||
|
||||
def get_usage(self):
|
||||
def get_usage(self) -> str:
|
||||
"""Helper method to get formatted usage string for the current
|
||||
context and command.
|
||||
"""
|
||||
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
|
||||
context and command.
|
||||
"""
|
||||
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
|
||||
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
|
||||
more information about this change and why it was done in a bugfix
|
||||
release see :ref:`upgrade-to-3.2`.
|
||||
"""
|
||||
self, callback = args[:2]
|
||||
ctx = self
|
||||
|
||||
# It's also possible to invoke another command which might or
|
||||
# might not have a callback. In that case we also fill
|
||||
# in defaults and make a new context for this command.
|
||||
if isinstance(callback, Command):
|
||||
other_cmd = callback
|
||||
callback = other_cmd.callback
|
||||
ctx = Context(other_cmd, info_name=other_cmd.name, parent=self)
|
||||
if callback is None:
|
||||
.. versionchanged:: 8.0
|
||||
All ``kwargs`` are tracked in :attr:`params` so they will be
|
||||
passed if :meth:`forward` is called at multiple levels.
|
||||
"""
|
||||
if isinstance(__callback, Command):
|
||||
other_cmd = __callback
|
||||
|
||||
if other_cmd.callback is None:
|
||||
raise TypeError(
|
||||
"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:
|
||||
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:]
|
||||
with augment_usage_errors(self):
|
||||
# Track all kwargs as params, so that forward() will pass
|
||||
# them on in subsequent calls.
|
||||
ctx.params.update(kwargs)
|
||||
else:
|
||||
ctx = __self
|
||||
|
||||
with augment_usage_errors(__self):
|
||||
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
|
||||
arguments from the current context if the other command expects
|
||||
it. This cannot invoke callbacks directly, only other commands.
|
||||
"""
|
||||
self, cmd = args[:2]
|
||||
|
||||
# It's also possible to invoke another command which might or
|
||||
# might not have a callback.
|
||||
if not isinstance(cmd, Command):
|
||||
.. versionchanged:: 8.0
|
||||
All ``kwargs`` are tracked in :attr:`params` so they will be
|
||||
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.")
|
||||
|
||||
for param in self.params:
|
||||
for param in __self.params:
|
||||
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.
|
||||
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
|
||||
|
@ -650,6 +822,10 @@ class BaseCommand(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.
|
||||
allow_extra_args = False
|
||||
#: 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.
|
||||
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
|
||||
#: on a :class:`Group` the group will default the command name
|
||||
#: with this information. You should instead use the
|
||||
#: :class:`Context`\'s :attr:`~Context.info_name` attribute.
|
||||
self.name = name
|
||||
|
||||
if context_settings is None:
|
||||
context_settings = {}
|
||||
|
||||
#: 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):
|
||||
return "<{} {}>".format(self.__class__.__name__, self.name)
|
||||
def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]:
|
||||
"""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")
|
||||
|
||||
def get_help(self, ctx):
|
||||
def get_help(self, ctx: Context) -> str:
|
||||
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
|
||||
off the parsing and create a new :class:`Context`. It does not
|
||||
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
|
||||
command. For the toplevel script it's usually
|
||||
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 parent: the parent context if available.
|
||||
:param extra: extra keyword arguments forwarded to the context
|
||||
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:
|
||||
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):
|
||||
self.parse_args(ctx, args)
|
||||
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
|
||||
and parses the arguments, then modifies the context as necessary.
|
||||
This is automatically invoked by :meth:`make_context`.
|
||||
"""
|
||||
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
|
||||
implementation is raising a not implemented error.
|
||||
"""
|
||||
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(
|
||||
self,
|
||||
args=None,
|
||||
prog_name=None,
|
||||
complete_var=None,
|
||||
standalone_mode=True,
|
||||
**extra
|
||||
):
|
||||
args: t.Optional[t.Sequence[str]] = None,
|
||||
prog_name: t.Optional[str] = None,
|
||||
complete_var: t.Optional[str] = None,
|
||||
standalone_mode: "te.Literal[True]" = True,
|
||||
**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
|
||||
whistles as a command line application. This will always terminate
|
||||
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
|
||||
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
|
||||
provided, ``sys.argv[1:]`` is used.
|
||||
: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
|
||||
value of this function is the return value
|
||||
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
|
||||
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
|
||||
# sane at this point or reject further execution to avoid a
|
||||
# broken script.
|
||||
if not PY2:
|
||||
_verify_python3_env()
|
||||
else:
|
||||
_check_for_unicode_literals()
|
||||
# Verify that the environment is configured correctly, or reject
|
||||
# further execution to avoid a broken script.
|
||||
_verify_python_env()
|
||||
|
||||
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:
|
||||
args = list(args)
|
||||
|
||||
if prog_name is None:
|
||||
prog_name = make_str(
|
||||
os.path.basename(sys.argv[0] if sys.argv else __file__)
|
||||
)
|
||||
prog_name = _detect_program_name()
|
||||
|
||||
# Hook for the Bash completion. This only activates if the Bash
|
||||
# completion is actually enabled, otherwise this is quite a fast
|
||||
# noop.
|
||||
_bashcomplete(self, prog_name, complete_var)
|
||||
# Process shell completion requests and exit early.
|
||||
self._main_shell_completion(extra, prog_name, complete_var)
|
||||
|
||||
try:
|
||||
try:
|
||||
|
@ -792,16 +1061,16 @@ class BaseCommand(object):
|
|||
ctx.exit()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
echo(file=sys.stderr)
|
||||
raise Abort()
|
||||
raise Abort() from None
|
||||
except ClickException as e:
|
||||
if not standalone_mode:
|
||||
raise
|
||||
e.show()
|
||||
sys.exit(e.exit_code)
|
||||
except IOError as e:
|
||||
except OSError as e:
|
||||
if e.errno == errno.EPIPE:
|
||||
sys.stdout = PacifyFlushWrapper(sys.stdout)
|
||||
sys.stderr = PacifyFlushWrapper(sys.stderr)
|
||||
sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout))
|
||||
sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr))
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise
|
||||
|
@ -821,10 +1090,38 @@ class BaseCommand(object):
|
|||
except Abort:
|
||||
if not standalone_mode:
|
||||
raise
|
||||
echo("Aborted!", file=sys.stderr)
|
||||
echo(_("Aborted!"), file=sys.stderr)
|
||||
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`."""
|
||||
return self.main(*args, **kwargs)
|
||||
|
||||
|
@ -836,6 +1133,8 @@ class Command(BaseCommand):
|
|||
|
||||
.. versionchanged:: 2.0
|
||||
Added the `context_settings` parameter.
|
||||
.. versionchanged:: 8.0
|
||||
Added repr showing the command name
|
||||
.. versionchanged:: 7.1
|
||||
Added the `no_args_is_help` parameter.
|
||||
|
||||
|
@ -864,31 +1163,33 @@ class Command(BaseCommand):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
context_settings=None,
|
||||
callback=None,
|
||||
params=None,
|
||||
help=None,
|
||||
epilog=None,
|
||||
short_help=None,
|
||||
options_metavar="[OPTIONS]",
|
||||
add_help_option=True,
|
||||
no_args_is_help=False,
|
||||
hidden=False,
|
||||
deprecated=False,
|
||||
):
|
||||
BaseCommand.__init__(self, name, context_settings)
|
||||
name: t.Optional[str],
|
||||
context_settings: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
callback: t.Optional[t.Callable[..., t.Any]] = None,
|
||||
params: t.Optional[t.List["Parameter"]] = None,
|
||||
help: t.Optional[str] = None,
|
||||
epilog: t.Optional[str] = None,
|
||||
short_help: t.Optional[str] = None,
|
||||
options_metavar: t.Optional[str] = "[OPTIONS]",
|
||||
add_help_option: bool = True,
|
||||
no_args_is_help: bool = False,
|
||||
hidden: bool = False,
|
||||
deprecated: bool = False,
|
||||
) -> None:
|
||||
super().__init__(name, context_settings)
|
||||
#: the callback to execute when the command fires. This might be
|
||||
#: `None` in which case nothing happens.
|
||||
self.callback = callback
|
||||
#: the list of parameters for this command in the order they
|
||||
#: should show up in the help page and execute. Eager parameters
|
||||
#: 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
|
||||
# text to the content preceding the first form feed
|
||||
if help and "\f" in help:
|
||||
help = help.split("\f", 1)[0]
|
||||
|
||||
self.help = help
|
||||
self.epilog = epilog
|
||||
self.options_metavar = options_metavar
|
||||
|
@ -898,7 +1199,19 @@ class Command(BaseCommand):
|
|||
self.hidden = hidden
|
||||
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.
|
||||
|
||||
Calls :meth:`format_usage` internally.
|
||||
|
@ -907,14 +1220,16 @@ class Command(BaseCommand):
|
|||
self.format_usage(ctx, formatter)
|
||||
return formatter.getvalue().rstrip("\n")
|
||||
|
||||
def get_params(self, ctx):
|
||||
def get_params(self, ctx: Context) -> t.List["Parameter"]:
|
||||
rv = self.params
|
||||
help_option = self.get_help_option(ctx)
|
||||
|
||||
if help_option is not None:
|
||||
rv = rv + [help_option]
|
||||
rv = [*rv, help_option]
|
||||
|
||||
return rv
|
||||
|
||||
def format_usage(self, ctx, formatter):
|
||||
def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None:
|
||||
"""Writes the usage line into the formatter.
|
||||
|
||||
This is a low-level method called by :meth:`get_usage`.
|
||||
|
@ -922,30 +1237,33 @@ class Command(BaseCommand):
|
|||
pieces = self.collect_usage_pieces(ctx)
|
||||
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
|
||||
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):
|
||||
rv.extend(param.get_usage_pieces(ctx))
|
||||
|
||||
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."""
|
||||
all_names = set(ctx.help_option_names)
|
||||
for param in self.params:
|
||||
all_names.difference_update(param.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."""
|
||||
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:
|
||||
echo(ctx.get_help(), color=ctx.color)
|
||||
ctx.exit()
|
||||
|
@ -956,17 +1274,17 @@ class Command(BaseCommand):
|
|||
is_eager=True,
|
||||
expose_value=False,
|
||||
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."""
|
||||
parser = OptionParser(ctx)
|
||||
for param in self.get_params(ctx):
|
||||
param.add_to_parser(parser, ctx)
|
||||
return parser
|
||||
|
||||
def get_help(self, ctx):
|
||||
def get_help(self, ctx: Context) -> str:
|
||||
"""Formats the help into a string and returns it.
|
||||
|
||||
Calls :meth:`format_help` internally.
|
||||
|
@ -975,18 +1293,21 @@ class Command(BaseCommand):
|
|||
self.format_help(ctx, formatter)
|
||||
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
|
||||
long help string.
|
||||
"""
|
||||
return (
|
||||
self.short_help
|
||||
or self.help
|
||||
and make_default_short_help(self.help, limit)
|
||||
or ""
|
||||
)
|
||||
text = self.short_help 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.
|
||||
|
||||
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_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."""
|
||||
if self.help:
|
||||
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)
|
||||
text = self.help or ""
|
||||
|
||||
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."""
|
||||
opts = []
|
||||
for param in self.get_params(ctx):
|
||||
|
@ -1026,17 +1346,17 @@ class Command(BaseCommand):
|
|||
opts.append(rv)
|
||||
|
||||
if opts:
|
||||
with formatter.section("Options"):
|
||||
with formatter.section(_("Options")):
|
||||
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."""
|
||||
if self.epilog:
|
||||
formatter.write_paragraph()
|
||||
with formatter.indentation():
|
||||
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:
|
||||
echo(ctx.get_help(), color=ctx.color)
|
||||
ctx.exit()
|
||||
|
@ -1049,22 +1369,64 @@ class Command(BaseCommand):
|
|||
|
||||
if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
|
||||
ctx.fail(
|
||||
"Got unexpected extra argument{} ({})".format(
|
||||
"s" if len(args) != 1 else "", " ".join(map(make_str, args))
|
||||
)
|
||||
ngettext(
|
||||
"Got unexpected extra argument ({args})",
|
||||
"Got unexpected extra arguments ({args})",
|
||||
len(args),
|
||||
).format(args=" ".join(map(str, args)))
|
||||
)
|
||||
|
||||
ctx.args = 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)
|
||||
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:
|
||||
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):
|
||||
"""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
|
||||
they cannot have optional arguments but it allows
|
||||
multiple commands to be chained together.
|
||||
:param result_callback: the result callback to attach to this multi
|
||||
command.
|
||||
:param result_callback: The result callback to attach to this multi
|
||||
command. This can be set or changed later with the
|
||||
:meth:`result_callback` decorator.
|
||||
"""
|
||||
|
||||
allow_extra_args = True
|
||||
|
@ -1095,29 +1458,33 @@ class MultiCommand(Command):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
name=None,
|
||||
invoke_without_command=False,
|
||||
no_args_is_help=None,
|
||||
subcommand_metavar=None,
|
||||
chain=False,
|
||||
result_callback=None,
|
||||
**attrs
|
||||
):
|
||||
Command.__init__(self, name, **attrs)
|
||||
name: t.Optional[str] = None,
|
||||
invoke_without_command: bool = False,
|
||||
no_args_is_help: t.Optional[bool] = None,
|
||||
subcommand_metavar: t.Optional[str] = None,
|
||||
chain: bool = False,
|
||||
result_callback: t.Optional[t.Callable[..., t.Any]] = None,
|
||||
**attrs: t.Any,
|
||||
) -> None:
|
||||
super().__init__(name, **attrs)
|
||||
|
||||
if no_args_is_help is None:
|
||||
no_args_is_help = not invoke_without_command
|
||||
|
||||
self.no_args_is_help = no_args_is_help
|
||||
self.invoke_without_command = invoke_without_command
|
||||
|
||||
if subcommand_metavar is None:
|
||||
if chain:
|
||||
subcommand_metavar = SUBCOMMANDS_METAVAR
|
||||
subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
|
||||
else:
|
||||
subcommand_metavar = SUBCOMMAND_METAVAR
|
||||
subcommand_metavar = "COMMAND [ARGS]..."
|
||||
|
||||
self.subcommand_metavar = subcommand_metavar
|
||||
self.chain = chain
|
||||
#: The result callback that is stored. This can be set or
|
||||
#: overridden with the :func:`resultcallback` decorator.
|
||||
self.result_callback = result_callback
|
||||
# The result callback that is stored. This can be set or
|
||||
# overridden with the :func:`result_callback` decorator.
|
||||
self._result_callback = result_callback
|
||||
|
||||
if self.chain:
|
||||
for param in self.params:
|
||||
|
@ -1127,17 +1494,35 @@ class MultiCommand(Command):
|
|||
" optional arguments."
|
||||
)
|
||||
|
||||
def collect_usage_pieces(self, ctx):
|
||||
rv = Command.collect_usage_pieces(self, ctx)
|
||||
def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]:
|
||||
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)
|
||||
return rv
|
||||
|
||||
def format_options(self, ctx, formatter):
|
||||
Command.format_options(self, ctx, formatter)
|
||||
def format_options(self, ctx: Context, formatter: HelpFormatter) -> None:
|
||||
super().format_options(ctx, formatter)
|
||||
self.format_commands(ctx, formatter)
|
||||
|
||||
def resultcallback(self, replace=False):
|
||||
"""Adds a result callback to the chain command. By default if a
|
||||
def result_callback(self, replace: bool = False) -> t.Callable[[F], F]:
|
||||
"""Adds a result callback to the command. By default if a
|
||||
result callback is already registered this will chain them but
|
||||
this can be disabled with the `replace` parameter. The result
|
||||
callback is invoked with the return value of the subcommand
|
||||
|
@ -1152,31 +1537,47 @@ class MultiCommand(Command):
|
|||
def cli(input):
|
||||
return 42
|
||||
|
||||
@cli.resultcallback()
|
||||
@cli.result_callback()
|
||||
def process_result(result, input):
|
||||
return result + input
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
:param replace: if set to `True` an already existing result
|
||||
callback will be removed.
|
||||
|
||||
.. versionchanged:: 8.0
|
||||
Renamed from ``resultcallback``.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
old_callback = self.result_callback
|
||||
def decorator(f: F) -> F:
|
||||
old_callback = self._result_callback
|
||||
|
||||
if old_callback is None or replace:
|
||||
self.result_callback = f
|
||||
self._result_callback = f
|
||||
return f
|
||||
|
||||
def function(__value, *args, **kwargs):
|
||||
return f(old_callback(__value, *args, **kwargs), *args, **kwargs)
|
||||
def function(__value, *args, **kwargs): # type: ignore
|
||||
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 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
|
||||
after the options.
|
||||
"""
|
||||
|
@ -1201,15 +1602,16 @@ class MultiCommand(Command):
|
|||
rows.append((subcommand, help))
|
||||
|
||||
if rows:
|
||||
with formatter.section("Commands"):
|
||||
with formatter.section(_("Commands")):
|
||||
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:
|
||||
echo(ctx.get_help(), color=ctx.color)
|
||||
ctx.exit()
|
||||
|
||||
rest = Command.parse_args(self, ctx, args)
|
||||
rest = super().parse_args(ctx, args)
|
||||
|
||||
if self.chain:
|
||||
ctx.protected_args = rest
|
||||
ctx.args = []
|
||||
|
@ -1218,29 +1620,24 @@ class MultiCommand(Command):
|
|||
|
||||
return ctx.args
|
||||
|
||||
def invoke(self, ctx):
|
||||
def _process_result(value):
|
||||
if self.result_callback is not None:
|
||||
value = ctx.invoke(self.result_callback, value, **ctx.params)
|
||||
def invoke(self, ctx: Context) -> t.Any:
|
||||
def _process_result(value: t.Any) -> t.Any:
|
||||
if self._result_callback is not None:
|
||||
value = ctx.invoke(self._result_callback, value, **ctx.params)
|
||||
return value
|
||||
|
||||
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 not self.chain:
|
||||
return Command.invoke(self, ctx)
|
||||
# No subcommand was invoked, so the result callback is
|
||||
# invoked with None for regular groups, or an empty list
|
||||
# for chained groups.
|
||||
with ctx:
|
||||
Command.invoke(self, ctx)
|
||||
return _process_result([])
|
||||
ctx.fail("Missing command.")
|
||||
super().invoke(ctx)
|
||||
return _process_result([] if self.chain else None)
|
||||
ctx.fail(_("Missing command."))
|
||||
|
||||
# Fetch args back out
|
||||
args = ctx.protected_args + ctx.args
|
||||
args = [*ctx.protected_args, *ctx.args]
|
||||
ctx.args = []
|
||||
ctx.protected_args = []
|
||||
|
||||
|
@ -1252,8 +1649,9 @@ class MultiCommand(Command):
|
|||
# resources until the result processor has worked.
|
||||
with ctx:
|
||||
cmd_name, cmd, args = self.resolve_command(ctx, args)
|
||||
assert cmd is not None
|
||||
ctx.invoked_subcommand = cmd_name
|
||||
Command.invoke(self, ctx)
|
||||
super().invoke(ctx)
|
||||
sub_ctx = cmd.make_context(cmd_name, args, parent=ctx)
|
||||
with sub_ctx:
|
||||
return _process_result(sub_ctx.command.invoke(sub_ctx))
|
||||
|
@ -1265,7 +1663,7 @@ class MultiCommand(Command):
|
|||
# but nothing else.
|
||||
with ctx:
|
||||
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
|
||||
# chain. In that case the return value to the result processor
|
||||
|
@ -1273,6 +1671,7 @@ class MultiCommand(Command):
|
|||
contexts = []
|
||||
while args:
|
||||
cmd_name, cmd, args = self.resolve_command(ctx, args)
|
||||
assert cmd is not None
|
||||
sub_ctx = cmd.make_context(
|
||||
cmd_name,
|
||||
args,
|
||||
|
@ -1289,7 +1688,9 @@ class MultiCommand(Command):
|
|||
rv.append(sub_ctx.command.invoke(sub_ctx))
|
||||
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])
|
||||
original_cmd_name = cmd_name
|
||||
|
||||
|
@ -1311,36 +1712,94 @@ class MultiCommand(Command):
|
|||
if cmd is None and not ctx.resilient_parsing:
|
||||
if split_opt(cmd_name)[0]:
|
||||
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, cmd_name):
|
||||
def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]:
|
||||
"""Given a context and a command name, this returns a
|
||||
: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
|
||||
appear.
|
||||
"""
|
||||
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):
|
||||
"""A group allows a command to have subcommands attached. This is the
|
||||
most common way to implement nesting in Click.
|
||||
"""A group allows a command to have subcommands attached. This is
|
||||
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):
|
||||
MultiCommand.__init__(self, name, **attrs)
|
||||
#: the registered subcommands by their exported names.
|
||||
self.commands = commands or {}
|
||||
#: If set, this is used by the group's :meth:`command` decorator
|
||||
#: as the default :class:`Command` class. This is useful to make all
|
||||
#: subcommands use a custom command class.
|
||||
#:
|
||||
#: .. 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
|
||||
is not provided, the name of the command is used.
|
||||
"""
|
||||
|
@ -1350,40 +1809,65 @@ class Group(MultiCommand):
|
|||
_check_multicommand(self, name, cmd, register=True)
|
||||
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
|
||||
the group. This takes the same arguments as :func:`command` but
|
||||
immediately registers the created command with this instance by
|
||||
calling into :meth:`add_command`.
|
||||
the group. This takes the same arguments as :func:`command` and
|
||||
immediately registers the created command with this group by
|
||||
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
|
||||
|
||||
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)
|
||||
self.add_command(cmd)
|
||||
return cmd
|
||||
|
||||
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
|
||||
the group. This takes the same arguments as :func:`group` but
|
||||
immediately registers the created command with this instance by
|
||||
calling into :meth:`add_command`.
|
||||
the group. This takes the same arguments as :func:`group` and
|
||||
immediately registers the created group with this group by
|
||||
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
|
||||
|
||||
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)
|
||||
self.add_command(cmd)
|
||||
return cmd
|
||||
|
||||
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)
|
||||
|
||||
def list_commands(self, ctx):
|
||||
def list_commands(self, ctx: Context) -> t.List[str]:
|
||||
return sorted(self.commands)
|
||||
|
||||
|
||||
|
@ -1394,31 +1878,52 @@ class CommandCollection(MultiCommand):
|
|||
provides all the commands for each of them.
|
||||
"""
|
||||
|
||||
def __init__(self, name=None, sources=None, **attrs):
|
||||
MultiCommand.__init__(self, name, **attrs)
|
||||
def __init__(
|
||||
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.
|
||||
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."""
|
||||
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:
|
||||
rv = source.get_command(ctx, cmd_name)
|
||||
|
||||
if rv is not None:
|
||||
if self.chain:
|
||||
_check_multicommand(self, cmd_name, rv)
|
||||
|
||||
return rv
|
||||
|
||||
def list_commands(self, ctx):
|
||||
rv = set()
|
||||
return None
|
||||
|
||||
def list_commands(self, ctx: Context) -> t.List[str]:
|
||||
rv: t.Set[str] = set()
|
||||
|
||||
for source in self.sources:
|
||||
rv.update(source.list_commands(ctx))
|
||||
|
||||
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
|
||||
:class:`Option`\s or :class:`Argument`\s. Other subclasses are currently
|
||||
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,
|
||||
in which case it's invoked when the default is needed
|
||||
without any arguments.
|
||||
:param callback: a callback that should be executed after the parameter
|
||||
was matched. This is called as ``fn(ctx, param,
|
||||
value)`` and needs to return the value.
|
||||
:param callback: A function to further process or validate the value
|
||||
after type conversion. It is called as ``f(ctx, param, 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
|
||||
value is a tuple instead of single value. The default for
|
||||
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 expose_value: if this is `True` then the value is passed onwards
|
||||
to the command callback and stored on the context,
|
||||
|
@ -1452,6 +1959,32 @@ class Parameter(object):
|
|||
order of processing.
|
||||
:param envvar: a string or list of strings that are environment variables
|
||||
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
|
||||
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
|
||||
raise a warning to give you a chance to migrate the code easier.
|
||||
"""
|
||||
|
||||
param_type_name = "parameter"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
param_decls=None,
|
||||
type=None,
|
||||
required=False,
|
||||
default=None,
|
||||
callback=None,
|
||||
nargs=None,
|
||||
metavar=None,
|
||||
expose_value=True,
|
||||
is_eager=False,
|
||||
envvar=None,
|
||||
autocompletion=None,
|
||||
):
|
||||
param_decls: t.Optional[t.Sequence[str]] = None,
|
||||
type: t.Optional[t.Union[types.ParamType, t.Any]] = None,
|
||||
required: bool = False,
|
||||
default: t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]] = None,
|
||||
callback: t.Optional[t.Callable[[Context, "Parameter", t.Any], t.Any]] = None,
|
||||
nargs: t.Optional[int] = None,
|
||||
multiple: bool = False,
|
||||
metavar: t.Optional[str] = None,
|
||||
expose_value: bool = True,
|
||||
is_eager: bool = False,
|
||||
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(
|
||||
param_decls or (), expose_value
|
||||
)
|
||||
|
||||
self.type = convert_type(type, default)
|
||||
self.type = types.convert_type(type, default)
|
||||
|
||||
# Default nargs to what the type tells us if we have that
|
||||
# information available.
|
||||
|
@ -1496,158 +2040,355 @@ class Parameter(object):
|
|||
self.required = required
|
||||
self.callback = callback
|
||||
self.nargs = nargs
|
||||
self.multiple = False
|
||||
self.multiple = multiple
|
||||
self.expose_value = expose_value
|
||||
self.default = default
|
||||
self.is_eager = is_eager
|
||||
self.metavar = metavar
|
||||
self.envvar = envvar
|
||||
self.autocompletion = autocompletion
|
||||
|
||||
def __repr__(self):
|
||||
return "<{} {}>".format(self.__class__.__name__, self.name)
|
||||
if autocompletion is not None:
|
||||
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
|
||||
def human_readable_name(self):
|
||||
def human_readable_name(self) -> str:
|
||||
"""Returns the human readable name of this parameter. This is the
|
||||
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:
|
||||
return self.metavar
|
||||
|
||||
metavar = self.type.get_metavar(self)
|
||||
|
||||
if metavar is None:
|
||||
metavar = self.type.name.upper()
|
||||
|
||||
if self.nargs != 1:
|
||||
metavar += "..."
|
||||
|
||||
return metavar
|
||||
|
||||
def get_default(self, ctx):
|
||||
"""Given a context variable this calculates the default value."""
|
||||
# Otherwise go with the regular default.
|
||||
if callable(self.default):
|
||||
rv = self.default()
|
||||
else:
|
||||
rv = self.default
|
||||
return self.type_cast_value(ctx, rv)
|
||||
@typing.overload
|
||||
def get_default(
|
||||
self, ctx: Context, call: "te.Literal[True]" = True
|
||||
) -> t.Optional[t.Any]:
|
||||
...
|
||||
|
||||
def add_to_parser(self, parser, ctx):
|
||||
pass
|
||||
@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]]]:
|
||||
"""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:
|
||||
value = self.value_from_envvar(ctx)
|
||||
if value is None:
|
||||
value = ctx.lookup_default(self.name)
|
||||
value = self.default
|
||||
|
||||
if call and callable(value):
|
||||
value = value()
|
||||
|
||||
return value
|
||||
|
||||
def type_cast_value(self, ctx, value):
|
||||
"""Given a value this runs it properly through the type system.
|
||||
This automatically handles things like `nargs` and `multiple` as
|
||||
well as composite types.
|
||||
def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
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 self.nargs <= 1:
|
||||
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)
|
||||
if value is None:
|
||||
return () if self.multiple or self.nargs == -1 else None
|
||||
|
||||
def _convert(value, level):
|
||||
if level == 0:
|
||||
return self.type(value, self, ctx)
|
||||
return tuple(_convert(x, level - 1) for x in value or ())
|
||||
def check_iter(value: t.Any) -> t.Iterator:
|
||||
try:
|
||||
return _check_iter(value)
|
||||
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):
|
||||
"""Given a value and context this runs the logic to convert the
|
||||
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 convert(value: t.Any) -> t.Tuple:
|
||||
return tuple(self.type(x, self, ctx) for x in check_iter(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:
|
||||
return True
|
||||
|
||||
if (self.nargs != 1 or self.multiple) and value == ():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def full_process_value(self, ctx, value):
|
||||
value = self.process_value(ctx, value)
|
||||
|
||||
if value is None and not ctx.resilient_parsing:
|
||||
value = self.get_default(ctx)
|
||||
def process_value(self, ctx: Context, value: t.Any) -> t.Any:
|
||||
value = self.type_cast_value(ctx, value)
|
||||
|
||||
if self.required and self.value_is_missing(value):
|
||||
raise MissingParameter(ctx=ctx, param=self)
|
||||
|
||||
if self.callback is not None:
|
||||
value = self.callback(ctx, self, value)
|
||||
|
||||
return value
|
||||
|
||||
def resolve_envvar_value(self, ctx):
|
||||
def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]:
|
||||
if self.envvar is None:
|
||||
return
|
||||
if isinstance(self.envvar, (tuple, list)):
|
||||
for envvar in self.envvar:
|
||||
rv = os.environ.get(envvar)
|
||||
if rv is not None:
|
||||
return rv
|
||||
else:
|
||||
return None
|
||||
|
||||
if isinstance(self.envvar, str):
|
||||
rv = os.environ.get(self.envvar)
|
||||
|
||||
if rv != "":
|
||||
if 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:
|
||||
rv = self.type.split_envvar_value(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):
|
||||
value = self.consume_value(ctx, opts)
|
||||
value, source = self.consume_value(ctx, opts)
|
||||
ctx.set_parameter_source(self.name, source) # type: ignore
|
||||
|
||||
try:
|
||||
value = self.full_process_value(ctx, value)
|
||||
value = self.process_value(ctx, value)
|
||||
except Exception:
|
||||
if not ctx.resilient_parsing:
|
||||
raise
|
||||
|
||||
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:
|
||||
ctx.params[self.name] = value
|
||||
ctx.params[self.name] = value # type: ignore
|
||||
|
||||
return value, args
|
||||
|
||||
def get_help_record(self, ctx):
|
||||
def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]:
|
||||
pass
|
||||
|
||||
def get_usage_pieces(self, ctx):
|
||||
def get_usage_pieces(self, ctx: Context) -> t.List[str]:
|
||||
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
|
||||
indicate which param caused the error.
|
||||
"""
|
||||
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):
|
||||
|
@ -1666,8 +2407,12 @@ class Option(Parameter):
|
|||
: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
|
||||
option name capitalized.
|
||||
:param confirmation_prompt: if set then the value will need to be confirmed
|
||||
if it was prompted for.
|
||||
:param confirmation_prompt: Prompt a second time to confirm the
|
||||
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
|
||||
hidden from the user. This is useful for password
|
||||
input.
|
||||
|
@ -1687,106 +2432,146 @@ class Option(Parameter):
|
|||
context.
|
||||
:param help: the help string.
|
||||
:param hidden: hide this option from help outputs.
|
||||
|
||||
.. versionchanged:: 8.0.1
|
||||
``type`` is detected from ``flag_value`` if given.
|
||||
"""
|
||||
|
||||
param_type_name = "option"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
param_decls=None,
|
||||
show_default=False,
|
||||
prompt=False,
|
||||
confirmation_prompt=False,
|
||||
hide_input=False,
|
||||
is_flag=None,
|
||||
flag_value=None,
|
||||
multiple=False,
|
||||
count=False,
|
||||
allow_from_autoenv=True,
|
||||
type=None,
|
||||
help=None,
|
||||
hidden=False,
|
||||
show_choices=True,
|
||||
show_envvar=False,
|
||||
**attrs
|
||||
):
|
||||
default_is_missing = attrs.get("default", _missing) is _missing
|
||||
Parameter.__init__(self, param_decls, type=type, **attrs)
|
||||
param_decls: t.Optional[t.Sequence[str]] = None,
|
||||
show_default: t.Union[bool, str] = False,
|
||||
prompt: t.Union[bool, str] = False,
|
||||
confirmation_prompt: t.Union[bool, str] = False,
|
||||
prompt_required: bool = True,
|
||||
hide_input: bool = False,
|
||||
is_flag: t.Optional[bool] = None,
|
||||
flag_value: t.Optional[t.Any] = None,
|
||||
multiple: bool = False,
|
||||
count: bool = False,
|
||||
allow_from_autoenv: bool = True,
|
||||
type: t.Optional[t.Union[types.ParamType, t.Any]] = None,
|
||||
help: t.Optional[str] = None,
|
||||
hidden: bool = False,
|
||||
show_choices: bool = True,
|
||||
show_envvar: bool = False,
|
||||
**attrs: t.Any,
|
||||
) -> None:
|
||||
default_is_missing = "default" not in attrs
|
||||
super().__init__(param_decls, type=type, multiple=multiple, **attrs)
|
||||
|
||||
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:
|
||||
prompt_text = None
|
||||
else:
|
||||
prompt_text = prompt
|
||||
prompt_text = t.cast(str, prompt)
|
||||
|
||||
self.prompt = prompt_text
|
||||
self.confirmation_prompt = confirmation_prompt
|
||||
self.prompt_required = prompt_required
|
||||
self.hide_input = hide_input
|
||||
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 flag_value is not None:
|
||||
# Implicitly a flag because flag_value was set.
|
||||
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:
|
||||
# Implicitly a flag because flag options were given.
|
||||
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:
|
||||
self.default = False
|
||||
self.default: t.Union[t.Any, t.Callable[[], t.Any]] = False
|
||||
|
||||
if flag_value is None:
|
||||
flag_value = not self.default
|
||||
self.is_flag = is_flag
|
||||
self.flag_value = flag_value
|
||||
if self.is_flag and isinstance(self.flag_value, bool) and type in [None, bool]:
|
||||
self.type = BOOL
|
||||
self.is_bool_flag = True
|
||||
else:
|
||||
self.is_bool_flag = False
|
||||
|
||||
if is_flag and type is None:
|
||||
# Re-guess the type from the flag value instead of the
|
||||
# default.
|
||||
self.type = types.convert_type(None, flag_value)
|
||||
|
||||
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
|
||||
self.count = count
|
||||
if count:
|
||||
if type is None:
|
||||
self.type = IntRange(min=0)
|
||||
self.type = types.IntRange(min=0)
|
||||
if default_is_missing:
|
||||
self.default = 0
|
||||
|
||||
self.multiple = multiple
|
||||
self.allow_from_autoenv = allow_from_autoenv
|
||||
self.help = help
|
||||
self.show_default = show_default
|
||||
self.show_choices = show_choices
|
||||
self.show_envvar = show_envvar
|
||||
|
||||
# Sanity check for stuff we don't support
|
||||
if __debug__:
|
||||
if self.nargs < 0:
|
||||
raise TypeError("Options cannot have nargs < 0")
|
||||
if self.nargs == -1:
|
||||
raise TypeError("nargs=-1 is not supported for options.")
|
||||
|
||||
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:
|
||||
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:
|
||||
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.multiple:
|
||||
raise TypeError(
|
||||
"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."
|
||||
)
|
||||
raise TypeError("'count' is not valid with 'multiple'.")
|
||||
|
||||
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 = []
|
||||
secondary_opts = []
|
||||
name = None
|
||||
possible_names = []
|
||||
|
||||
for decl in decls:
|
||||
if isidentifier(decl):
|
||||
if decl.isidentifier():
|
||||
if name is not None:
|
||||
raise TypeError("Name defined twice")
|
||||
raise TypeError(f"Name '{name}' defined twice")
|
||||
name = decl
|
||||
else:
|
||||
split_char = ";" if decl[:1] == "/" else "/"
|
||||
|
@ -1799,6 +2584,11 @@ class Option(Parameter):
|
|||
second = second.lstrip()
|
||||
if second:
|
||||
secondary_opts.append(second.lstrip())
|
||||
if first == second:
|
||||
raise ValueError(
|
||||
f"Boolean option {decl!r} cannot use the"
|
||||
" same flag for true/false."
|
||||
)
|
||||
else:
|
||||
possible_names.append(split_opt(decl))
|
||||
opts.append(decl)
|
||||
|
@ -1806,7 +2596,7 @@ class Option(Parameter):
|
|||
if name is None and possible_names:
|
||||
possible_names.sort(key=lambda x: -len(x[0])) # group long options first
|
||||
name = possible_names[0][1].replace("-", "_").lower()
|
||||
if not isidentifier(name):
|
||||
if not name.isidentifier():
|
||||
name = None
|
||||
|
||||
if name is None:
|
||||
|
@ -1816,19 +2606,14 @@ class Option(Parameter):
|
|||
|
||||
if not opts and not secondary_opts:
|
||||
raise TypeError(
|
||||
"No options defined but a name was passed ({}). Did you"
|
||||
" mean to declare an argument instead of an option?".format(name)
|
||||
f"No options defined but a name was passed ({name})."
|
||||
" Did you mean to declare an argument instead? Did"
|
||||
f" you mean to pass '--{name}'?"
|
||||
)
|
||||
|
||||
return name, opts, secondary_opts
|
||||
|
||||
def add_to_parser(self, parser, ctx):
|
||||
kwargs = {
|
||||
"dest": self.name,
|
||||
"nargs": self.nargs,
|
||||
"obj": self,
|
||||
}
|
||||
|
||||
def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
|
||||
if self.multiple:
|
||||
action = "append"
|
||||
elif self.count:
|
||||
|
@ -1837,74 +2622,150 @@ class Option(Parameter):
|
|||
action = "store"
|
||||
|
||||
if self.is_flag:
|
||||
kwargs.pop("nargs", None)
|
||||
action_const = "{}_const".format(action)
|
||||
action = f"{action}_const"
|
||||
|
||||
if self.is_bool_flag and self.secondary_opts:
|
||||
parser.add_option(self.opts, action=action_const, const=True, **kwargs)
|
||||
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:
|
||||
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:
|
||||
kwargs["action"] = action
|
||||
parser.add_option(self.opts, **kwargs)
|
||||
parser.add_option(
|
||||
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:
|
||||
return
|
||||
any_prefix_is_slash = []
|
||||
return None
|
||||
|
||||
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)
|
||||
|
||||
if any_slashes:
|
||||
any_prefix_is_slash[:] = [True]
|
||||
any_prefix_is_slash = True
|
||||
|
||||
if not self.is_flag and not self.count:
|
||||
rv += " {}".format(self.make_metavar())
|
||||
rv += f" {self.make_metavar()}"
|
||||
|
||||
return rv
|
||||
|
||||
rv = [_write_opts(self.opts)]
|
||||
|
||||
if self.secondary_opts:
|
||||
rv.append(_write_opts(self.secondary_opts))
|
||||
|
||||
help = self.help or ""
|
||||
extra = []
|
||||
|
||||
if self.show_envvar:
|
||||
envvar = self.envvar
|
||||
|
||||
if envvar is None:
|
||||
if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None:
|
||||
envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper())
|
||||
if (
|
||||
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:
|
||||
extra.append(
|
||||
"env var: {}".format(
|
||||
", ".join(str(d) for d in envvar)
|
||||
if isinstance(envvar, (list, tuple))
|
||||
else envvar
|
||||
)
|
||||
var_str = (
|
||||
envvar
|
||||
if isinstance(envvar, str)
|
||||
else ", ".join(str(d) for d in envvar)
|
||||
)
|
||||
if self.default is not None and (self.show_default or ctx.show_default):
|
||||
if isinstance(self.show_default, string_types):
|
||||
default_string = "({})".format(self.show_default)
|
||||
elif isinstance(self.default, (list, tuple)):
|
||||
default_string = ", ".join(str(d) for d in self.default)
|
||||
elif inspect.isfunction(self.default):
|
||||
default_string = "(dynamic)"
|
||||
extra.append(_("env var: {var}").format(var=var_str))
|
||||
|
||||
# Temporarily enable resilient parsing to avoid type casting
|
||||
# failing for the default. Might be possible to extend this to
|
||||
# help formatting in general.
|
||||
resilient = ctx.resilient_parsing
|
||||
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:
|
||||
default_string = self.default
|
||||
extra.append("default: {}".format(default_string))
|
||||
default_string = str(default_value)
|
||||
|
||||
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:
|
||||
extra.append("required")
|
||||
extra.append(_("required"))
|
||||
|
||||
if extra:
|
||||
help = "{}[{}]".format(
|
||||
"{} ".format(help) if help else "", "; ".join(extra)
|
||||
)
|
||||
extra_str = "; ".join(extra)
|
||||
help = f"{help} [{extra_str}]" if help else f"[{extra_str}]"
|
||||
|
||||
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
|
||||
# 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
|
||||
|
@ -1912,16 +2773,20 @@ class Option(Parameter):
|
|||
if self.is_flag and not self.is_bool_flag:
|
||||
for param in ctx.command.params:
|
||||
if param.name == self.name and param.default:
|
||||
return param.flag_value
|
||||
return None
|
||||
return Parameter.get_default(self, ctx)
|
||||
return param.flag_value # type: ignore
|
||||
|
||||
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
|
||||
value processing if a value does not exist. It will prompt the
|
||||
user until a valid value exists and then returns the processed
|
||||
value as result.
|
||||
"""
|
||||
assert self.prompt is not None
|
||||
|
||||
# Calculate the default before prompting anything to be stable.
|
||||
default = self.get_default(ctx)
|
||||
|
||||
|
@ -1940,29 +2805,74 @@ class Option(Parameter):
|
|||
value_proc=lambda x: self.process_value(ctx, x),
|
||||
)
|
||||
|
||||
def resolve_envvar_value(self, ctx):
|
||||
rv = Parameter.resolve_envvar_value(self, ctx)
|
||||
def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]:
|
||||
rv = super().resolve_envvar_value(ctx)
|
||||
|
||||
if rv is not None:
|
||||
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):
|
||||
rv = self.resolve_envvar_value(ctx)
|
||||
if rv is None:
|
||||
return None
|
||||
value_depth = (self.nargs != 1) + bool(self.multiple)
|
||||
if value_depth > 0 and rv is not None:
|
||||
rv = self.type.split_envvar_value(rv)
|
||||
if self.multiple and self.nargs != 1:
|
||||
rv = batch(rv, self.nargs)
|
||||
if (
|
||||
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()}"
|
||||
rv = os.environ.get(envvar)
|
||||
|
||||
return rv
|
||||
|
||||
def full_process_value(self, ctx, value):
|
||||
if value is None and self.prompt is not None and not ctx.resilient_parsing:
|
||||
return self.prompt_for_value(ctx)
|
||||
return Parameter.full_process_value(self, ctx, value)
|
||||
def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]:
|
||||
rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx)
|
||||
|
||||
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):
|
||||
|
@ -1975,37 +2885,48 @@ class Argument(Parameter):
|
|||
|
||||
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 attrs.get("default") is not None:
|
||||
required = False
|
||||
else:
|
||||
required = attrs.get("nargs", 1) > 0
|
||||
Parameter.__init__(self, param_decls, required=required, **attrs)
|
||||
if self.default is not None and self.nargs < 0:
|
||||
raise TypeError(
|
||||
"nargs=-1 in combination with a default value is not supported."
|
||||
)
|
||||
|
||||
if "multiple" in attrs:
|
||||
raise TypeError("__init__() got an unexpected keyword argument 'multiple'.")
|
||||
|
||||
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
|
||||
def human_readable_name(self):
|
||||
def human_readable_name(self) -> str:
|
||||
if self.metavar is not None:
|
||||
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:
|
||||
return self.metavar
|
||||
var = self.type.get_metavar(self)
|
||||
if not var:
|
||||
var = self.name.upper()
|
||||
var = self.name.upper() # type: ignore
|
||||
if not self.required:
|
||||
var = "[{}]".format(var)
|
||||
var = f"[{var}]"
|
||||
if self.nargs != 1:
|
||||
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 expose_value:
|
||||
return None, [], []
|
||||
|
@ -2016,15 +2937,15 @@ class Argument(Parameter):
|
|||
else:
|
||||
raise TypeError(
|
||||
"Arguments take exactly one parameter declaration, got"
|
||||
" {}".format(len(decls))
|
||||
f" {len(decls)}."
|
||||
)
|
||||
return name, [arg], []
|
||||
|
||||
def get_usage_pieces(self, ctx):
|
||||
def get_usage_pieces(self, ctx: Context) -> t.List[str]:
|
||||
return [self.make_metavar()]
|
||||
|
||||
def get_error_hint(self, ctx):
|
||||
return repr(self.make_metavar())
|
||||
def get_error_hint(self, ctx: Context) -> str:
|
||||
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)
|
||||
|
|
|
@ -1,41 +1,48 @@
|
|||
import inspect
|
||||
import sys
|
||||
import types
|
||||
import typing as t
|
||||
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 Command
|
||||
from .core import Context
|
||||
from .core import Group
|
||||
from .core import Option
|
||||
from .core import Parameter
|
||||
from .globals import get_current_context
|
||||
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
|
||||
object as first argument.
|
||||
"""
|
||||
|
||||
def new_func(*args, **kwargs):
|
||||
def new_func(*args, **kwargs): # type: ignore
|
||||
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
|
||||
context onwards (:attr:`Context.obj`). This is useful if that object
|
||||
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 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
|
||||
similar to :func:`pass_obj` but instead of passing the object of the
|
||||
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.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
def new_func(*args, **kwargs):
|
||||
def decorator(f: F) -> F:
|
||||
def new_func(*args, **kwargs): # type: ignore
|
||||
ctx = get_current_context()
|
||||
|
||||
if ensure:
|
||||
obj = ctx.ensure_object(object_type)
|
||||
else:
|
||||
obj = ctx.find_object(object_type)
|
||||
|
||||
if obj is None:
|
||||
raise RuntimeError(
|
||||
"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 update_wrapper(new_func, f)
|
||||
return update_wrapper(t.cast(F, new_func), f)
|
||||
|
||||
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):
|
||||
raise TypeError("Attempted to convert a callback into a command twice.")
|
||||
|
||||
try:
|
||||
params = f.__click_params__
|
||||
params = f.__click_params__ # type: ignore
|
||||
params.reverse()
|
||||
del f.__click_params__
|
||||
del f.__click_params__ # type: ignore
|
||||
except AttributeError:
|
||||
params = []
|
||||
|
||||
help = attrs.get("help")
|
||||
|
||||
if help is None:
|
||||
help = inspect.getdoc(f)
|
||||
if isinstance(help, bytes):
|
||||
help = help.decode("utf-8")
|
||||
else:
|
||||
help = inspect.cleandoc(help)
|
||||
|
||||
attrs["help"] = help
|
||||
_check_for_unicode_literals()
|
||||
return cls(
|
||||
name=name or f.__name__.lower().replace("_", "-"),
|
||||
callback=f,
|
||||
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
|
||||
callback. This will also automatically attach all decorated
|
||||
: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:
|
||||
cls = Command
|
||||
|
||||
def decorator(f):
|
||||
cmd = _make_command(f, name, attrs, cls)
|
||||
def decorator(f: t.Callable[..., t.Any]) -> Command:
|
||||
cmd = _make_command(f, name, attrs, cls) # type: ignore
|
||||
cmd.__doc__ = f.__doc__
|
||||
return cmd
|
||||
|
||||
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
|
||||
works otherwise the same as :func:`command` just that the `cls`
|
||||
parameter is set to :class:`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):
|
||||
f.params.append(param)
|
||||
else:
|
||||
if not hasattr(f, "__click_params__"):
|
||||
f.__click_params__ = []
|
||||
f.__click_params__.append(param)
|
||||
f.__click_params__ = [] # type: ignore
|
||||
|
||||
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
|
||||
passed as parameter declarations to :class:`Argument`; all keyword
|
||||
arguments are forwarded unchanged (except ``cls``).
|
||||
|
@ -163,7 +218,7 @@ def argument(*param_decls, **attrs):
|
|||
:class:`Argument`.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
def decorator(f: FC) -> FC:
|
||||
ArgumentClass = attrs.pop("cls", Argument)
|
||||
_param_memo(f, ArgumentClass(param_decls, **attrs))
|
||||
return f
|
||||
|
@ -171,7 +226,7 @@ def argument(*param_decls, **attrs):
|
|||
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
|
||||
passed as parameter declarations to :class:`Option`; all keyword
|
||||
arguments are forwarded unchanged (except ``cls``).
|
||||
|
@ -182,7 +237,7 @@ def option(*param_decls, **attrs):
|
|||
:class:`Option`.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
def decorator(f: FC) -> FC:
|
||||
# Issue 926, copy attrs, so pre-defined options can re-use the same cls=
|
||||
option_attrs = attrs.copy()
|
||||
|
||||
|
@ -195,139 +250,187 @@ def option(*param_decls, **attrs):
|
|||
return decorator
|
||||
|
||||
|
||||
def confirmation_option(*param_decls, **attrs):
|
||||
"""Shortcut for confirmation prompts that can be ignored by passing
|
||||
``--yes`` as parameter.
|
||||
def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
|
||||
"""Add a ``--yes`` option which shows a prompt before continuing if
|
||||
not passed. If the prompt is declined, the program will exit.
|
||||
|
||||
This is equivalent to decorating a function with :func:`option` with
|
||||
the following parameters::
|
||||
|
||||
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
|
||||
:param param_decls: One or more option names. Defaults to the single
|
||||
value ``"--yes"``.
|
||||
:param kwargs: Extra arguments are passed to :func:`option`.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
def callback(ctx, param, value):
|
||||
if not value:
|
||||
ctx.abort()
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
if not value:
|
||||
ctx.abort()
|
||||
|
||||
attrs.setdefault("is_flag", True)
|
||||
attrs.setdefault("callback", callback)
|
||||
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)
|
||||
if not param_decls:
|
||||
param_decls = ("--yes",)
|
||||
|
||||
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):
|
||||
"""Shortcut for password prompts.
|
||||
def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
|
||||
"""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
|
||||
the following parameters::
|
||||
:param param_decls: One or more option names. Defaults to the single
|
||||
value ``"--password"``.
|
||||
:param kwargs: Extra arguments are passed to :func:`option`.
|
||||
"""
|
||||
if not param_decls:
|
||||
param_decls = ("--password",)
|
||||
|
||||
@click.command()
|
||||
@click.option('--password', prompt=True, confirmation_prompt=True,
|
||||
hide_input=True)
|
||||
def changeadmin(password):
|
||||
pass
|
||||
kwargs.setdefault("prompt", True)
|
||||
kwargs.setdefault("confirmation_prompt", True)
|
||||
kwargs.setdefault("hide_input", True)
|
||||
return option(*param_decls, **kwargs)
|
||||
|
||||
|
||||
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):
|
||||
attrs.setdefault("prompt", True)
|
||||
attrs.setdefault("confirmation_prompt", True)
|
||||
attrs.setdefault("hide_input", True)
|
||||
return option(*(param_decls or ("--password",)), **attrs)(f)
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
|
||||
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):
|
||||
"""Adds a ``--version`` option which immediately ends the program
|
||||
printing out the version number. This is implemented as an eager
|
||||
option that prints the version and exits the program in the callback.
|
||||
|
||||
:param version: the version number to show. If not provided Click
|
||||
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
|
||||
kwargs.setdefault("is_flag", True)
|
||||
kwargs.setdefault("expose_value", False)
|
||||
kwargs.setdefault("is_eager", True)
|
||||
kwargs.setdefault("help", _("Show this message and exit."))
|
||||
kwargs["callback"] = callback
|
||||
return option(*param_decls, **kwargs)
|
||||
|
|
|
@ -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 PY2
|
||||
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 param_hint
|
||||
|
||||
|
||||
class ClickException(Exception):
|
||||
"""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
|
||||
|
||||
def __init__(self, message):
|
||||
ctor_msg = message
|
||||
if PY2:
|
||||
if ctor_msg is not None:
|
||||
ctor_msg = ctor_msg.encode("utf-8")
|
||||
Exception.__init__(self, ctor_msg)
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
def format_message(self):
|
||||
def format_message(self) -> str:
|
||||
return self.message
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
if PY2:
|
||||
__unicode__ = __str__
|
||||
|
||||
def __str__(self):
|
||||
return self.message.encode("utf-8")
|
||||
|
||||
def show(self, file=None):
|
||||
def show(self, file: t.Optional[t.IO] = None) -> None:
|
||||
if file is None:
|
||||
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):
|
||||
|
@ -53,24 +54,32 @@ class UsageError(ClickException):
|
|||
|
||||
exit_code = 2
|
||||
|
||||
def __init__(self, message, ctx=None):
|
||||
ClickException.__init__(self, message)
|
||||
def __init__(self, message: str, ctx: t.Optional["Context"] = None) -> None:
|
||||
super().__init__(message)
|
||||
self.ctx = ctx
|
||||
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:
|
||||
file = get_text_stderr()
|
||||
color = None
|
||||
hint = ""
|
||||
if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None:
|
||||
hint = "Try '{} {}' for help.\n".format(
|
||||
self.ctx.command_path, self.ctx.help_option_names[0]
|
||||
if (
|
||||
self.ctx is not None
|
||||
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:
|
||||
color = self.ctx.color
|
||||
echo("{}\n{}".format(self.ctx.get_usage(), hint), file=file, color=color)
|
||||
echo("Error: {}".format(self.format_message()), file=file, color=color)
|
||||
echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color)
|
||||
echo(
|
||||
_("Error: {message}").format(message=self.format_message()),
|
||||
file=file,
|
||||
color=color,
|
||||
)
|
||||
|
||||
|
||||
class BadParameter(UsageError):
|
||||
|
@ -91,21 +100,28 @@ class BadParameter(UsageError):
|
|||
each item is quoted and separated.
|
||||
"""
|
||||
|
||||
def __init__(self, message, ctx=None, param=None, param_hint=None):
|
||||
UsageError.__init__(self, message, ctx)
|
||||
def __init__(
|
||||
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_hint = param_hint
|
||||
|
||||
def format_message(self):
|
||||
def format_message(self) -> str:
|
||||
if self.param_hint is not None:
|
||||
param_hint = self.param_hint
|
||||
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:
|
||||
return "Invalid value: {}".format(self.message)
|
||||
param_hint = _join_param_hints(param_hint)
|
||||
return _("Invalid value: {message}").format(message=self.message)
|
||||
|
||||
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):
|
||||
|
@ -121,19 +137,26 @@ class MissingParameter(BadParameter):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, message=None, ctx=None, param=None, param_hint=None, param_type=None
|
||||
):
|
||||
BadParameter.__init__(self, message, ctx, param, param_hint)
|
||||
self,
|
||||
message: t.Optional[str] = None,
|
||||
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
|
||||
|
||||
def format_message(self):
|
||||
def format_message(self) -> str:
|
||||
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:
|
||||
param_hint = self.param.get_error_hint(self.ctx)
|
||||
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
|
||||
else:
|
||||
param_hint = None
|
||||
|
||||
param_hint = _join_param_hints(param_hint)
|
||||
param_hint = f" {param_hint}" if param_hint else ""
|
||||
|
||||
param_type = self.param_type
|
||||
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)
|
||||
if msg_extra:
|
||||
if msg:
|
||||
msg += ". {}".format(msg_extra)
|
||||
msg += f". {msg_extra}"
|
||||
else:
|
||||
msg = msg_extra
|
||||
|
||||
return "Missing {}{}{}{}".format(
|
||||
param_type,
|
||||
" {}".format(param_hint) if param_hint else "",
|
||||
". " if msg else ".",
|
||||
msg or "",
|
||||
)
|
||||
msg = f" {msg}" if msg else ""
|
||||
|
||||
def __str__(self):
|
||||
if self.message is None:
|
||||
# Translate param_type for known types.
|
||||
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
|
||||
return "missing parameter: {}".format(param_name)
|
||||
return _("Missing parameter: {param_name}").format(param_name=param_name)
|
||||
else:
|
||||
return self.message
|
||||
|
||||
if PY2:
|
||||
__unicode__ = __str__
|
||||
|
||||
def __str__(self):
|
||||
return self.__unicode__().encode("utf-8")
|
||||
|
||||
|
||||
class NoSuchOption(UsageError):
|
||||
"""Raised if click attempted to handle an option that does not
|
||||
|
@ -176,22 +200,31 @@ class NoSuchOption(UsageError):
|
|||
.. 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:
|
||||
message = "no such option: {}".format(option_name)
|
||||
UsageError.__init__(self, message, ctx)
|
||||
message = _("No such option: {name}").format(name=option_name)
|
||||
|
||||
super().__init__(message, ctx)
|
||||
self.option_name = option_name
|
||||
self.possibilities = possibilities
|
||||
|
||||
def format_message(self):
|
||||
bits = [self.message]
|
||||
if self.possibilities:
|
||||
if len(self.possibilities) == 1:
|
||||
bits.append("Did you mean {}?".format(self.possibilities[0]))
|
||||
else:
|
||||
possibilities = sorted(self.possibilities)
|
||||
bits.append("(Possible options: {})".format(", ".join(possibilities)))
|
||||
return " ".join(bits)
|
||||
def format_message(self) -> str:
|
||||
if not self.possibilities:
|
||||
return self.message
|
||||
|
||||
possibility_str = ", ".join(sorted(self.possibilities))
|
||||
suggest = ngettext(
|
||||
"Did you mean {possibility}?",
|
||||
"(Possible options: {possibilities})",
|
||||
len(self.possibilities),
|
||||
).format(possibility=possibility_str, possibilities=possibility_str)
|
||||
return f"{self.message} {suggest}"
|
||||
|
||||
|
||||
class BadOptionUsage(UsageError):
|
||||
|
@ -204,8 +237,10 @@ class BadOptionUsage(UsageError):
|
|||
:param option_name: the name of the option being used incorrectly.
|
||||
"""
|
||||
|
||||
def __init__(self, option_name, message, ctx=None):
|
||||
UsageError.__init__(self, message, ctx)
|
||||
def __init__(
|
||||
self, option_name: str, message: str, ctx: t.Optional["Context"] = None
|
||||
) -> None:
|
||||
super().__init__(message, ctx)
|
||||
self.option_name = option_name
|
||||
|
||||
|
||||
|
@ -217,23 +252,22 @@ class BadArgumentUsage(UsageError):
|
|||
.. versionadded:: 6.0
|
||||
"""
|
||||
|
||||
def __init__(self, message, ctx=None):
|
||||
UsageError.__init__(self, message, ctx)
|
||||
|
||||
|
||||
class FileError(ClickException):
|
||||
"""Raised if a file cannot be opened."""
|
||||
|
||||
def __init__(self, filename, hint=None):
|
||||
ui_filename = filename_to_ui(filename)
|
||||
def __init__(self, filename: str, hint: t.Optional[str] = None) -> None:
|
||||
if hint is None:
|
||||
hint = "unknown error"
|
||||
ClickException.__init__(self, hint)
|
||||
self.ui_filename = ui_filename
|
||||
hint = _("unknown error")
|
||||
|
||||
super().__init__(hint)
|
||||
self.ui_filename = os.fsdecode(filename)
|
||||
self.filename = filename
|
||||
|
||||
def format_message(self):
|
||||
return "Could not open file {}: {}".format(self.ui_filename, self.message)
|
||||
def format_message(self) -> str:
|
||||
return _("Could not open file {filename!r}: {message}").format(
|
||||
filename=self.ui_filename, message=self.message
|
||||
)
|
||||
|
||||
|
||||
class Abort(RuntimeError):
|
||||
|
@ -249,5 +283,5 @@ class Exit(RuntimeError):
|
|||
|
||||
__slots__ = ("exit_code",)
|
||||
|
||||
def __init__(self, code=0):
|
||||
def __init__(self, code: int = 0) -> None:
|
||||
self.exit_code = code
|
||||
|
|
|
@ -1,30 +1,38 @@
|
|||
import typing as t
|
||||
from contextlib import contextmanager
|
||||
from gettext import gettext as _
|
||||
|
||||
from ._compat import term_len
|
||||
from .parser import split_opt
|
||||
from .termui import get_terminal_size
|
||||
|
||||
# Can force a width. This is used by the test system
|
||||
FORCED_WIDTH = None
|
||||
FORCED_WIDTH: t.Optional[int] = None
|
||||
|
||||
|
||||
def measure_table(rows):
|
||||
widths = {}
|
||||
def measure_table(rows: t.Iterable[t.Tuple[str, str]]) -> t.Tuple[int, ...]:
|
||||
widths: t.Dict[int, int] = {}
|
||||
|
||||
for row in rows:
|
||||
for idx, col in enumerate(row):
|
||||
widths[idx] = max(widths.get(idx, 0), term_len(col))
|
||||
|
||||
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:
|
||||
row = tuple(row)
|
||||
yield row + ("",) * (col_count - len(row))
|
||||
|
||||
|
||||
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
|
||||
assumes that it operates on a single paragraph of text but if the
|
||||
`preserve_paragraphs` parameter is provided it will intelligently
|
||||
|
@ -55,11 +63,11 @@ def wrap_text(
|
|||
if not preserve_paragraphs:
|
||||
return wrapper.fill(text)
|
||||
|
||||
p = []
|
||||
buf = []
|
||||
p: t.List[t.Tuple[int, bool, str]] = []
|
||||
buf: t.List[str] = []
|
||||
indent = None
|
||||
|
||||
def _flush_par():
|
||||
def _flush_par() -> None:
|
||||
if not buf:
|
||||
return
|
||||
if buf[0].strip() == "\b":
|
||||
|
@ -91,7 +99,7 @@ def wrap_text(
|
|||
return "\n\n".join(rv)
|
||||
|
||||
|
||||
class HelpFormatter(object):
|
||||
class HelpFormatter:
|
||||
"""This class helps with formatting text-based help pages. It's
|
||||
usually just needed for very special internal cases, but it's also
|
||||
exposed so that developers can write their own fancy outputs.
|
||||
|
@ -103,38 +111,51 @@ class HelpFormatter(object):
|
|||
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
|
||||
if max_width is None:
|
||||
max_width = 80
|
||||
if width is None:
|
||||
width = FORCED_WIDTH
|
||||
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.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."""
|
||||
self.buffer.append(string)
|
||||
|
||||
def indent(self):
|
||||
def indent(self) -> None:
|
||||
"""Increases the indentation."""
|
||||
self.current_indent += self.indent_increment
|
||||
|
||||
def dedent(self):
|
||||
def dedent(self) -> None:
|
||||
"""Decreases the indentation."""
|
||||
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.
|
||||
|
||||
:param prog: the program name.
|
||||
: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
|
||||
|
||||
if text_width >= (term_len(usage_prefix) + 20):
|
||||
|
@ -161,25 +182,24 @@ class HelpFormatter(object):
|
|||
|
||||
self.write("\n")
|
||||
|
||||
def write_heading(self, heading):
|
||||
def write_heading(self, heading: str) -> None:
|
||||
"""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."""
|
||||
if self.buffer:
|
||||
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
|
||||
preserves paragraphs.
|
||||
"""
|
||||
text_width = max(self.width - self.current_indent, 11)
|
||||
indent = " " * self.current_indent
|
||||
self.write(
|
||||
wrap_text(
|
||||
text,
|
||||
text_width,
|
||||
self.width,
|
||||
initial_indent=indent,
|
||||
subsequent_indent=indent,
|
||||
preserve_paragraphs=True,
|
||||
|
@ -187,7 +207,12 @@ class HelpFormatter(object):
|
|||
)
|
||||
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
|
||||
and commands are usually formatted.
|
||||
|
||||
|
@ -204,7 +229,7 @@ class HelpFormatter(object):
|
|||
first_col = min(widths[0], col_max) + col_spacing
|
||||
|
||||
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:
|
||||
self.write("\n")
|
||||
continue
|
||||
|
@ -219,23 +244,15 @@ class HelpFormatter(object):
|
|||
lines = wrapped_text.splitlines()
|
||||
|
||||
if lines:
|
||||
self.write("{}\n".format(lines[0]))
|
||||
self.write(f"{lines[0]}\n")
|
||||
|
||||
for line in lines[1:]:
|
||||
self.write(
|
||||
"{:>{w}}{}\n".format(
|
||||
"", line, w=first_col + self.current_indent
|
||||
)
|
||||
)
|
||||
|
||||
if len(lines) > 1:
|
||||
# separate long help from next option
|
||||
self.write("\n")
|
||||
self.write(f"{'':>{first_col + self.current_indent}}{line}\n")
|
||||
else:
|
||||
self.write("\n")
|
||||
|
||||
@contextmanager
|
||||
def section(self, name):
|
||||
def section(self, name: str) -> t.Iterator[None]:
|
||||
"""Helpful context manager that writes a paragraph, a heading,
|
||||
and the indents.
|
||||
|
||||
|
@ -250,7 +267,7 @@ class HelpFormatter(object):
|
|||
self.dedent()
|
||||
|
||||
@contextmanager
|
||||
def indentation(self):
|
||||
def indentation(self) -> t.Iterator[None]:
|
||||
"""A context manager that increases the indentation."""
|
||||
self.indent()
|
||||
try:
|
||||
|
@ -258,12 +275,12 @@ class HelpFormatter(object):
|
|||
finally:
|
||||
self.dedent()
|
||||
|
||||
def getvalue(self):
|
||||
def getvalue(self) -> str:
|
||||
"""Returns the buffer contents."""
|
||||
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
|
||||
way and returns them in the form ``(formatted_string,
|
||||
any_prefix_is_slash)`` where the second item in the tuple is a flag that
|
||||
|
@ -271,13 +288,14 @@ def join_options(options):
|
|||
"""
|
||||
rv = []
|
||||
any_prefix_is_slash = False
|
||||
|
||||
for opt in options:
|
||||
prefix = split_opt(opt)[0]
|
||||
|
||||
if prefix == "/":
|
||||
any_prefix_is_slash = True
|
||||
|
||||
rv.append((len(prefix), opt))
|
||||
|
||||
rv.sort(key=lambda x: x[0])
|
||||
|
||||
rv = ", ".join(x[1] for x in rv)
|
||||
return rv, any_prefix_is_slash
|
||||
return ", ".join(x[1] for x in rv), any_prefix_is_slash
|
||||
|
|
|
@ -1,9 +1,25 @@
|
|||
import typing
|
||||
import typing as t
|
||||
from threading import local
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
from .core import Context
|
||||
|
||||
_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
|
||||
access the current context object from anywhere. This is a more implicit
|
||||
alternative to the :func:`pass_context` decorator. This function is
|
||||
|
@ -19,29 +35,35 @@ def get_current_context(silent=False):
|
|||
:exc:`RuntimeError`.
|
||||
"""
|
||||
try:
|
||||
return _local.stack[-1]
|
||||
except (AttributeError, IndexError):
|
||||
return t.cast("Context", _local.stack[-1])
|
||||
except (AttributeError, IndexError) as e:
|
||||
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."""
|
||||
_local.__dict__.setdefault("stack", []).append(ctx)
|
||||
|
||||
|
||||
def pop_context():
|
||||
def pop_context() -> None:
|
||||
"""Removes the top level from the stack."""
|
||||
_local.stack.pop()
|
||||
|
||||
|
||||
def resolve_color_default(color=None):
|
||||
""""Internal helper to get the default value of the color flag. If a
|
||||
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
|
||||
value is passed it's returned unchanged, otherwise it's looked up from
|
||||
the current context.
|
||||
"""
|
||||
if color is not None:
|
||||
return color
|
||||
|
||||
ctx = get_current_context(silent=True)
|
||||
|
||||
if ctx is not None:
|
||||
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
|
||||
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 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 gettext import gettext as _
|
||||
from gettext import ngettext
|
||||
|
||||
from .exceptions import BadArgumentUsage
|
||||
from .exceptions import BadOptionUsage
|
||||
from .exceptions import NoSuchOption
|
||||
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,
|
||||
it returns a tuple with all the unpacked arguments at the first index
|
||||
and all remaining arguments as the second.
|
||||
|
@ -39,10 +60,10 @@ def _unpack_args(args, nargs_spec):
|
|||
"""
|
||||
args = deque(args)
|
||||
nargs_spec = deque(nargs_spec)
|
||||
rv = []
|
||||
spos = None
|
||||
rv: t.List[t.Union[str, t.Tuple[t.Optional[str], ...], None]] = []
|
||||
spos: t.Optional[int] = None
|
||||
|
||||
def _fetch(c):
|
||||
def _fetch(c: "te.Deque[V]") -> t.Optional[V]:
|
||||
try:
|
||||
if spos is None:
|
||||
return c.popleft()
|
||||
|
@ -53,18 +74,25 @@ def _unpack_args(args, nargs_spec):
|
|||
|
||||
while nargs_spec:
|
||||
nargs = _fetch(nargs_spec)
|
||||
|
||||
if nargs is None:
|
||||
continue
|
||||
|
||||
if nargs == 1:
|
||||
rv.append(_fetch(args))
|
||||
elif nargs > 1:
|
||||
x = [_fetch(args) for _ in range(nargs)]
|
||||
|
||||
# If we're reversed, we're pulling in the arguments in reverse,
|
||||
# so we need to turn them around.
|
||||
if spos is not None:
|
||||
x.reverse()
|
||||
|
||||
rv.append(tuple(x))
|
||||
elif nargs < 0:
|
||||
if spos is not None:
|
||||
raise TypeError("Cannot have two nargs < 0")
|
||||
|
||||
spos = len(rv)
|
||||
rv.append(None)
|
||||
|
||||
|
@ -78,13 +106,7 @@ def _unpack_args(args, nargs_spec):
|
|||
return tuple(rv), list(args)
|
||||
|
||||
|
||||
def _error_opt_args(nargs, opt):
|
||||
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):
|
||||
def split_opt(opt: str) -> t.Tuple[str, str]:
|
||||
first = opt[:1]
|
||||
if first.isalnum():
|
||||
return "", opt
|
||||
|
@ -93,34 +115,57 @@ def split_opt(opt):
|
|||
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:
|
||||
return 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):
|
||||
"""Given an argument string this attempts to split it into small parts."""
|
||||
rv = []
|
||||
for match in re.finditer(
|
||||
r"('([^'\\]*(?:\\.[^'\\]*)*)'|\"([^\"\\]*(?:\\.[^\"\\]*)*)\"|\S+)\s*",
|
||||
string,
|
||||
re.S,
|
||||
def split_arg_string(string: str) -> t.List[str]:
|
||||
"""Split an argument string as with :func:`shlex.split`, but don't
|
||||
fail if the string is incomplete. Ignores a missing closing quote or
|
||||
incomplete escape sequence and uses the partial token as-is.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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._long_opts = []
|
||||
self.prefixes = set()
|
||||
|
@ -128,7 +173,7 @@ class Option(object):
|
|||
for opt in opts:
|
||||
prefix, value = split_opt(opt)
|
||||
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])
|
||||
if len(prefix) == 1 and len(value) == 1:
|
||||
self._short_opts.append(opt)
|
||||
|
@ -146,53 +191,66 @@ class Option(object):
|
|||
self.obj = obj
|
||||
|
||||
@property
|
||||
def takes_value(self):
|
||||
def takes_value(self) -> bool:
|
||||
return self.action in ("store", "append")
|
||||
|
||||
def process(self, value, state):
|
||||
def process(self, value: str, state: "ParsingState") -> None:
|
||||
if self.action == "store":
|
||||
state.opts[self.dest] = value
|
||||
state.opts[self.dest] = value # type: ignore
|
||||
elif self.action == "store_const":
|
||||
state.opts[self.dest] = self.const
|
||||
state.opts[self.dest] = self.const # type: ignore
|
||||
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":
|
||||
state.opts.setdefault(self.dest, []).append(self.const)
|
||||
state.opts.setdefault(self.dest, []).append(self.const) # type: ignore
|
||||
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:
|
||||
raise ValueError("unknown action '{}'".format(self.action))
|
||||
raise ValueError(f"unknown action '{self.action}'")
|
||||
state.order.append(self.obj)
|
||||
|
||||
|
||||
class Argument(object):
|
||||
def __init__(self, dest, nargs=1, obj=None):
|
||||
class Argument:
|
||||
def __init__(self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1):
|
||||
self.dest = dest
|
||||
self.nargs = nargs
|
||||
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:
|
||||
assert value is not None
|
||||
holes = sum(1 for x in value if x is None)
|
||||
if holes == len(value):
|
||||
value = None
|
||||
elif holes != 0:
|
||||
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)
|
||||
|
||||
|
||||
class ParsingState(object):
|
||||
def __init__(self, rargs):
|
||||
self.opts = {}
|
||||
self.largs = []
|
||||
class ParsingState:
|
||||
def __init__(self, rargs: t.List[str]) -> None:
|
||||
self.opts: t.Dict[str, t.Any] = {}
|
||||
self.largs: t.List[str] = []
|
||||
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
|
||||
parse options and arguments. It's modelled after optparse and brings
|
||||
a similar but vastly simplified API. It should generally not be used
|
||||
|
@ -206,7 +264,7 @@ class OptionParser(object):
|
|||
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
|
||||
#: `None` for some advanced use cases.
|
||||
self.ctx = ctx
|
||||
|
@ -220,44 +278,54 @@ class OptionParser(object):
|
|||
#: second mode where it will ignore it and continue processing
|
||||
#: after shifting all the unknown options into the resulting args.
|
||||
self.ignore_unknown_options = False
|
||||
|
||||
if ctx is not None:
|
||||
self.allow_interspersed_args = ctx.allow_interspersed_args
|
||||
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
|
||||
is not inferred (unlike with optparse) and needs to be explicitly
|
||||
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
|
||||
that is returned from the parser.
|
||||
"""
|
||||
if obj is None:
|
||||
obj = dest
|
||||
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)
|
||||
for opt in option._short_opts:
|
||||
self._short_opt[opt] = option
|
||||
for opt in option._long_opts:
|
||||
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.
|
||||
|
||||
The `obj` can be used to identify the option in the order list
|
||||
that is returned from the parser.
|
||||
"""
|
||||
if obj is None:
|
||||
obj = dest
|
||||
self._args.append(Argument(dest=dest, nargs=nargs, obj=obj))
|
||||
self._args.append(Argument(obj, dest=dest, nargs=nargs))
|
||||
|
||||
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)``
|
||||
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
|
||||
|
@ -273,7 +341,7 @@ class OptionParser(object):
|
|||
raise
|
||||
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(
|
||||
state.largs + state.rargs, [x.nargs for x in self._args]
|
||||
)
|
||||
|
@ -284,7 +352,7 @@ class OptionParser(object):
|
|||
state.largs = args
|
||||
state.rargs = []
|
||||
|
||||
def _process_args_for_options(self, state):
|
||||
def _process_args_for_options(self, state: ParsingState) -> None:
|
||||
while state.rargs:
|
||||
arg = state.rargs.pop(0)
|
||||
arglen = len(arg)
|
||||
|
@ -320,9 +388,13 @@ class OptionParser(object):
|
|||
# *empty* -- still a subset of [arg0, ..., arg(i-1)], but
|
||||
# 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:
|
||||
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)
|
||||
|
||||
option = self._long_opt[opt]
|
||||
|
@ -334,31 +406,26 @@ class OptionParser(object):
|
|||
if explicit_value is not None:
|
||||
state.rargs.insert(0, explicit_value)
|
||||
|
||||
nargs = option.nargs
|
||||
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]
|
||||
value = self._get_value_from_state(opt, option, state)
|
||||
|
||||
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:
|
||||
value = None
|
||||
|
||||
option.process(value, state)
|
||||
|
||||
def _match_short_opt(self, arg, state):
|
||||
def _match_short_opt(self, arg: str, state: ParsingState) -> None:
|
||||
stop = False
|
||||
i = 1
|
||||
prefix = arg[0]
|
||||
unknown_options = []
|
||||
|
||||
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)
|
||||
i += 1
|
||||
|
||||
|
@ -374,14 +441,7 @@ class OptionParser(object):
|
|||
state.rargs.insert(0, arg[i:])
|
||||
stop = True
|
||||
|
||||
nargs = option.nargs
|
||||
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]
|
||||
value = self._get_value_from_state(opt, option, state)
|
||||
|
||||
else:
|
||||
value = None
|
||||
|
@ -396,9 +456,47 @@ class OptionParser(object):
|
|||
# to the state as new larg. This way there is basic combinatorics
|
||||
# that can be achieved while still ignoring unknown arguments.
|
||||
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
|
||||
# Long option handling happens in two parts. The first part is
|
||||
# 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
|
||||
# error.
|
||||
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:
|
||||
raise
|
||||
|
||||
state.largs.append(arg)
|
||||
|
|
|
@ -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 itertools
|
||||
import os
|
||||
import struct
|
||||
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 raw_input
|
||||
from ._compat import string_types
|
||||
from ._compat import strip_ansi
|
||||
from ._compat import text_type
|
||||
from ._compat import WIN
|
||||
from .exceptions import Abort
|
||||
from .exceptions import UsageError
|
||||
from .globals import resolve_color_default
|
||||
from .types import Choice
|
||||
from .types import convert_type
|
||||
from .types import Path
|
||||
from .types import ParamType
|
||||
from .utils import echo
|
||||
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
|
||||
# functions to customize how they work.
|
||||
visible_prompt_func = raw_input
|
||||
visible_prompt_func: t.Callable[[str], str] = input
|
||||
|
||||
_ansi_colors = {
|
||||
"black": 30,
|
||||
|
@ -48,63 +50,61 @@ _ansi_colors = {
|
|||
_ansi_reset_all = "\033[0m"
|
||||
|
||||
|
||||
def hidden_prompt_func(prompt):
|
||||
def hidden_prompt_func(prompt: str) -> str:
|
||||
import getpass
|
||||
|
||||
return getpass.getpass(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
|
||||
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:
|
||||
prompt = "{} [{}]".format(prompt, _format_default(default))
|
||||
return prompt + suffix
|
||||
prompt = f"{prompt} [{_format_default(default)}]"
|
||||
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"):
|
||||
return default.name
|
||||
return default.name # type: ignore
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def prompt(
|
||||
text,
|
||||
default=None,
|
||||
hide_input=False,
|
||||
confirmation_prompt=False,
|
||||
type=None,
|
||||
value_proc=None,
|
||||
prompt_suffix=": ",
|
||||
show_default=True,
|
||||
err=False,
|
||||
show_choices=True,
|
||||
):
|
||||
text: str,
|
||||
default: t.Optional[t.Any] = None,
|
||||
hide_input: bool = False,
|
||||
confirmation_prompt: t.Union[bool, str] = False,
|
||||
type: t.Optional[t.Union[ParamType, t.Any]] = None,
|
||||
value_proc: t.Optional[t.Callable[[str], t.Any]] = None,
|
||||
prompt_suffix: str = ": ",
|
||||
show_default: bool = True,
|
||||
err: bool = False,
|
||||
show_choices: bool = True,
|
||||
) -> t.Any:
|
||||
"""Prompts a user for input. This is a convenience function that can
|
||||
be used to prompt a user for input later.
|
||||
|
||||
If the user aborts the input by sending a interrupt signal, this
|
||||
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 default: the default value to use if no input happens. If this
|
||||
is not given it will prompt until it's aborted.
|
||||
:param hide_input: if this is set to true then the input value will
|
||||
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 value_proc: if this parameter is provided it's a function that
|
||||
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,
|
||||
show_choices is true and text is "Group by" then the
|
||||
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
|
||||
try:
|
||||
# Write the prompt separately so that we get nice
|
||||
# coloring through colorama on Windows
|
||||
echo(text, nl=False, err=err)
|
||||
return f("")
|
||||
echo(text.rstrip(" "), nl=False, err=err)
|
||||
# Echo a space to stdout to work around an issue where
|
||||
# readline causes backspace to clear the whole line.
|
||||
return f(" ")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
# getpass doesn't print a newline if the user aborts input with ^C.
|
||||
# Allegedly this behavior is inherited from getpass(3).
|
||||
# A doc bug has been filed at https://bugs.python.org/issue24711
|
||||
if hide_input:
|
||||
echo(None, err=err)
|
||||
raise Abort()
|
||||
raise Abort() from None
|
||||
|
||||
if value_proc is None:
|
||||
value_proc = convert_type(type, default)
|
||||
|
@ -142,72 +156,93 @@ def prompt(
|
|||
text, prompt_suffix, show_default, default, show_choices, type
|
||||
)
|
||||
|
||||
while 1:
|
||||
while 1:
|
||||
if confirmation_prompt:
|
||||
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)
|
||||
if value:
|
||||
break
|
||||
elif default is not None:
|
||||
if isinstance(value_proc, Path):
|
||||
# validate Path default value(exists, dir_okay etc.)
|
||||
value = default
|
||||
break
|
||||
return default
|
||||
value = default
|
||||
break
|
||||
try:
|
||||
result = value_proc(value)
|
||||
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
|
||||
if not confirmation_prompt:
|
||||
return result
|
||||
while 1:
|
||||
value2 = prompt_func("Repeat for confirmation: ")
|
||||
while True:
|
||||
confirmation_prompt = t.cast(str, confirmation_prompt)
|
||||
value2 = prompt_func(confirmation_prompt)
|
||||
if value2:
|
||||
break
|
||||
if value == value2:
|
||||
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(
|
||||
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).
|
||||
|
||||
If the user aborts the input by sending a interrupt signal this
|
||||
function will catch it and raise a :exc:`Abort` exception.
|
||||
|
||||
.. versionadded:: 4.0
|
||||
Added the `err` parameter.
|
||||
|
||||
: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
|
||||
exception by raising :exc:`Abort`.
|
||||
: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 err: if set to true the file defaults to ``stderr`` instead of
|
||||
``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(
|
||||
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:
|
||||
# Write the prompt separately so that we get nice
|
||||
# coloring through colorama on Windows
|
||||
echo(prompt, nl=False, err=err)
|
||||
value = visible_prompt_func("").lower().strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
raise Abort()
|
||||
raise Abort() from None
|
||||
if value in ("y", "yes"):
|
||||
rv = True
|
||||
elif value in ("n", "no"):
|
||||
rv = False
|
||||
elif value == "":
|
||||
elif default is not None and value == "":
|
||||
rv = default
|
||||
else:
|
||||
echo("Error: invalid input", err=err)
|
||||
echo(_("Error: invalid input"), err=err)
|
||||
continue
|
||||
break
|
||||
if abort and not rv:
|
||||
|
@ -215,54 +250,30 @@ def confirm(
|
|||
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
|
||||
``(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
|
||||
if sys.version_info >= (3, 3):
|
||||
import shutil
|
||||
import shutil
|
||||
import warnings
|
||||
|
||||
shutil_get_terminal_size = getattr(shutil, "get_terminal_size", None)
|
||||
if shutil_get_terminal_size:
|
||||
sz = shutil_get_terminal_size()
|
||||
return sz.columns, sz.lines
|
||||
|
||||
# We provide a sensible default for get_winterm_size() when being invoked
|
||||
# inside a subprocess. Without this, it would not provide a useful input.
|
||||
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])
|
||||
warnings.warn(
|
||||
"'click.get_terminal_size()' is deprecated and will be removed"
|
||||
" in Click 8.1. Use 'shutil.get_terminal_size()' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return shutil.get_terminal_size()
|
||||
|
||||
|
||||
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
|
||||
pager on stdout.
|
||||
|
||||
|
@ -277,14 +288,14 @@ def echo_via_pager(text_or_generator, color=None):
|
|||
color = resolve_color_default(color)
|
||||
|
||||
if inspect.isgeneratorfunction(text_or_generator):
|
||||
i = text_or_generator()
|
||||
elif isinstance(text_or_generator, string_types):
|
||||
i = t.cast(t.Callable[[], t.Iterable[str]], text_or_generator)()
|
||||
elif isinstance(text_or_generator, str):
|
||||
i = [text_or_generator]
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -292,21 +303,22 @@ def echo_via_pager(text_or_generator, color=None):
|
|||
|
||||
|
||||
def progressbar(
|
||||
iterable=None,
|
||||
length=None,
|
||||
label=None,
|
||||
show_eta=True,
|
||||
show_percent=None,
|
||||
show_pos=False,
|
||||
item_show_func=None,
|
||||
fill_char="#",
|
||||
empty_char="-",
|
||||
bar_template="%(label)s [%(bar)s] %(info)s",
|
||||
info_sep=" ",
|
||||
width=36,
|
||||
file=None,
|
||||
color=None,
|
||||
):
|
||||
iterable: t.Optional[t.Iterable[V]] = None,
|
||||
length: t.Optional[int] = None,
|
||||
label: t.Optional[str] = None,
|
||||
show_eta: bool = True,
|
||||
show_percent: t.Optional[bool] = None,
|
||||
show_pos: bool = False,
|
||||
item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None,
|
||||
fill_char: str = "#",
|
||||
empty_char: str = "-",
|
||||
bar_template: str = "%(label)s [%(bar)s] %(info)s",
|
||||
info_sep: str = " ",
|
||||
width: int = 36,
|
||||
file: t.Optional[t.TextIO] = None,
|
||||
color: t.Optional[bool] = None,
|
||||
update_min_steps: int = 1,
|
||||
) -> "ProgressBar[V]":
|
||||
"""This function creates an iterable context manager that can be used
|
||||
to iterate over something while showing a progress bar. It will
|
||||
either iterate over the `iterable` or `length` items (that are counted
|
||||
|
@ -346,11 +358,19 @@ def progressbar(
|
|||
process_chunk(chunk)
|
||||
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
|
||||
Added the `color` parameter. Added a `update` method to the
|
||||
progressbar object.
|
||||
with click.progressbar(
|
||||
length=total_size,
|
||||
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
|
||||
is required.
|
||||
|
@ -369,10 +389,10 @@ def progressbar(
|
|||
`False` if not.
|
||||
:param show_pos: enables or disables the absolute position display. The
|
||||
default is `False`.
|
||||
:param item_show_func: a function called with the current item which
|
||||
can return a string to show the current item
|
||||
next to the progress bar. Note that the current
|
||||
item can be `None`!
|
||||
:param item_show_func: A function called with the current item which
|
||||
can return a string to show next to the progress bar. If the
|
||||
function returns ``None`` nothing is shown. The current item can
|
||||
be ``None``, such as when entering and exiting the bar.
|
||||
:param fill_char: the character to use to show the filled part of the
|
||||
progress bar.
|
||||
: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 width: the width of the progress bar in characters, 0 means full
|
||||
terminal width
|
||||
:param file: the file to write to. If this is not a terminal then
|
||||
only the label is printed.
|
||||
:param file: The file to write to. If this is not a terminal then
|
||||
only the label is printed.
|
||||
:param color: controls if the terminal supports ANSI colors or not. The
|
||||
default is autodetection. This is only needed if ANSI
|
||||
codes are included anywhere in the progress bar output
|
||||
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
|
||||
|
||||
|
@ -409,10 +450,11 @@ def progressbar(
|
|||
label=label,
|
||||
width=width,
|
||||
color=color,
|
||||
update_min_steps=update_min_steps,
|
||||
)
|
||||
|
||||
|
||||
def clear():
|
||||
def clear() -> None:
|
||||
"""Clears the terminal screen. This will have the effect of clearing
|
||||
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.
|
||||
|
@ -421,26 +463,39 @@ def clear():
|
|||
"""
|
||||
if not isatty(sys.stdout):
|
||||
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:
|
||||
os.system("cls")
|
||||
else:
|
||||
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(
|
||||
text,
|
||||
fg=None,
|
||||
bg=None,
|
||||
bold=None,
|
||||
dim=None,
|
||||
underline=None,
|
||||
blink=None,
|
||||
reverse=None,
|
||||
reset=True,
|
||||
):
|
||||
text: t.Any,
|
||||
fg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None,
|
||||
bg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None,
|
||||
bold: t.Optional[bool] = None,
|
||||
dim: t.Optional[bool] = None,
|
||||
underline: t.Optional[bool] = None,
|
||||
overline: t.Optional[bool] = None,
|
||||
italic: t.Optional[bool] = None,
|
||||
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
|
||||
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
|
||||
|
@ -451,6 +506,7 @@ def style(
|
|||
click.echo(click.style('Hello World!', fg='green'))
|
||||
click.echo(click.style('ATTENTION!', blink=True))
|
||||
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:
|
||||
|
||||
|
@ -472,10 +528,15 @@ def style(
|
|||
* ``bright_white``
|
||||
* ``reset`` (reset the color code only)
|
||||
|
||||
.. versionadded:: 2.0
|
||||
If the terminal supports it, color may also be specified as:
|
||||
|
||||
.. versionadded:: 7.0
|
||||
Added support for bright colors.
|
||||
- An integer in the interval [0, 255]. The terminal must support
|
||||
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 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
|
||||
badly supported.
|
||||
: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 reverse: if provided this will enable or disable inverse
|
||||
rendering (foreground becomes background and the
|
||||
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
|
||||
string which means that styles do not carry over. This
|
||||
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 = []
|
||||
|
||||
if fg:
|
||||
try:
|
||||
bits.append("\033[{}m".format(_ansi_colors[fg]))
|
||||
bits.append(f"\033[{_interpret_color(fg)}m")
|
||||
except KeyError:
|
||||
raise TypeError("Unknown color '{}'".format(fg))
|
||||
raise TypeError(f"Unknown color {fg!r}") from None
|
||||
|
||||
if bg:
|
||||
try:
|
||||
bits.append("\033[{}m".format(_ansi_colors[bg] + 10))
|
||||
bits.append(f"\033[{_interpret_color(bg, 10)}m")
|
||||
except KeyError:
|
||||
raise TypeError("Unknown color '{}'".format(bg))
|
||||
raise TypeError(f"Unknown color {bg!r}") from 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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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)
|
||||
if reset:
|
||||
bits.append(_ansi_reset_all)
|
||||
return "".join(bits)
|
||||
|
||||
|
||||
def unstyle(text):
|
||||
def unstyle(text: str) -> str:
|
||||
"""Removes ANSI styling information from a string. Usually it's not
|
||||
necessary to use this function as Click's echo function will
|
||||
automatically remove styling if necessary.
|
||||
|
@ -531,7 +623,14 @@ def unstyle(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
|
||||
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
|
||||
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
|
||||
"""
|
||||
if message is not None:
|
||||
if message is not None and not isinstance(message, (bytes, bytearray)):
|
||||
message = style(message, **styles)
|
||||
|
||||
return echo(message, file=file, nl=nl, err=err, color=color)
|
||||
|
||||
|
||||
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
|
||||
(should be the full path to the executable but the regular operating
|
||||
system search path is used for finding the executable) it overrides
|
||||
|
@ -580,15 +694,16 @@ def edit(
|
|||
"""
|
||||
from ._termui_impl import Editor
|
||||
|
||||
editor = Editor(
|
||||
editor=editor, env=env, require_save=require_save, extension=extension
|
||||
)
|
||||
ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension)
|
||||
|
||||
if filename is None:
|
||||
return editor.edit(text)
|
||||
editor.edit_file(filename)
|
||||
return ed.edit(text)
|
||||
|
||||
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
|
||||
viewer application for this file type. If this is an executable, it
|
||||
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
|
||||
|
||||
: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
|
||||
application associated with the URL it will attempt to
|
||||
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
|
||||
# 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
|
||||
will always return a unicode character and under certain rare
|
||||
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
|
||||
the terminal. The default is to not show it.
|
||||
"""
|
||||
f = _getchar
|
||||
if f is None:
|
||||
global _getchar
|
||||
|
||||
if _getchar is None:
|
||||
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
|
||||
|
||||
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
|
||||
key to continue. This is similar to the Windows batch "pause"
|
||||
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
|
||||
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
|
||||
``stdout``, the same as with echo.
|
||||
"""
|
||||
if not isatty(sys.stdin) or not isatty(sys.stdout):
|
||||
return
|
||||
|
||||
if info is None:
|
||||
info = _("Press any key to continue...")
|
||||
|
||||
try:
|
||||
if info:
|
||||
echo(info, nl=False, err=err)
|
||||
|
|
|
@ -1,77 +1,117 @@
|
|||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import typing as t
|
||||
from types import TracebackType
|
||||
|
||||
from . import formatting
|
||||
from . import termui
|
||||
from . import utils
|
||||
from ._compat import iteritems
|
||||
from ._compat import PY2
|
||||
from ._compat import string_types
|
||||
from ._compat import _find_binary_reader
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from .core import BaseCommand
|
||||
|
||||
|
||||
if PY2:
|
||||
from cStringIO import StringIO
|
||||
else:
|
||||
import io
|
||||
from ._compat import _find_binary_reader
|
||||
|
||||
|
||||
class EchoingStdin(object):
|
||||
def __init__(self, input, output):
|
||||
class EchoingStdin:
|
||||
def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None:
|
||||
self._input = input
|
||||
self._output = output
|
||||
self._paused = False
|
||||
|
||||
def __getattr__(self, x):
|
||||
def __getattr__(self, x: str) -> t.Any:
|
||||
return getattr(self._input, x)
|
||||
|
||||
def _echo(self, rv):
|
||||
self._output.write(rv)
|
||||
def _echo(self, rv: bytes) -> bytes:
|
||||
if not self._paused:
|
||||
self._output.write(rv)
|
||||
|
||||
return rv
|
||||
|
||||
def read(self, n=-1):
|
||||
def read(self, n: int = -1) -> bytes:
|
||||
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))
|
||||
|
||||
def readlines(self):
|
||||
def readlines(self) -> t.List[bytes]:
|
||||
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)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
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.
|
||||
if hasattr(input, "read"):
|
||||
if PY2:
|
||||
return input
|
||||
rv = _find_binary_reader(input)
|
||||
rv = _find_binary_reader(t.cast(t.IO, input))
|
||||
|
||||
if rv is not None:
|
||||
return rv
|
||||
|
||||
raise TypeError("Could not find binary reader for input stream.")
|
||||
|
||||
if input is None:
|
||||
input = b""
|
||||
elif not isinstance(input, bytes):
|
||||
elif isinstance(input, str):
|
||||
input = input.encode(charset)
|
||||
if PY2:
|
||||
return StringIO(input)
|
||||
return io.BytesIO(input)
|
||||
|
||||
return io.BytesIO(t.cast(bytes, input))
|
||||
|
||||
|
||||
class Result(object):
|
||||
class Result:
|
||||
"""Holds the captured result of an invoked CLI script."""
|
||||
|
||||
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
|
||||
self.runner = runner
|
||||
|
@ -79,6 +119,10 @@ class Result(object):
|
|||
self.stdout_bytes = stdout_bytes
|
||||
#: The standard error as bytes, or None if not available
|
||||
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.
|
||||
self.exit_code = exit_code
|
||||
#: The exception that happened if one did.
|
||||
|
@ -87,19 +131,19 @@ class Result(object):
|
|||
self.exc_info = exc_info
|
||||
|
||||
@property
|
||||
def output(self):
|
||||
def output(self) -> str:
|
||||
"""The (standard) output as unicode string."""
|
||||
return self.stdout
|
||||
|
||||
@property
|
||||
def stdout(self):
|
||||
def stdout(self) -> str:
|
||||
"""The standard output as unicode string."""
|
||||
return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
|
||||
"\r\n", "\n"
|
||||
)
|
||||
|
||||
@property
|
||||
def stderr(self):
|
||||
def stderr(self) -> str:
|
||||
"""The standard error as unicode string."""
|
||||
if self.stderr_bytes is None:
|
||||
raise ValueError("stderr not separately captured")
|
||||
|
@ -107,21 +151,18 @@ class Result(object):
|
|||
"\r\n", "\n"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<{} {}>".format(
|
||||
type(self).__name__, repr(self.exception) if self.exception else "okay"
|
||||
)
|
||||
def __repr__(self) -> str:
|
||||
exc_str = 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
|
||||
script for unittesting purposes in a isolated environment. This only
|
||||
works in single-threaded systems without any concurrency as it changes the
|
||||
global interpreter state.
|
||||
|
||||
:param charset: the character set for the input and output data. This is
|
||||
UTF-8 by default and should not be changed currently as
|
||||
the reporting to Click only works in Python 2 properly.
|
||||
:param charset: the character set for the input and output data.
|
||||
:param env: a dictionary with environment variables for overriding.
|
||||
:param echo_stdin: if this is set to `True`, then reading from stdin writes
|
||||
to stdout. This is useful for showing examples in
|
||||
|
@ -134,22 +175,28 @@ class CliRunner(object):
|
|||
independently
|
||||
"""
|
||||
|
||||
def __init__(self, charset=None, env=None, echo_stdin=False, mix_stderr=True):
|
||||
if charset is None:
|
||||
charset = "utf-8"
|
||||
def __init__(
|
||||
self,
|
||||
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.env = env or {}
|
||||
self.echo_stdin = echo_stdin
|
||||
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
|
||||
for it. The default is the `name` attribute or ``"root"`` if not
|
||||
set.
|
||||
"""
|
||||
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."""
|
||||
rv = dict(self.env)
|
||||
if overrides:
|
||||
|
@ -157,7 +204,12 @@ class CliRunner(object):
|
|||
return rv
|
||||
|
||||
@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
|
||||
command line tool. This sets up stdin with the given input data
|
||||
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.
|
||||
|
||||
.. versionadded:: 4.0
|
||||
The ``color`` parameter was added.
|
||||
|
||||
:param input: the input stream to put into sys.stdin.
|
||||
:param env: the environment overrides as dictionary.
|
||||
:param color: whether the output should contain color codes. The
|
||||
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_stdout = sys.stdout
|
||||
|
@ -184,51 +241,68 @@ class CliRunner(object):
|
|||
|
||||
env = self.make_env(env)
|
||||
|
||||
if PY2:
|
||||
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)
|
||||
bytes_output = io.BytesIO()
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
def visible_input(prompt=None):
|
||||
@_pause_echo(echo_input) # type: ignore
|
||||
def visible_input(prompt: t.Optional[str] = None) -> str:
|
||||
sys.stdout.write(prompt or "")
|
||||
val = input.readline().rstrip("\r\n")
|
||||
sys.stdout.write("{}\n".format(val))
|
||||
val = text_input.readline().rstrip("\r\n")
|
||||
sys.stdout.write(f"{val}\n")
|
||||
sys.stdout.flush()
|
||||
return val
|
||||
|
||||
def hidden_input(prompt=None):
|
||||
sys.stdout.write("{}\n".format(prompt or ""))
|
||||
@_pause_echo(echo_input) # type: ignore
|
||||
def hidden_input(prompt: t.Optional[str] = None) -> str:
|
||||
sys.stdout.write(f"{prompt or ''}\n")
|
||||
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)
|
||||
|
||||
if echo:
|
||||
sys.stdout.write(char)
|
||||
sys.stdout.flush()
|
||||
|
||||
sys.stdout.flush()
|
||||
return char
|
||||
|
||||
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:
|
||||
return not default_color
|
||||
return not color
|
||||
|
@ -236,15 +310,15 @@ class CliRunner(object):
|
|||
old_visible_prompt_func = termui.visible_prompt_func
|
||||
old_hidden_prompt_func = termui.hidden_prompt_func
|
||||
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.hidden_prompt_func = hidden_input
|
||||
termui._getchar = _getchar
|
||||
utils.should_strip_ansi = should_strip_ansi
|
||||
utils.should_strip_ansi = should_strip_ansi # type: ignore
|
||||
|
||||
old_env = {}
|
||||
try:
|
||||
for key, value in iteritems(env):
|
||||
for key, value in env.items():
|
||||
old_env[key] = os.environ.get(key)
|
||||
if value is None:
|
||||
try:
|
||||
|
@ -253,9 +327,9 @@ class CliRunner(object):
|
|||
pass
|
||||
else:
|
||||
os.environ[key] = value
|
||||
yield (bytes_output, not self.mix_stderr and bytes_error)
|
||||
yield (bytes_output, bytes_error)
|
||||
finally:
|
||||
for key, value in iteritems(old_env):
|
||||
for key, value in old_env.items():
|
||||
if value is None:
|
||||
try:
|
||||
del os.environ[key]
|
||||
|
@ -269,19 +343,19 @@ class CliRunner(object):
|
|||
termui.visible_prompt_func = old_visible_prompt_func
|
||||
termui.hidden_prompt_func = old_hidden_prompt_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
|
||||
|
||||
def invoke(
|
||||
self,
|
||||
cli,
|
||||
args=None,
|
||||
input=None,
|
||||
env=None,
|
||||
catch_exceptions=True,
|
||||
color=False,
|
||||
**extra
|
||||
):
|
||||
cli: "BaseCommand",
|
||||
args: t.Optional[t.Union[str, t.Sequence[str]]] = None,
|
||||
input: t.Optional[t.Union[str, bytes, t.IO]] = None,
|
||||
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
|
||||
catch_exceptions: bool = True,
|
||||
color: bool = False,
|
||||
**extra: t.Any,
|
||||
) -> Result:
|
||||
"""Invokes a command in an isolated environment. The arguments are
|
||||
forwarded directly to the command line script, the `extra` keyword
|
||||
arguments are passed to the :meth:`~clickpkg.Command.main` function of
|
||||
|
@ -289,16 +363,6 @@ class CliRunner(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 args: the arguments to invoke. It may be given as an iterable
|
||||
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 color: whether the output should contain color codes. The
|
||||
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
|
||||
with self.isolation(input=input, env=env, color=color) as outstreams:
|
||||
exception = None
|
||||
return_value = None
|
||||
exception: t.Optional[BaseException] = None
|
||||
exit_code = 0
|
||||
|
||||
if isinstance(args, string_types):
|
||||
if isinstance(args, str):
|
||||
args = shlex.split(args)
|
||||
|
||||
try:
|
||||
|
@ -326,20 +405,23 @@ class CliRunner(object):
|
|||
prog_name = self.get_default_prog_name(cli)
|
||||
|
||||
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:
|
||||
exc_info = sys.exc_info()
|
||||
exit_code = e.code
|
||||
if exit_code is None:
|
||||
exit_code = 0
|
||||
e_code = t.cast(t.Optional[t.Union[int, t.Any]], e.code)
|
||||
|
||||
if exit_code != 0:
|
||||
if e_code is None:
|
||||
e_code = 0
|
||||
|
||||
if e_code != 0:
|
||||
exception = e
|
||||
|
||||
if not isinstance(exit_code, int):
|
||||
sys.stdout.write(str(exit_code))
|
||||
if not isinstance(e_code, int):
|
||||
sys.stdout.write(str(e_code))
|
||||
sys.stdout.write("\n")
|
||||
exit_code = 1
|
||||
e_code = 1
|
||||
|
||||
exit_code = e_code
|
||||
|
||||
except Exception as e:
|
||||
if not catch_exceptions:
|
||||
|
@ -353,30 +435,45 @@ class CliRunner(object):
|
|||
if self.mix_stderr:
|
||||
stderr = None
|
||||
else:
|
||||
stderr = outstreams[1].getvalue()
|
||||
stderr = outstreams[1].getvalue() # type: ignore
|
||||
|
||||
return Result(
|
||||
runner=self,
|
||||
stdout_bytes=stdout,
|
||||
stderr_bytes=stderr,
|
||||
return_value=return_value,
|
||||
exit_code=exit_code,
|
||||
exception=exception,
|
||||
exc_info=exc_info,
|
||||
exc_info=exc_info, # type: ignore
|
||||
)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def isolated_filesystem(self):
|
||||
"""A context manager that creates a temporary folder and changes
|
||||
the current working directory to it for isolated filesystem tests.
|
||||
def isolated_filesystem(
|
||||
self, temp_dir: t.Optional[t.Union[str, os.PathLike]] = None
|
||||
) -> 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()
|
||||
t = tempfile.mkdtemp()
|
||||
t = tempfile.mkdtemp(dir=temp_dir)
|
||||
os.chdir(t)
|
||||
|
||||
try:
|
||||
yield t
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
try:
|
||||
shutil.rmtree(t)
|
||||
except (OSError, IOError): # noqa: B014
|
||||
pass
|
||||
|
||||
if temp_dir is None:
|
||||
try:
|
||||
shutil.rmtree(t)
|
||||
except OSError: # noqa: B014
|
||||
pass
|
||||
|
|
|
@ -1,37 +1,47 @@
|
|||
import os
|
||||
import stat
|
||||
import typing as t
|
||||
from datetime import datetime
|
||||
from gettext import gettext as _
|
||||
from gettext import ngettext
|
||||
|
||||
from ._compat import _get_argv_encoding
|
||||
from ._compat import filename_to_ui
|
||||
from ._compat import get_filesystem_encoding
|
||||
from ._compat import get_streerror
|
||||
from ._compat import open_stream
|
||||
from ._compat import PY2
|
||||
from ._compat import text_type
|
||||
from .exceptions import BadParameter
|
||||
from .utils import LazyFile
|
||||
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
|
||||
* it needs to pass through None unchanged
|
||||
* it needs to convert from a string
|
||||
* it needs to convert its result type through unchanged
|
||||
(eg: needs to be idempotent)
|
||||
* it needs to be able to deal with param and context being `None`.
|
||||
This can be the case when the object is used with prompt
|
||||
inputs.
|
||||
class ParamType:
|
||||
"""Represents the type of a parameter. Validates and converts values
|
||||
from the command line or Python into the correct type.
|
||||
|
||||
To implement a custom type, subclass and implement at least the
|
||||
following:
|
||||
|
||||
- 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
|
||||
name = None
|
||||
name: str
|
||||
|
||||
#: 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`
|
||||
|
@ -39,29 +49,66 @@ class ParamType(object):
|
|||
#: whitespace splits them up. The exception are paths and files which
|
||||
#: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
|
||||
#: 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:
|
||||
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."""
|
||||
|
||||
def get_missing_message(self, param):
|
||||
def get_missing_message(self, param: "Parameter") -> t.Optional[str]:
|
||||
"""Optionally might return extra information about a missing
|
||||
parameter.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
"""Converts the value. This is not invoked for values that are
|
||||
`None` (the missing value).
|
||||
def convert(
|
||||
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||
) -> 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
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
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."""
|
||||
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):
|
||||
is_composite = True
|
||||
|
||||
@property
|
||||
def arity(self):
|
||||
def arity(self) -> int: # type: ignore
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class FuncParamType(ParamType):
|
||||
def __init__(self, func):
|
||||
def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None:
|
||||
self.name = func.__name__
|
||||
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:
|
||||
return self.func(value)
|
||||
except ValueError:
|
||||
try:
|
||||
value = text_type(value)
|
||||
value = str(value)
|
||||
except UnicodeError:
|
||||
value = str(value).decode("utf-8", "replace")
|
||||
value = value.decode("utf-8", "replace")
|
||||
|
||||
self.fail(value, param, ctx)
|
||||
|
||||
|
||||
class UnprocessedParamType(ParamType):
|
||||
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
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "UNPROCESSED"
|
||||
|
||||
|
||||
class StringParamType(ParamType):
|
||||
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):
|
||||
enc = _get_argv_encoding()
|
||||
try:
|
||||
|
@ -128,9 +209,9 @@ class StringParamType(ParamType):
|
|||
else:
|
||||
value = value.decode("utf-8", "replace")
|
||||
return value
|
||||
return value
|
||||
return str(value)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "STRING"
|
||||
|
||||
|
||||
|
@ -153,17 +234,32 @@ class Choice(ParamType):
|
|||
|
||||
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.case_sensitive = case_sensitive
|
||||
|
||||
def get_metavar(self, param):
|
||||
return "[{}]".format("|".join(self.choices))
|
||||
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||
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):
|
||||
return "Choose from:\n\t{}.".format(",\n\t".join(self.choices))
|
||||
def get_metavar(self, param: "Parameter") -> str:
|
||||
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
|
||||
# first do token_normalize_func, then lowercase
|
||||
# preserve original `value` to produce an accurate message in
|
||||
|
@ -179,30 +275,51 @@ class Choice(ParamType):
|
|||
}
|
||||
|
||||
if not self.case_sensitive:
|
||||
if PY2:
|
||||
lower = str.lower
|
||||
else:
|
||||
lower = str.casefold
|
||||
|
||||
normed_value = lower(normed_value)
|
||||
normed_value = normed_value.casefold()
|
||||
normed_choices = {
|
||||
lower(normed_choice): original
|
||||
normed_choice.casefold(): original
|
||||
for normed_choice, original in normed_choices.items()
|
||||
}
|
||||
|
||||
if normed_value in normed_choices:
|
||||
return normed_choices[normed_value]
|
||||
|
||||
choices_str = ", ".join(map(repr, self.choices))
|
||||
self.fail(
|
||||
"invalid choice: {}. (choose from {})".format(
|
||||
value, ", ".join(self.choices)
|
||||
),
|
||||
ngettext(
|
||||
"{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,
|
||||
ctx,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "Choice('{}')".format(list(self.choices))
|
||||
def __repr__(self) -> str:
|
||||
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):
|
||||
|
@ -228,212 +345,285 @@ class DateTime(ParamType):
|
|||
|
||||
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"]
|
||||
|
||||
def get_metavar(self, param):
|
||||
return "[{}]".format("|".join(self.formats))
|
||||
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||
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:
|
||||
return datetime.strptime(value, format)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
# Exact match
|
||||
for format in self.formats:
|
||||
dtime = self._try_to_convert_date(value, format)
|
||||
if dtime:
|
||||
return dtime
|
||||
def convert(
|
||||
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||
) -> t.Any:
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
|
||||
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(
|
||||
"invalid datetime format: {}. (choose from {})".format(
|
||||
value, ", ".join(self.formats)
|
||||
)
|
||||
ngettext(
|
||||
"{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"
|
||||
|
||||
|
||||
class IntParamType(ParamType):
|
||||
name = "integer"
|
||||
class _NumberParamTypeBase(ParamType):
|
||||
_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:
|
||||
return int(value)
|
||||
return self._number_class(value)
|
||||
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"
|
||||
|
||||
|
||||
class IntRange(IntParamType):
|
||||
"""A parameter that works similar to :data:`click.INT` but restricts
|
||||
the value to fit into a range. The default behavior is to fail if the
|
||||
value falls outside the range, but it can also be silently clamped
|
||||
between the two edges.
|
||||
class IntRange(_NumberRangeBase, IntParamType):
|
||||
"""Restrict an :data:`click.INT` value to a range of accepted
|
||||
values. See :ref:`ranges`.
|
||||
|
||||
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"
|
||||
|
||||
def __init__(self, min=None, max=None, clamp=False):
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.clamp = clamp
|
||||
def _clamp( # type: ignore
|
||||
self, bound: int, dir: "te.Literal[1, -1]", open: bool
|
||||
) -> int:
|
||||
if not open:
|
||||
return bound
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
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)
|
||||
return bound + dir
|
||||
|
||||
|
||||
class FloatParamType(ParamType):
|
||||
class FloatParamType(_NumberParamTypeBase):
|
||||
name = "float"
|
||||
_number_class = float
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
self.fail(
|
||||
"{} is not a valid floating point value".format(value), param, ctx
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "FLOAT"
|
||||
|
||||
|
||||
class FloatRange(FloatParamType):
|
||||
"""A parameter that works similar to :data:`click.FLOAT` but restricts
|
||||
the value to fit into a range. The default behavior is to fail if the
|
||||
value falls outside the range, but it can also be silently clamped
|
||||
between the two edges.
|
||||
class FloatRange(_NumberRangeBase, FloatParamType):
|
||||
"""Restrict a :data:`click.FLOAT` value to a range of accepted
|
||||
values. See :ref:`ranges`.
|
||||
|
||||
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"
|
||||
|
||||
def __init__(self, min=None, max=None, clamp=False):
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.clamp = clamp
|
||||
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:
|
||||
super().__init__(
|
||||
min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp
|
||||
)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
rv = FloatParamType.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
|
||||
if (min_open or max_open) and clamp:
|
||||
raise TypeError("Clamping is not supported for open bounds.")
|
||||
|
||||
def __repr__(self):
|
||||
return "FloatRange({}, {})".format(self.min, self.max)
|
||||
def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float:
|
||||
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):
|
||||
name = "boolean"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if isinstance(value, bool):
|
||||
def convert(
|
||||
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||
) -> t.Any:
|
||||
if value in {False, True}:
|
||||
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"
|
||||
|
||||
|
||||
class UUIDParameterType(ParamType):
|
||||
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
|
||||
|
||||
if isinstance(value, uuid.UUID):
|
||||
return value
|
||||
|
||||
value = value.strip()
|
||||
|
||||
try:
|
||||
if PY2 and isinstance(value, text_type):
|
||||
value = value.encode("ascii")
|
||||
return uuid.UUID(value)
|
||||
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"
|
||||
|
||||
|
||||
|
@ -468,15 +658,25 @@ class File(ParamType):
|
|||
envvar_list_splitter = os.path.pathsep
|
||||
|
||||
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.encoding = encoding
|
||||
self.errors = errors
|
||||
self.lazy = lazy
|
||||
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:
|
||||
return self.lazy
|
||||
if value == "-":
|
||||
|
@ -485,7 +685,9 @@ class File(ParamType):
|
|||
return True
|
||||
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:
|
||||
if hasattr(value, "read") or hasattr(value, "write"):
|
||||
return value
|
||||
|
@ -493,16 +695,22 @@ class File(ParamType):
|
|||
lazy = self.resolve_lazy_flag(value)
|
||||
|
||||
if lazy:
|
||||
f = LazyFile(
|
||||
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||
f: t.IO = t.cast(
|
||||
t.IO,
|
||||
LazyFile(
|
||||
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||
),
|
||||
)
|
||||
|
||||
if ctx is not None:
|
||||
ctx.call_on_close(f.close_intelligently)
|
||||
ctx.call_on_close(f.close_intelligently) # type: ignore
|
||||
|
||||
return f
|
||||
|
||||
f, should_close = open_stream(
|
||||
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||
)
|
||||
|
||||
# If a context is provided, we automatically close the file
|
||||
# at the end of the context execution (or flush out). If a
|
||||
# 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))
|
||||
else:
|
||||
ctx.call_on_close(safecall(f.flush))
|
||||
|
||||
return f
|
||||
except (IOError, OSError) as e: # noqa: B014
|
||||
self.fail(
|
||||
"Could not open file: {}: {}".format(
|
||||
filename_to_ui(value), get_streerror(e)
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
)
|
||||
except OSError as e: # noqa: B014
|
||||
self.fail(f"{os.fsdecode(value)!r}: {e.strerror}", param, ctx)
|
||||
|
||||
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 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):
|
||||
|
@ -530,9 +749,6 @@ class Path(ParamType):
|
|||
handle it returns just the filename. Secondly, it can perform various
|
||||
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
|
||||
this value to be valid. If this is not required and a
|
||||
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.
|
||||
:param allow_dash: If this is set to `True`, a single dash to indicate
|
||||
standard streams is permitted.
|
||||
:param path_type: optionally a string type that should be used to
|
||||
represent the path. The default is `None` which
|
||||
means the return value will be either bytes or
|
||||
unicode depending on what makes most sense given the
|
||||
input data Click deals with.
|
||||
:param path_type: Convert the incoming path value to this type. If
|
||||
``None``, keep Python's default, which is ``str``. Useful to
|
||||
convert to :class:`pathlib.Path`.
|
||||
|
||||
.. versionchanged:: 8.0
|
||||
Allow passing ``type=pathlib.Path``.
|
||||
|
||||
.. versionchanged:: 6.0
|
||||
Added the ``allow_dash`` parameter.
|
||||
"""
|
||||
|
||||
envvar_list_splitter = os.path.pathsep
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
exists=False,
|
||||
file_okay=True,
|
||||
dir_okay=True,
|
||||
writable=False,
|
||||
readable=True,
|
||||
resolve_path=False,
|
||||
allow_dash=False,
|
||||
path_type=None,
|
||||
exists: bool = False,
|
||||
file_okay: bool = True,
|
||||
dir_okay: bool = True,
|
||||
writable: bool = False,
|
||||
readable: bool = True,
|
||||
resolve_path: bool = False,
|
||||
allow_dash: bool = False,
|
||||
path_type: t.Optional[t.Type] = None,
|
||||
):
|
||||
self.exists = exists
|
||||
self.file_okay = file_okay
|
||||
|
@ -578,31 +798,58 @@ class Path(ParamType):
|
|||
self.type = path_type
|
||||
|
||||
if self.file_okay and not self.dir_okay:
|
||||
self.name = "file"
|
||||
self.path_type = "File"
|
||||
self.name = _("file")
|
||||
elif self.dir_okay and not self.file_okay:
|
||||
self.name = "directory"
|
||||
self.path_type = "Directory"
|
||||
self.name = _("directory")
|
||||
else:
|
||||
self.name = "path"
|
||||
self.path_type = "Path"
|
||||
self.name = _("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 text_type:
|
||||
rv = rv.decode(get_filesystem_encoding())
|
||||
if self.type is str:
|
||||
rv = os.fsdecode(rv)
|
||||
elif self.type is bytes:
|
||||
rv = os.fsencode(rv)
|
||||
else:
|
||||
rv = rv.encode(get_filesystem_encoding())
|
||||
rv = self.type(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
|
||||
|
||||
is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
|
||||
|
||||
if not is_dash:
|
||||
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:
|
||||
st = os.stat(rv)
|
||||
|
@ -610,8 +857,8 @@ class Path(ParamType):
|
|||
if not self.exists:
|
||||
return self.coerce_path_result(rv)
|
||||
self.fail(
|
||||
"{} '{}' does not exist.".format(
|
||||
self.path_type, filename_to_ui(value)
|
||||
_("{name} {filename!r} does not exist.").format(
|
||||
name=self.name.title(), filename=os.fsdecode(value)
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
|
@ -619,30 +866,32 @@ class Path(ParamType):
|
|||
|
||||
if not self.file_okay and stat.S_ISREG(st.st_mode):
|
||||
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,
|
||||
ctx,
|
||||
)
|
||||
if not self.dir_okay and stat.S_ISDIR(st.st_mode):
|
||||
self.fail(
|
||||
"{} '{}' is a directory.".format(
|
||||
self.path_type, filename_to_ui(value)
|
||||
_("{name} {filename!r} is a directory.").format(
|
||||
name=self.name.title(), filename=os.fsdecode(value)
|
||||
),
|
||||
param,
|
||||
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(
|
||||
"{} '{}' is not writable.".format(
|
||||
self.path_type, filename_to_ui(value)
|
||||
_("{name} {filename!r} is not writable.").format(
|
||||
name=self.name.title(), filename=os.fsdecode(value)
|
||||
),
|
||||
param,
|
||||
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(
|
||||
"{} '{}' is not readable.".format(
|
||||
self.path_type, filename_to_ui(value)
|
||||
_("{name} {filename!r} is not readable.").format(
|
||||
name=self.name.title(), filename=os.fsdecode(value)
|
||||
),
|
||||
param,
|
||||
ctx,
|
||||
|
@ -650,6 +899,24 @@ class Path(ParamType):
|
|||
|
||||
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):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "<{}>".format(" ".join(ty.name for ty in self.types))
|
||||
def to_info_dict(self) -> t.Dict[str, t.Any]:
|
||||
info_dict = super().to_info_dict()
|
||||
info_dict["types"] = [t.to_info_dict() for t in self.types]
|
||||
return info_dict
|
||||
|
||||
@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)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if len(value) != len(self.types):
|
||||
raise TypeError(
|
||||
"It would appear that nargs is set to conflict with the"
|
||||
" composite type arity."
|
||||
def convert(
|
||||
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
|
||||
) -> t.Any:
|
||||
len_type = len(self.types)
|
||||
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))
|
||||
|
||||
|
||||
def convert_type(ty, default=None):
|
||||
"""Converts a callable or python type into the most appropriate
|
||||
param type.
|
||||
def convert_type(ty: t.Optional[t.Any], default: t.Optional[t.Any] = None) -> ParamType:
|
||||
"""Find the most appropriate :class:`ParamType` for the given Python
|
||||
type. If the type isn't provided, it can be inferred from a default
|
||||
value.
|
||||
"""
|
||||
guessed_type = False
|
||||
|
||||
if ty is None and default is not None:
|
||||
if isinstance(default, tuple):
|
||||
ty = tuple(map(type, default))
|
||||
if isinstance(default, (tuple, list)):
|
||||
# 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:
|
||||
ty = type(default)
|
||||
|
||||
guessed_type = True
|
||||
|
||||
if isinstance(ty, tuple):
|
||||
return Tuple(ty)
|
||||
|
||||
if isinstance(ty, ParamType):
|
||||
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
|
||||
|
||||
if ty is 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:
|
||||
return FLOAT
|
||||
|
||||
if ty is bool:
|
||||
return BOOL
|
||||
|
||||
if guessed_type:
|
||||
return STRING
|
||||
|
||||
# Catch a common mistake
|
||||
if __debug__:
|
||||
try:
|
||||
if issubclass(ty, ParamType):
|
||||
raise AssertionError(
|
||||
"Attempted to use an uninstantiated parameter type ({}).".format(ty)
|
||||
f"Attempted to use an uninstantiated parameter type ({ty})."
|
||||
)
|
||||
except TypeError:
|
||||
# ty is an instance (correct), so issubclass fails.
|
||||
pass
|
||||
|
||||
return FuncParamType(ty)
|
||||
|
||||
|
||||
#: A dummy parameter type that just does nothing. From a user's
|
||||
#: perspective this appears to just be the same as `STRING` but internally
|
||||
#: no string conversion takes place. This is necessary to achieve the
|
||||
#: same bytes/unicode behavior on Python 2/3 in situations where you want
|
||||
#: to not convert argument types. This is usually useful when working
|
||||
#: with file paths as they can appear in bytes and unicode.
|
||||
#: perspective this appears to just be the same as `STRING` but
|
||||
#: internally no string conversion takes place if the input was bytes.
|
||||
#: This is usually useful when working with file paths as they can
|
||||
#: appear in bytes and unicode.
|
||||
#:
|
||||
#: 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
|
||||
|
|
|
@ -1,86 +1,105 @@
|
|||
import os
|
||||
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_stdout
|
||||
from ._compat import _find_binary_writer
|
||||
from ._compat import auto_wrap_for_ansi
|
||||
from ._compat import binary_streams
|
||||
from ._compat import filename_to_ui
|
||||
from ._compat import get_filesystem_encoding
|
||||
from ._compat import get_streerror
|
||||
from ._compat import is_bytes
|
||||
from ._compat import open_stream
|
||||
from ._compat import PY2
|
||||
from ._compat import should_strip_ansi
|
||||
from ._compat import string_types
|
||||
from ._compat import strip_ansi
|
||||
from ._compat import text_streams
|
||||
from ._compat import text_type
|
||||
from ._compat import WIN
|
||||
from .globals import resolve_color_default
|
||||
|
||||
if not PY2:
|
||||
from ._compat import _find_binary_writer
|
||||
elif WIN:
|
||||
from ._winconsole import _get_windows_argv
|
||||
from ._winconsole import _hash_py_argv
|
||||
from ._winconsole import _initial_argv_hash
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def safecall(func):
|
||||
def safecall(func: F) -> F:
|
||||
"""Wraps a function so that it swallows exceptions."""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
def wrapper(*args, **kwargs): # type: ignore
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception:
|
||||
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."""
|
||||
if isinstance(value, bytes):
|
||||
try:
|
||||
return value.decode(get_filesystem_encoding())
|
||||
except UnicodeError:
|
||||
return value.decode("utf-8", "replace")
|
||||
return text_type(value)
|
||||
return str(value)
|
||||
|
||||
|
||||
def make_default_short_help(help, max_length=45):
|
||||
"""Return a condensed version of help string."""
|
||||
def make_default_short_help(help: str, max_length: int = 45) -> str:
|
||||
"""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()
|
||||
|
||||
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
|
||||
result = []
|
||||
done = False
|
||||
last_index = len(words) - 1
|
||||
|
||||
for word in words:
|
||||
if word[-1:] == ".":
|
||||
done = True
|
||||
new_length = 1 + len(word) if result else len(word)
|
||||
if total_length + new_length > max_length:
|
||||
result.append("...")
|
||||
done = True
|
||||
else:
|
||||
if result:
|
||||
result.append(" ")
|
||||
result.append(word)
|
||||
if done:
|
||||
for i, word in enumerate(words):
|
||||
total_length += len(word) + (i > 0)
|
||||
|
||||
if total_length > max_length: # too long, truncate
|
||||
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
|
||||
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
|
||||
|
@ -88,13 +107,19 @@ class LazyFile(object):
|
|||
"""
|
||||
|
||||
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.mode = mode
|
||||
self.encoding = encoding
|
||||
self.errors = errors
|
||||
self.atomic = atomic
|
||||
self._f: t.Optional[t.IO]
|
||||
|
||||
if filename == "-":
|
||||
self._f, self.should_close = open_stream(filename, mode, encoding, errors)
|
||||
|
@ -107,15 +132,15 @@ class LazyFile(object):
|
|||
self._f = None
|
||||
self.should_close = True
|
||||
|
||||
def __getattr__(self, name):
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
return getattr(self.open(), name)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
if self._f is not None:
|
||||
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
|
||||
a :exc:`FileError`. Not handling this error will produce an error
|
||||
that Click shows.
|
||||
|
@ -126,102 +151,100 @@ class LazyFile(object):
|
|||
rv, self.should_close = open_stream(
|
||||
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
|
||||
|
||||
raise FileError(self.name, hint=get_streerror(e))
|
||||
raise FileError(self.name, hint=e.strerror) from e
|
||||
self._f = rv
|
||||
return rv
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
"""Closes the underlying file, no matter what."""
|
||||
if self._f is not None:
|
||||
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
|
||||
file wrapper. For instance this will never close stdin.
|
||||
"""
|
||||
if self.should_close:
|
||||
self.close()
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> "LazyFile":
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||
self.close_intelligently()
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> t.Iterator[t.AnyStr]:
|
||||
self.open()
|
||||
return iter(self._f)
|
||||
return iter(self._f) # type: ignore
|
||||
|
||||
|
||||
class KeepOpenFile(object):
|
||||
def __init__(self, file):
|
||||
class KeepOpenFile:
|
||||
def __init__(self, file: t.IO) -> None:
|
||||
self._file = file
|
||||
|
||||
def __getattr__(self, name):
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
return getattr(self._file, name)
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> "KeepOpenFile":
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
def __exit__(self, exc_type, exc_value, tb): # type: ignore
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return repr(self._file)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> t.Iterator[t.AnyStr]:
|
||||
return iter(self._file)
|
||||
|
||||
|
||||
def echo(message=None, file=None, nl=True, err=False, color=None):
|
||||
"""Prints a message plus a newline to the given file or stdout. On
|
||||
first sight, this looks like the print function, but it has improved
|
||||
support for handling Unicode and binary data that does not fail no
|
||||
matter how badly configured the system is.
|
||||
def echo(
|
||||
message: t.Optional[t.Any] = None,
|
||||
file: t.Optional[t.IO] = None,
|
||||
nl: bool = True,
|
||||
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
|
||||
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.
|
||||
Compared to :func:`print`, this does the following:
|
||||
|
||||
In addition to that, if `colorama`_ is installed, the echo function will
|
||||
also support clever handling of ANSI codes. Essentially it will then
|
||||
do the following:
|
||||
- Ensures that the output encoding is not misconfigured on Linux.
|
||||
- Supports Unicode in the Windows console.
|
||||
- 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.
|
||||
- hide ANSI codes automatically if the destination file is not a
|
||||
terminal.
|
||||
|
||||
.. _colorama: https://pypi.org/project/colorama/
|
||||
:param message: The string or bytes to output. Other objects are
|
||||
converted to strings.
|
||||
:param file: The file to write to. Defaults to ``stdout``.
|
||||
:param err: Write to ``stderr`` instead of ``stdout``.
|
||||
: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
|
||||
As of Click 6.0 the echo function will properly support unicode
|
||||
output on the windows console. Not that click does not modify
|
||||
the interpreter in any way which means that `sys.stdout` or the
|
||||
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.
|
||||
Support Unicode output on the Windows console. Click does not
|
||||
modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()``
|
||||
will still not support Unicode.
|
||||
|
||||
.. versionchanged:: 4.0
|
||||
Added the `color` flag.
|
||||
Added the ``color`` parameter.
|
||||
|
||||
:param message: the message to print
|
||||
:param file: the file to write to (defaults to ``stdout``)
|
||||
:param err: if set to true the file defaults to ``stderr`` instead of
|
||||
``stdout``. This is faster and easier than calling
|
||||
:func:`get_text_stderr` yourself.
|
||||
: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.
|
||||
.. versionadded:: 3.0
|
||||
Added the ``err`` parameter.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
Support colors on Windows if colorama is installed.
|
||||
"""
|
||||
if file is None:
|
||||
if err:
|
||||
|
@ -230,70 +253,73 @@ def echo(message=None, file=None, nl=True, err=False, color=None):
|
|||
file = _default_text_stdout()
|
||||
|
||||
# Convert non bytes/text into the native string type.
|
||||
if message is not None and not isinstance(message, echo_native_types):
|
||||
message = text_type(message)
|
||||
if message is not None and not isinstance(message, (str, bytes, bytearray)):
|
||||
out: t.Optional[t.Union[str, bytes]] = str(message)
|
||||
else:
|
||||
out = message
|
||||
|
||||
if nl:
|
||||
message = message or u""
|
||||
if isinstance(message, text_type):
|
||||
message += u"\n"
|
||||
out = out or ""
|
||||
if isinstance(out, str):
|
||||
out += "\n"
|
||||
else:
|
||||
message += b"\n"
|
||||
out += b"\n"
|
||||
|
||||
# If there is a message, and we're in Python 3, and the value looks
|
||||
# like bytes, we manually 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 message and not PY2 and is_bytes(message):
|
||||
if not out:
|
||||
file.flush()
|
||||
return
|
||||
|
||||
# If there is a message and the value looks like bytes, we manually
|
||||
# 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)
|
||||
|
||||
if binary_file is not None:
|
||||
file.flush()
|
||||
binary_file.write(message)
|
||||
binary_file.write(out)
|
||||
binary_file.flush()
|
||||
return
|
||||
|
||||
# ANSI-style support. If there is no message or we are dealing with
|
||||
# bytes nothing is happening. If we are connected to a file we want
|
||||
# to strip colors. If we are on windows we either wrap the stream
|
||||
# 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):
|
||||
# ANSI style code support. For no message or bytes, nothing happens.
|
||||
# When outputting to a file instead of a terminal, strip codes.
|
||||
else:
|
||||
color = resolve_color_default(color)
|
||||
|
||||
if should_strip_ansi(file, color):
|
||||
message = strip_ansi(message)
|
||||
out = strip_ansi(out)
|
||||
elif WIN:
|
||||
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:
|
||||
message = strip_ansi(message)
|
||||
out = strip_ansi(out)
|
||||
|
||||
if message:
|
||||
file.write(message)
|
||||
file.write(out) # type: ignore
|
||||
file.flush()
|
||||
|
||||
|
||||
def get_binary_stream(name):
|
||||
"""Returns a system stream for byte processing. This essentially
|
||||
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.
|
||||
def get_binary_stream(name: "te.Literal['stdin', 'stdout', 'stderr']") -> t.BinaryIO:
|
||||
"""Returns a system stream for byte processing.
|
||||
|
||||
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
||||
``'stdout'`` and ``'stderr'``
|
||||
"""
|
||||
opener = binary_streams.get(name)
|
||||
if opener is None:
|
||||
raise TypeError("Unknown standard stream '{}'".format(name))
|
||||
raise TypeError(f"Unknown standard stream '{name}'")
|
||||
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
|
||||
a wrapped stream around a binary stream returned from
|
||||
:func:`get_binary_stream` but it also can take shortcuts on Python 3
|
||||
for already correctly configured streams.
|
||||
:func:`get_binary_stream` but it also can take shortcuts for already
|
||||
correctly configured streams.
|
||||
|
||||
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
||||
``'stdout'`` and ``'stderr'``
|
||||
|
@ -302,13 +328,18 @@ def get_text_stream(name, encoding=None, errors="strict"):
|
|||
"""
|
||||
opener = text_streams.get(name)
|
||||
if opener is None:
|
||||
raise TypeError("Unknown standard stream '{}'".format(name))
|
||||
raise TypeError(f"Unknown standard stream '{name}'")
|
||||
return opener(encoding, errors)
|
||||
|
||||
|
||||
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
|
||||
usage. Files are opened non lazy by default. This can open regular
|
||||
files as well as stdin/stdout if ``'-'`` is passed.
|
||||
|
@ -332,35 +363,35 @@ def open_file(
|
|||
moved on close.
|
||||
"""
|
||||
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)
|
||||
if not should_close:
|
||||
f = KeepOpenFile(f)
|
||||
f = t.cast(t.IO, KeepOpenFile(f))
|
||||
return f
|
||||
|
||||
|
||||
def get_os_args():
|
||||
"""This returns the argument part of sys.argv in the most appropriate
|
||||
form for processing. What this means is that this return value is in
|
||||
a format that works for Click to process but does not necessarily
|
||||
correspond well to what's actually standard for the interpreter.
|
||||
def get_os_args() -> t.Sequence[str]:
|
||||
"""Returns the argument part of ``sys.argv``, removing the first
|
||||
value which is the name of the script.
|
||||
|
||||
On most environments the return value is ``sys.argv[:1]`` unchanged.
|
||||
However if you are on Windows and running Python 2 the return value
|
||||
will actually be a list of unicode strings instead because the
|
||||
default behavior on that platform otherwise will not be able to
|
||||
carry all possible values that sys.argv can have.
|
||||
|
||||
.. versionadded:: 6.0
|
||||
.. deprecated:: 8.0
|
||||
Will be removed in Click 8.1. Access ``sys.argv[1:]`` directly
|
||||
instead.
|
||||
"""
|
||||
# We can only extract the unicode argv if sys.argv has not been
|
||||
# changed since the startup of the application.
|
||||
if PY2 and WIN and _initial_argv_hash == _hash_py_argv():
|
||||
return _get_windows_argv()
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"'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:]
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
|
@ -374,10 +405,11 @@ def format_filename(filename, shorten=False):
|
|||
"""
|
||||
if shorten:
|
||||
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
|
||||
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``
|
||||
Unix (POSIX):
|
||||
``~/.foo-bar``
|
||||
Win XP (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):
|
||||
Windows (roaming):
|
||||
``C:\Users\<user>\AppData\Roaming\Foo Bar``
|
||||
Win 7 (not roaming):
|
||||
Windows (not roaming):
|
||||
``C:\Users\<user>\AppData\Local\Foo Bar``
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
@ -419,7 +447,7 @@ def get_app_dir(app_name, roaming=True, force_posix=False):
|
|||
folder = os.path.expanduser("~")
|
||||
return os.path.join(folder, app_name)
|
||||
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":
|
||||
return os.path.join(
|
||||
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
|
||||
from ``.flush()`` being called on broken pipe during the shutdown/final-GC
|
||||
of the Python interpreter. Notably ``.flush()`` is always called on
|
||||
|
@ -439,17 +467,113 @@ class PacifyFlushWrapper(object):
|
|||
pipe, all calls and attributes are proxied.
|
||||
"""
|
||||
|
||||
def __init__(self, wrapped):
|
||||
def __init__(self, wrapped: t.IO) -> None:
|
||||
self.wrapped = wrapped
|
||||
|
||||
def flush(self):
|
||||
def flush(self) -> None:
|
||||
try:
|
||||
self.wrapped.flush()
|
||||
except IOError as e:
|
||||
except OSError as e:
|
||||
import errno
|
||||
|
||||
if e.errno != errno.EPIPE:
|
||||
raise
|
||||
|
||||
def __getattr__(self, attr):
|
||||
def __getattr__(self, attr: str) -> t.Any:
|
||||
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
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
@ -6,3 +10,22 @@ from click.testing import CliRunner
|
|||
@pytest.fixture(scope="function")
|
||||
def runner(request):
|
||||
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 pytest
|
||||
|
||||
import click
|
||||
from click._compat import PY2
|
||||
from click._compat import text_type
|
||||
|
||||
|
||||
def test_nargs_star(runner):
|
||||
|
@ -13,19 +10,19 @@ def test_nargs_star(runner):
|
|||
@click.argument("src", nargs=-1)
|
||||
@click.argument("dst")
|
||||
def copy(src, dst):
|
||||
click.echo("src={}".format("|".join(src)))
|
||||
click.echo("dst={}".format(dst))
|
||||
click.echo(f"src={'|'.join(src)}")
|
||||
click.echo(f"dst={dst}")
|
||||
|
||||
result = runner.invoke(copy, ["foo.txt", "bar.txt", "dir"])
|
||||
assert not result.exception
|
||||
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"):
|
||||
|
||||
@click.command()
|
||||
@click.argument("src", nargs=-1, default=42)
|
||||
@click.argument("src", nargs=-1, default=["42"])
|
||||
def copy(src):
|
||||
pass
|
||||
|
||||
|
@ -35,8 +32,9 @@ def test_nargs_tup(runner):
|
|||
@click.argument("name", nargs=1)
|
||||
@click.argument("point", nargs=2, type=click.INT)
|
||||
def copy(name, point):
|
||||
click.echo("name={}".format(name))
|
||||
click.echo("point={0[0]}/{0[1]}".format(point))
|
||||
click.echo(f"name={name}")
|
||||
x, y = point
|
||||
click.echo(f"point={x}/{y}")
|
||||
|
||||
result = runner.invoke(copy, ["peter", "1", "2"])
|
||||
assert not result.exception
|
||||
|
@ -56,7 +54,8 @@ def test_nargs_tup_composite(runner):
|
|||
@click.command()
|
||||
@click.argument("item", **opts)
|
||||
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"])
|
||||
assert not result.exception
|
||||
|
@ -83,22 +82,17 @@ def test_bytes_args(runner, monkeypatch):
|
|||
@click.argument("arg")
|
||||
def from_bytes(arg):
|
||||
assert isinstance(
|
||||
arg, text_type
|
||||
arg, str
|
||||
), "UTF-8 encoded argument should be implicitly converted to Unicode"
|
||||
|
||||
# Simulate empty locale environment variables
|
||||
if PY2:
|
||||
monkeypatch.setattr(sys.stdin, "encoding", "ANSI_X3.4-1968")
|
||||
monkeypatch.setattr(sys, "getfilesystemencoding", lambda: "ANSI_X3.4-1968")
|
||||
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")
|
||||
monkeypatch.setattr(sys.stdin, "encoding", "utf-8")
|
||||
monkeypatch.setattr(sys, "getfilesystemencoding", lambda: "utf-8")
|
||||
monkeypatch.setattr(sys, "getdefaultencoding", lambda: "utf-8")
|
||||
|
||||
runner.invoke(
|
||||
from_bytes,
|
||||
[u"Something outside of ASCII range: 林".encode("UTF-8")],
|
||||
["Something outside of ASCII range: 林".encode()],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
|
||||
|
@ -169,33 +163,55 @@ def test_stdout_default(runner):
|
|||
assert result.output == "Foo bar baz\n"
|
||||
|
||||
|
||||
def test_nargs_envvar(runner):
|
||||
@click.command()
|
||||
@click.option("--arg", nargs=2)
|
||||
def cmd(arg):
|
||||
click.echo("|".join(arg))
|
||||
|
||||
result = runner.invoke(
|
||||
cmd, [], auto_envvar_prefix="TEST", env={"TEST_ARG": "foo bar"}
|
||||
)
|
||||
assert not result.exception
|
||||
assert result.output == "foo|bar\n"
|
||||
@pytest.mark.parametrize(
|
||||
("nargs", "value", "expect"),
|
||||
[
|
||||
(2, "", None),
|
||||
(2, "a", "Takes 2 values but 1 was given."),
|
||||
(2, "a b", ("a", "b")),
|
||||
(2, "a b c", "Takes 2 values but 3 were given."),
|
||||
(-1, "a b c", ("a", "b", "c")),
|
||||
(-1, "", ()),
|
||||
],
|
||||
)
|
||||
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.option("--arg", envvar="X", nargs=2)
|
||||
@param
|
||||
def cmd(arg):
|
||||
click.echo("|".join(arg))
|
||||
return arg
|
||||
|
||||
result = runner.invoke(cmd, [], env={"X": "foo bar"})
|
||||
assert not result.exception
|
||||
assert result.output == "foo|bar\n"
|
||||
result = runner.invoke(cmd, env={"X": value}, standalone_mode=False)
|
||||
|
||||
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):
|
||||
@click.command()
|
||||
@click.argument("arg", nargs=-1)
|
||||
def cmd(arg):
|
||||
click.echo("arg:{}".format("|".join(arg)))
|
||||
click.echo(f"arg:{'|'.join(arg)}")
|
||||
|
||||
result = runner.invoke(cmd, [])
|
||||
assert result.exit_code == 0
|
||||
|
@ -204,7 +220,7 @@ def test_empty_nargs(runner):
|
|||
@click.command()
|
||||
@click.argument("arg", nargs=-1, required=True)
|
||||
def cmd2(arg):
|
||||
click.echo("arg:{}".format("|".join(arg)))
|
||||
click.echo(f"arg:{'|'.join(arg)}")
|
||||
|
||||
result = runner.invoke(cmd2, [])
|
||||
assert result.exit_code == 2
|
||||
|
@ -215,7 +231,7 @@ def test_missing_arg(runner):
|
|||
@click.command()
|
||||
@click.argument("arg")
|
||||
def cmd(arg):
|
||||
click.echo("arg:{}".format(arg))
|
||||
click.echo(f"arg:{arg}")
|
||||
|
||||
result = runner.invoke(cmd, [])
|
||||
assert result.exit_code == 2
|
||||
|
@ -226,9 +242,9 @@ def test_missing_argument_string_cast():
|
|||
ctx = click.Context(click.Command(""))
|
||||
|
||||
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):
|
||||
|
@ -268,7 +284,7 @@ def test_nargs_star_ordering(runner):
|
|||
click.echo(arg)
|
||||
|
||||
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):
|
||||
|
@ -281,11 +297,7 @@ def test_nargs_specified_plus_star_ordering(runner):
|
|||
click.echo(arg)
|
||||
|
||||
result = runner.invoke(cmd, ["a", "b", "c", "d", "e", "f"])
|
||||
assert result.output.splitlines() == [
|
||||
"(u'a', u'b', u'c')" if PY2 else "('a', 'b', 'c')",
|
||||
"d",
|
||||
"(u'e', u'f')" if PY2 else "('e', 'f')",
|
||||
]
|
||||
assert result.output.splitlines() == ["('a', 'b', 'c')", "d", "('e', 'f')"]
|
||||
|
||||
|
||||
def test_defaults_for_nargs(runner):
|
||||
|
@ -303,7 +315,7 @@ def test_defaults_for_nargs(runner):
|
|||
|
||||
result = runner.invoke(cmd, ["3"])
|
||||
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):
|
||||
|
@ -313,3 +325,55 @@ def test_multiple_param_decls_not_allowed(runner):
|
|||
@click.argument("x", click.Choice(["a", "b"]))
|
||||
def copy(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 uuid
|
||||
from itertools import chain
|
||||
|
||||
import pytest
|
||||
|
||||
import click
|
||||
|
||||
|
@ -78,11 +80,35 @@ def test_basic_group(runner):
|
|||
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):
|
||||
@click.command()
|
||||
@click.option("--foo", default="no value")
|
||||
def cli(foo):
|
||||
click.echo(u"FOO:[{}]".format(foo))
|
||||
click.echo(f"FOO:[{foo}]")
|
||||
|
||||
result = runner.invoke(cli, [])
|
||||
assert not result.exception
|
||||
|
@ -94,22 +120,22 @@ def test_basic_option(runner):
|
|||
|
||||
result = runner.invoke(cli, ["--foo"])
|
||||
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="])
|
||||
assert not result.exception
|
||||
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 u"FOO:[\N{SNOWMAN}]" in result.output
|
||||
assert "FOO:[\N{SNOWMAN}]" in result.output
|
||||
|
||||
|
||||
def test_int_option(runner):
|
||||
@click.command()
|
||||
@click.option("--foo", default=42)
|
||||
def cli(foo):
|
||||
click.echo("FOO:[{}]".format(foo * 2))
|
||||
click.echo(f"FOO:[{foo * 2}]")
|
||||
|
||||
result = runner.invoke(cli, [])
|
||||
assert not result.exception
|
||||
|
@ -121,7 +147,7 @@ def test_int_option(runner):
|
|||
|
||||
result = runner.invoke(cli, ["--foo=bar"])
|
||||
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):
|
||||
|
@ -131,7 +157,7 @@ def test_uuid_option(runner):
|
|||
)
|
||||
def cli(u):
|
||||
assert type(u) is uuid.UUID
|
||||
click.echo("U:[{}]".format(u))
|
||||
click.echo(f"U:[{u}]")
|
||||
|
||||
result = runner.invoke(cli, [])
|
||||
assert not result.exception
|
||||
|
@ -143,7 +169,7 @@ def test_uuid_option(runner):
|
|||
|
||||
result = runner.invoke(cli, ["--u=bar"])
|
||||
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):
|
||||
|
@ -151,7 +177,7 @@ def test_float_option(runner):
|
|||
@click.option("--foo", default=42, type=click.FLOAT)
|
||||
def cli(foo):
|
||||
assert type(foo) is float
|
||||
click.echo("FOO:[{}]".format(foo))
|
||||
click.echo(f"FOO:[{foo}]")
|
||||
|
||||
result = runner.invoke(cli, [])
|
||||
assert not result.exception
|
||||
|
@ -163,7 +189,7 @@ def test_float_option(runner):
|
|||
|
||||
result = runner.invoke(cli, ["--foo=bar"])
|
||||
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):
|
||||
|
@ -182,7 +208,7 @@ def test_boolean_option(runner):
|
|||
assert result.output == "False\n"
|
||||
result = runner.invoke(cli, [])
|
||||
assert not result.exception
|
||||
assert result.output == "{}\n".format(default)
|
||||
assert result.output == f"{default}\n"
|
||||
|
||||
for default in True, False:
|
||||
|
||||
|
@ -193,33 +219,30 @@ def test_boolean_option(runner):
|
|||
|
||||
result = runner.invoke(cli, ["--flag"])
|
||||
assert not result.exception
|
||||
assert result.output == "{}\n".format(not default)
|
||||
assert result.output == f"{not default}\n"
|
||||
result = runner.invoke(cli, [])
|
||||
assert not result.exception
|
||||
assert result.output == "{}\n".format(default)
|
||||
assert result.output == f"{default}\n"
|
||||
|
||||
|
||||
def test_boolean_conversion(runner):
|
||||
for default in True, False:
|
||||
@pytest.mark.parametrize(
|
||||
("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()
|
||||
@click.option("--flag", default=default, type=bool)
|
||||
def cli(flag):
|
||||
click.echo(flag)
|
||||
result = runner.invoke(cli, ["--flag", value])
|
||||
assert result.output == expect
|
||||
|
||||
for value in "true", "t", "1", "yes", "y":
|
||||
result = runner.invoke(cli, ["--flag", value])
|
||||
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)
|
||||
result = runner.invoke(cli, ["--flag", value.title()])
|
||||
assert result.output == expect
|
||||
|
||||
|
||||
def test_file_option(runner):
|
||||
|
@ -280,10 +303,7 @@ def test_file_lazy_mode(runner):
|
|||
os.mkdir("example.txt")
|
||||
result_in = runner.invoke(input_non_lazy, ["--file=example.txt"])
|
||||
assert result_in.exit_code == 2
|
||||
assert (
|
||||
"Invalid value for '--file': Could not open file: example.txt"
|
||||
in result_in.output
|
||||
)
|
||||
assert "Invalid value for '--file': 'example.txt'" in result_in.output
|
||||
|
||||
|
||||
def test_path_option(runner):
|
||||
|
@ -308,8 +328,8 @@ def test_path_option(runner):
|
|||
@click.command()
|
||||
@click.option("-f", type=click.Path(exists=True))
|
||||
def showtype(f):
|
||||
click.echo("is_file={}".format(os.path.isfile(f)))
|
||||
click.echo("is_dir={}".format(os.path.isdir(f)))
|
||||
click.echo(f"is_file={os.path.isfile(f)}")
|
||||
click.echo(f"is_dir={os.path.isdir(f)}")
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(showtype, ["-f", "xxx"])
|
||||
|
@ -322,7 +342,7 @@ def test_path_option(runner):
|
|||
@click.command()
|
||||
@click.option("-f", type=click.Path())
|
||||
def exists(f):
|
||||
click.echo("exists={}".format(os.path.exists(f)))
|
||||
click.echo(f"exists={os.path.exists(f)}")
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(exists, ["-f", "xxx"])
|
||||
|
@ -345,14 +365,35 @@ def test_choice_option(runner):
|
|||
result = runner.invoke(cli, ["--method=meh"])
|
||||
assert result.exit_code == 2
|
||||
assert (
|
||||
"Invalid value for '--method': invalid choice: meh."
|
||||
" (choose from foo, bar, baz)" in result.output
|
||||
"Invalid value for '--method': 'meh' is not one of 'foo', 'bar', 'baz'."
|
||||
in result.output
|
||||
)
|
||||
|
||||
result = runner.invoke(cli, ["--help"])
|
||||
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):
|
||||
@click.command()
|
||||
@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"])
|
||||
assert result.exit_code == 2
|
||||
assert (
|
||||
"Invalid value for '--start_date':"
|
||||
" invalid datetime format: 2015-09."
|
||||
" (choose from %Y-%m-%d, %Y-%m-%dT%H:%M:%S, %Y-%m-%d %H:%M:%S)"
|
||||
"Invalid value for '--start_date': '2015-09' does not match the formats"
|
||||
" '%Y-%m-%d', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S'."
|
||||
) in result.output
|
||||
|
||||
result = runner.invoke(cli, ["--help"])
|
||||
|
@ -392,76 +432,6 @@ def test_datetime_option_custom(runner):
|
|||
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):
|
||||
@click.command()
|
||||
@click.option("--foo", required=True)
|
||||
|
@ -558,3 +528,39 @@ def test_hidden_group(runner):
|
|||
assert result.exit_code == 0
|
||||
assert "subgroup" 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():
|
||||
click.echo(
|
||||
"{}={}".format(
|
||||
sys._getframe(1).f_code.co_name, "|".join(click.get_current_context().args)
|
||||
)
|
||||
f"{sys._getframe(1).f_code.co_name}"
|
||||
f"={'|'.join(click.get_current_context().args)}"
|
||||
)
|
||||
|
||||
|
||||
|
@ -77,18 +76,37 @@ def test_chaining_with_options(runner):
|
|||
@cli.command("sdist")
|
||||
@click.option("--format")
|
||||
def sdist(format):
|
||||
click.echo("sdist called {}".format(format))
|
||||
click.echo(f"sdist called {format}")
|
||||
|
||||
@cli.command("bdist")
|
||||
@click.option("--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"])
|
||||
assert not result.exception
|
||||
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):
|
||||
@click.group(chain=True)
|
||||
def cli():
|
||||
|
@ -97,12 +115,12 @@ def test_chaining_with_arguments(runner):
|
|||
@cli.command("sdist")
|
||||
@click.argument("format")
|
||||
def sdist(format):
|
||||
click.echo("sdist called {}".format(format))
|
||||
click.echo(f"sdist called {format}")
|
||||
|
||||
@cli.command("bdist")
|
||||
@click.argument("format")
|
||||
def bdist(format):
|
||||
click.echo("bdist called {}".format(format))
|
||||
click.echo(f"bdist called {format}")
|
||||
|
||||
result = runner.invoke(cli, ["bdist", "1", "sdist", "2"])
|
||||
assert not result.exception
|
||||
|
@ -115,7 +133,7 @@ def test_pipeline(runner):
|
|||
def cli(input):
|
||||
pass
|
||||
|
||||
@cli.resultcallback()
|
||||
@cli.result_callback()
|
||||
def process_pipeline(processors, input):
|
||||
iterator = (x.rstrip("\r\n") for x in input)
|
||||
for processor in processors:
|
||||
|
@ -192,7 +210,7 @@ def test_multicommand_arg_behavior(runner):
|
|||
@click.group(chain=True)
|
||||
@click.argument("arg")
|
||||
def cli(arg):
|
||||
click.echo("cli:{}".format(arg))
|
||||
click.echo(f"cli:{arg}")
|
||||
|
||||
@cli.command()
|
||||
def a():
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
|
||||
import click
|
||||
|
@ -26,7 +25,7 @@ def test_other_command_forward(runner):
|
|||
@cli.command()
|
||||
@click.option("--count", default=1)
|
||||
def test(count):
|
||||
click.echo("Count: {:d}".format(count))
|
||||
click.echo(f"Count: {count:d}")
|
||||
|
||||
@cli.command()
|
||||
@click.option("--count", default=1)
|
||||
|
@ -40,6 +39,28 @@ def test_other_command_forward(runner):
|
|||
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):
|
||||
@click.group()
|
||||
def cli():
|
||||
|
@ -102,7 +123,7 @@ def test_group_with_args(runner):
|
|||
@click.group()
|
||||
@click.argument("obj")
|
||||
def cli(obj):
|
||||
click.echo("obj={}".format(obj))
|
||||
click.echo(f"obj={obj}")
|
||||
|
||||
@cli.command()
|
||||
def move():
|
||||
|
@ -134,7 +155,7 @@ def test_base_command(runner):
|
|||
|
||||
class OptParseCommand(click.BaseCommand):
|
||||
def __init__(self, name, parser, callback):
|
||||
click.BaseCommand.__init__(self, name)
|
||||
super().__init__(name)
|
||||
self.parser = parser
|
||||
self.callback = callback
|
||||
|
||||
|
@ -211,7 +232,7 @@ def test_object_propagation(runner):
|
|||
@cli.command()
|
||||
@click.pass_context
|
||||
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"])
|
||||
assert result.exception is None
|
||||
|
@ -259,13 +280,43 @@ def test_invoked_subcommand(runner):
|
|||
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):
|
||||
@click.command(context_settings=dict(ignore_unknown_options=True))
|
||||
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
||||
@click.option("--verbose", "-v", count=True)
|
||||
def cli(verbose, args):
|
||||
click.echo("Verbosity: {}".format(verbose))
|
||||
click.echo("Args: {}".format("|".join(args)))
|
||||
click.echo(f"Verbosity: {verbose}")
|
||||
click.echo(f"Args: {'|'.join(args)}")
|
||||
|
||||
result = runner.invoke(cli, ["-foo", "-vvvvx", "--muhaha", "x", "y", "-x"])
|
||||
assert not result.exception
|
||||
|
@ -282,14 +333,14 @@ def test_deprecated_in_help_messages(runner):
|
|||
pass
|
||||
|
||||
result = runner.invoke(cmd_with_help, ["--help"])
|
||||
assert "(DEPRECATED)" in result.output
|
||||
assert "(Deprecated)" in result.output
|
||||
|
||||
@click.command(deprecated=True)
|
||||
def cmd_without_help():
|
||||
pass
|
||||
|
||||
result = runner.invoke(cmd_without_help, ["--help"])
|
||||
assert "(DEPRECATED)" in result.output
|
||||
assert "(Deprecated)" in result.output
|
||||
|
||||
|
||||
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 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():
|
||||
class JupyterKernelFakeStream(object):
|
||||
class JupyterKernelFakeStream:
|
||||
pass
|
||||
|
||||
# implementation detail, aka cheapskate test
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from contextlib import contextmanager
|
||||
|
||||
import pytest
|
||||
|
||||
import click
|
||||
from click.core import ParameterSource
|
||||
from click.decorators import pass_meta_key
|
||||
|
||||
|
||||
def test_ensure_context_objects(runner):
|
||||
class Foo(object):
|
||||
class Foo:
|
||||
def __init__(self):
|
||||
self.title = "default"
|
||||
|
||||
|
@ -25,7 +30,7 @@ def test_ensure_context_objects(runner):
|
|||
|
||||
|
||||
def test_get_context_objects(runner):
|
||||
class Foo(object):
|
||||
class Foo:
|
||||
def __init__(self):
|
||||
self.title = "default"
|
||||
|
||||
|
@ -48,7 +53,7 @@ def test_get_context_objects(runner):
|
|||
|
||||
|
||||
def test_get_context_objects_no_ensuring(runner):
|
||||
class Foo(object):
|
||||
class Foo:
|
||||
def __init__(self):
|
||||
self.title = "default"
|
||||
|
||||
|
@ -71,7 +76,7 @@ def test_get_context_objects_no_ensuring(runner):
|
|||
|
||||
|
||||
def test_get_context_objects_missing(runner):
|
||||
class Foo(object):
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
pass_foo = click.make_pass_decorator(Foo)
|
||||
|
@ -129,7 +134,7 @@ def test_global_context_object(runner):
|
|||
|
||||
|
||||
def test_context_meta(runner):
|
||||
LANG_KEY = "{}.lang".format(__name__)
|
||||
LANG_KEY = f"{__name__}.lang"
|
||||
|
||||
def set_language(value):
|
||||
click.get_current_context().meta[LANG_KEY] = value
|
||||
|
@ -147,6 +152,28 @@ def test_context_meta(runner):
|
|||
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():
|
||||
rv = []
|
||||
|
||||
|
@ -210,13 +237,29 @@ def test_close_before_pop(runner):
|
|||
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):
|
||||
"""
|
||||
Test to check that make_pass_decorator doesn't consume arguments based on
|
||||
invocation order.
|
||||
"""
|
||||
|
||||
class Foo(object):
|
||||
class Foo:
|
||||
title = "foocmd"
|
||||
|
||||
pass_foo = click.make_pass_decorator(Foo)
|
||||
|
@ -247,6 +290,20 @@ def test_make_pass_decorator_args(runner):
|
|||
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():
|
||||
@click.command()
|
||||
@click.pass_context
|
||||
|
@ -261,3 +318,50 @@ def test_exit_not_standalone():
|
|||
ctx.exit(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
|
||||
|
|
|
@ -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)
|
||||
def cli(foo):
|
||||
assert type(foo) is float
|
||||
click.echo("FOO:[{}]".format(foo))
|
||||
click.echo(f"FOO:[{foo}]")
|
||||
|
||||
result = runner.invoke(cli, [])
|
||||
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
|
||||
)
|
||||
def cli(arg):
|
||||
for item in arg:
|
||||
click.echo("<{0[0]:d}|{0[1]:d}>".format(item))
|
||||
for a, b in arg:
|
||||
click.echo(f"<{a:d}|{b:d}>")
|
||||
|
||||
result = runner.invoke(cli, [])
|
||||
assert not result.exception
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue