New upstream version 8.0.2

This commit is contained in:
Sandro Tosi 2021-10-09 21:31:57 -04:00
parent 4e974d1c0d
commit 46bc5bd117
111 changed files with 9282 additions and 5180 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
root = true
[*]
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
charset = utf-8
max_line_length = 88
[*.{yml,yaml,json,js,css,html}]
indent_size = 2

27
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,27 @@
---
name: Bug report
about: Report a bug in Click (not other projects which depend on Click)
---
<!--
This issue tracker is a tool to address bugs in Click itself. Please use
Pallets Discord or Stack Overflow for questions about your own code.
Replace this comment with a clear outline of what the bug is.
-->
<!--
Describe how to replicate the bug.
Include a minimal reproducible example that demonstrates the bug.
Include the full traceback if there was an exception.
-->
<!--
Describe the expected behavior that should have happened but didn't.
-->
Environment:
- Python version:
- Click version:

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Security issue
url: security@palletsprojects.com
about: Do not report security issues publicly. Email our security contact.
- name: Questions
url: https://stackoverflow.com/questions/tagged/python-click?tab=Frequent
about: Search for and ask questions about your code on Stack Overflow.
- name: Questions and discussions
url: https://discord.gg/pallets
about: Discuss questions about your code on our Discord chat.

View File

@ -0,0 +1,15 @@
---
name: Feature request
about: Suggest a new feature for Click
---
<!--
Replace this comment with a description of what the feature should do.
Include details such as links relevant specs or previous discussions.
-->
<!--
Replace this comment with an example of the problem which this feature
would resolve. Is this problem solvable without changes to Click,
such as by subclassing or using an extension?
-->

8
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: monthly
time: "08:00"
open-pull-requests-limit: 99

30
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,30 @@
<!--
Before opening a PR, open a ticket describing the issue or feature the
PR will address. Follow the steps in CONTRIBUTING.rst.
Replace this comment with a description of the change. Describe how it
addresses the linked ticket.
-->
<!--
Link to relevant issues or previous PRs, one per line. Use "fixes" to
automatically close an issue.
-->
- fixes #<issue number>
<!--
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to
each box below.
If only docs were changed, these aren't relevant and can be removed.
-->
Checklist:
- [ ] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [ ] Add or update relevant docs, in the docs folder and in code.
- [ ] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [ ] Add `.. versionchanged::` entries in any relevant code docs.
- [ ] Run `pre-commit` hooks and fix any issues.
- [ ] Run `pytest` and `tox`, no tests failed.

15
.github/workflows/lock.yaml vendored Normal file
View File

@ -0,0 +1,15 @@
name: 'Lock threads'
on:
schedule:
- cron: '0 0 * * *'
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: 14
pr-lock-inactive-days: 14

60
.github/workflows/tests.yaml vendored Normal file
View File

@ -0,0 +1,60 @@
name: Tests
on:
push:
branches:
- main
- '*.x'
paths-ignore:
- 'docs/**'
- '*.md'
- '*.rst'
pull_request:
branches:
- main
- '*.x'
paths-ignore:
- 'docs/**'
- '*.md'
- '*.rst'
jobs:
tests:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- {name: Linux, python: '3.9', os: ubuntu-latest, tox: py39}
- {name: Windows, python: '3.9', os: windows-latest, tox: py39}
- {name: Mac, python: '3.9', os: macos-latest, tox: py39}
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
- {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36}
- {name: 'PyPy', python: pypy3, os: ubuntu-latest, tox: pypy3}
- {name: Typing, python: '3.9', os: ubuntu-latest, tox: typing}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python }}
- name: update pip
run: |
pip install -U wheel
pip install -U setuptools
python -m pip install -U pip
- name: get pip cache dir
id: pip-cache
run: echo "::set-output name=dir::$(pip cache dir)"
- name: cache pip
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}|${{ hashFiles('requirements/*.txt') }}
- name: cache mypy
uses: actions/cache@v2
with:
path: ./.mypy_cache
key: mypy|${{ matrix.python }}|${{ hashFiles('setup.cfg') }}
if: matrix.tox == 'typing'
- run: pip install tox
- run: tox -e ${{ matrix.tox }}

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
/.idea/
/.vscode/
/env/
/venv/
__pycache__/
*.pyc
*.egg-info/
/build/
/dist/
/.pytest_cache/
/.tox/
.coverage
.coverage.*
/htmlcov/
/docs/_build/

30
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,30 @@
ci:
autoupdate_schedule: monthly
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v2.29.0
hooks:
- id: pyupgrade
args: ["--py36-plus"]
- repo: https://github.com/asottile/reorder_python_imports
rev: v2.6.0
hooks:
- id: reorder-python-imports
args: ["--application-directories", "src"]
- repo: https://github.com/psf/black
rev: 21.9b0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
hooks:
- id: flake8
additional_dependencies:
- flake8-bugbear
- flake8-implicit-str-concat
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: fix-byte-order-marker
- id: trailing-whitespace
- id: end-of-file-fixer

9
.readthedocs.yaml Normal file
View File

@ -0,0 +1,9 @@
version: 2
python:
install:
- requirements: requirements/docs.txt
- method: pip
path: .
sphinx:
builder: dirhtml
fail_on_warning: true

View File

@ -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.

76
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at report@palletsprojects.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

222
CONTRIBUTING.rst Normal file
View File

@ -0,0 +1,222 @@
How to contribute to Click
==========================
Thank you for considering contributing to Click!
Support questions
-----------------
Please don't use the issue tracker for this. The issue tracker is a tool
to address bugs and feature requests in Click itself. Use one of the
following resources for questions about using Click or issues with your
own code:
- The ``#get-help`` channel on our Discord chat:
https://discord.gg/pallets
- The mailing list flask@python.org for long term discussion or larger
issues.
- Ask on `Stack Overflow`_. Search with Google first using:
``site:stackoverflow.com python click {search term, exception message, etc.}``
.. _Stack Overflow: https://stackoverflow.com/questions/tagged/python-click?tab=Frequent
Reporting issues
----------------
Include the following information in your post:
- Describe what you expected to happen.
- If possible, include a `minimal reproducible example`_ to help us
identify the issue. This also helps check that the issue is not with
your own code.
- Describe what actually happened. Include the full traceback if there
was an exception.
- List your Python and Click versions. If possible, check if this
issue is already fixed in the latest releases or the latest code in
the repository.
.. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example
Submitting patches
------------------
If there is not an open issue for what you want to submit, prefer
opening one for discussion before working on a PR. You can work on any
issue that doesn't have an open PR linked to it or a maintainer assigned
to it. These show up in the sidebar. No need to ask if you can work on
an issue that interests you.
Include the following in your patch:
- Use `Black`_ to format your code. This and other tools will run
automatically if you install `pre-commit`_ using the instructions
below.
- Include tests if your patch adds or changes code. Make sure the test
fails without your patch.
- Update any relevant docs pages and docstrings. Docs pages and
docstrings should be wrapped at 72 characters.
- Add an entry in ``CHANGES.rst``. Use the same style as other
entries. Also include ``.. versionchanged::`` inline changelogs in
relevant docstrings.
.. _Black: https://black.readthedocs.io
.. _pre-commit: https://pre-commit.com
First time setup
~~~~~~~~~~~~~~~~
- Download and install the `latest version of git`_.
- Configure git with your `username`_ and `email`_.
.. code-block:: text
$ git config --global user.name 'your name'
$ git config --global user.email 'your email'
- Make sure you have a `GitHub account`_.
- Fork Click to your GitHub account by clicking the `Fork`_ button.
- `Clone`_ the main repository locally.
.. code-block:: text
$ git clone https://github.com/pallets/click
$ cd click
- Add your fork as a remote to push your work to. Replace
``{username}`` with your username. This names the remote "fork", the
default Pallets remote is "origin".
.. code-block:: text
$ git remote add fork https://github.com/{username}/click
- Create a virtualenv.
.. code-block:: text
$ python3 -m venv env
$ . env/bin/activate
On Windows, activating is different.
.. code-block:: text
> env\Scripts\activate
- Upgrade pip and setuptools.
.. code-block:: text
$ python -m pip install --upgrade pip setuptools
- Install the development dependencies, then install Click in
editable mode.
.. code-block:: text
$ pip install -r requirements/dev.txt && pip install -e .
- Install the pre-commit hooks.
.. code-block:: text
$ pre-commit install
.. _latest version of git: https://git-scm.com/downloads
.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git
.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address
.. _GitHub account: https://github.com/join
.. _Fork: https://github.com/pallets/click/fork
.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork
Start coding
~~~~~~~~~~~~
- Create a branch to identify the issue you would like to work on. If
you're submitting a bug or documentation fix, branch off of the
latest ".x" branch.
.. code-block:: text
$ git fetch origin
$ git checkout -b your-branch-name origin/8.0.x
If you're submitting a feature addition or change, branch off of the
"main" branch.
.. code-block:: text
$ git fetch origin
$ git checkout -b your-branch-name origin/main
- Using your favorite editor, make your changes,
`committing as you go`_.
- Include tests that cover any code changes you make. Make sure the
test fails without your patch. Run the tests as described below.
- Push your commits to your fork on GitHub and
`create a pull request`_. Link to the issue being addressed with
``fixes #123`` in the pull request.
.. code-block:: text
$ git push --set-upstream fork your-branch-name
.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes
.. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
Running the tests
~~~~~~~~~~~~~~~~~
Run the basic test suite with pytest.
.. code-block:: text
$ pytest
This runs the tests for the current environment, which is usually
sufficient. CI will run the full suite when you submit your pull
request. You can run the full test suite with tox if you don't want to
wait.
.. code-block:: text
$ tox
Running test coverage
~~~~~~~~~~~~~~~~~~~~~
Generating a report of lines that do not have test coverage can indicate
where to start contributing. Run ``pytest`` using ``coverage`` and
generate a report.
.. code-block:: text
$ pip install coverage
$ coverage run -m pytest
$ coverage html
Open ``htmlcov/index.html`` in your browser to explore the report.
Read more about `coverage <https://coverage.readthedocs.io>`__.
Building the docs
~~~~~~~~~~~~~~~~~
Build the docs in the ``docs`` directory using Sphinx.
.. code-block:: text
$ cd docs
$ make html
Open ``_build/html/index.html`` in your browser to view the docs.
Read more about `Sphinx <https://www.sphinx-doc.org/en/stable/>`__.

View File

@ -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
View File

@ -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.*

View File

@ -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

BIN
docs/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -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()

View File

@ -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
-------

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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"

View File

@ -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:

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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',
],
},
)

285
docs/shell-completion.rst Normal file
View File

@ -0,0 +1,285 @@
.. currentmodule:: click.shell_completion
Shell Completion
================
Click provides tab completion support for Bash (version 4.4 and up),
Zsh, and Fish. It is possible to add support for other shells too, and
suggestions can be customized at multiple levels.
Shell completion suggests command names, option names, and values for
choice, file, and path parameter types. Options are only listed if at
least a dash has been entered. Hidden commands and options are not
shown.
.. code-block:: text
$ repo <TAB><TAB>
clone commit copy delete setuser
$ repo clone -<TAB><TAB>
--deep --help --rev --shallow -r
Enabling Completion
-------------------
Completion is only available if a script is installed and invoked
through an entry point, not through the ``python`` command. See
:doc:`/setuptools`. Once the executable is installed, calling it with
a special environment variable will put Click in completion mode.
In order for completion to be used, the user needs to register a special
function with their shell. The script is different for every shell, and
Click will output it when called with ``_{PROG_NAME}_COMPLETE`` set to
``{shell}_source``. ``{PROG_NAME}`` is the executable name in uppercase
with dashes replaced by underscores. The built-in shells are ``bash``,
``zsh``, and ``fish``.
Provide your users with the following instructions customized to your
program name. This uses ``foo-bar`` as an example.
.. tabs::
.. group-tab:: Bash
Add this to ``~/.bashrc``:
.. code-block:: bash
eval "$(_FOO_BAR_COMPLETE=bash_source foo-bar)"
.. group-tab:: Zsh
Add this to ``~/.zshrc``:
.. code-block:: zsh
eval "$(_FOO_BAR_COMPLETE=zsh_source foo-bar)"
.. group-tab:: Fish
Add this to ``~/.config/fish/completions/foo-bar.fish``:
.. code-block:: fish
eval (env _FOO_BAR_COMPLETE=fish_source foo-bar)
This is the same file used for the activation script method
below. For Fish it's probably always easier to use that method.
Using ``eval`` means that the command is invoked and evaluated every
time a shell is started, which can delay shell responsiveness. To speed
it up, write the generated script to a file, then source that. You can
generate the files ahead of time and distribute them with your program
to save your users a step.
.. tabs::
.. group-tab:: Bash
Save the script somewhere.
.. code-block:: bash
_FOO_BAR_COMPLETE=bash_source foo-bar > ~/.foo-bar-complete.bash
Source the file in ``~/.bashrc``.
.. code-block:: bash
. ~/.foo-bar-complete.bash
.. group-tab:: Zsh
Save the script somewhere.
.. code-block:: bash
_FOO_BAR_COMPLETE=zsh_source foo-bar > ~/.foo-bar-complete.zsh
Source the file in ``~/.zshrc``.
.. code-block:: bash
. ~/.foo-bar-complete.zsh
.. group-tab:: Fish
Save the script to ``~/.config/fish/completions/foo-bar.fish``:
.. code-block:: fish
_FOO_BAR_COMPLETE=fish_source foo-bar > ~/.config/fish/completions/foo-bar.fish
After modifying the shell config, you need to start a new shell in order
for the changes to be loaded.
Custom Type Completion
----------------------
When creating a custom :class:`~click.ParamType`, override its
:meth:`~click.ParamType.shell_complete` method to provide shell
completion for parameters with the type. The method must return a list
of :class:`~CompletionItem` objects. Besides the value, these objects
hold metadata that shell support might use. The built-in implementations
use ``type`` to indicate special handling for paths, and ``help`` for
shells that support showing a help string next to a suggestion.
In this example, the type will suggest environment variables that start
with the incomplete value.
.. code-block:: python
class EnvVarType(ParamType):
def shell_complete(self, ctx, param, incomplete):
return [
CompletionItem(name)
for name in os.environ if name.startswith(incomplete)
]
@click.command()
@click.option("--ev", type=EnvVarType())
def cli(ev):
click.echo(os.environ[ev])
Overriding Value Completion
---------------------------
Value completions for a parameter can be customized without a custom
type by providing a ``shell_complete`` function. The function is used
instead of any completion provided by the type. It is passed 3 keyword
arguments:
- ``ctx`` - The current command context.
- ``param`` - The current parameter requesting completion.
- ``incomplete`` - The partial word that is being completed. May
be an empty string if no characters have been entered yet.
It must return a list of :class:`CompletionItem` objects, or as a
shortcut it can return a list of strings.
In this example, the command will suggest environment variables that
start with the incomplete value.
.. code-block:: python
def complete_env_vars(ctx, param, incomplete):
return [k for k in os.environ if k.startswith(incomplete)]
@click.command()
@click.argument("name", shell_complete=complete_env_vars)
def cli(name):
click.echo(f"Name: {name}")
click.echo(f"Value: {os.environ[name]}")
Adding Support for a Shell
--------------------------
Support can be added for shells that do not come built in. Be sure to
check PyPI to see if there's already a package that adds support for
your shell. This topic is very technical, you'll want to look at Click's
source to study the built-in implementations.
Shell support is provided by subclasses of :class:`ShellComplete`
registered with :func:`add_completion_class`. When Click is invoked in
completion mode, it calls :meth:`~ShellComplete.source` to output the
completion script, or :meth:`~ShellComplete.complete` to output
completions. The base class provides default implementations that
require implementing some smaller parts.
First, you'll need to figure out how your shell's completion system
works and write a script to integrate it with Click. It must invoke your
program with the environment variable ``_{PROG_NAME}_COMPLETE`` set to
``{shell}_complete`` and pass the complete args and incomplete value.
How it passes those values, and the format of the completion response
from Click is up to you.
In your subclass, set :attr:`~ShellComplete.source_template` to the
completion script. The default implementation will perform ``%``
formatting with the following variables:
- ``complete_func`` - A safe name for the completion function defined
in the script.
- ``complete_var`` - The environment variable name for passing the
``{shell}_complete`` instruction.
- ``prog_name`` - The name of the executable being completed.
The example code is for a made up shell "My Shell" or "mysh" for short.
.. code-block:: python
from click.shell_completion import add_completion_class
from click.shell_completion import ShellComplete
_mysh_source = """\
%(complete_func)s {
response=$(%(complete_var)s=mysh_complete %(prog_name)s)
# parse response and set completions somehow
}
call-on-complete %(prog_name)s %(complete_func)s
"""
@add_completion_class
class MyshComplete(ShellComplete):
name = "mysh"
source_template = _mysh_source
Next, implement :meth:`~ShellComplete.get_completion_args`. This must
get, parse, and return the complete args and incomplete value from the
completion script. For example, for the Bash implementation the
``COMP_WORDS`` env var contains the command line args as a string, and
the ``COMP_CWORD`` env var contains the index of the incomplete arg. The
method must return a ``(args, incomplete)`` tuple.
.. code-block:: python
import os
from click.parser import split_arg_string
class MyshComplete(ShellComplete):
...
def get_completion_args(self):
args = split_arg_string(os.environ["COMP_WORDS"])
if os.environ["COMP_PARTIAL"] == "1":
incomplete = args.pop()
return args, incomplete
return args, ""
Finally, implement :meth:`~ShellComplete.format_completion`. This is
called to format each :class:`CompletionItem` into a string. For
example, the Bash implementation returns ``f"{item.type},{item.value}``
(it doesn't support help strings), and the Zsh implementation returns
each part separated by a newline, replacing empty help with a ``_``
placeholder. This format is entirely up to what you parse with your
completion script.
The ``type`` value is usually ``plain``, but it can be another value
that the completion script can switch on. For example, ``file`` or
``dir`` can tell the shell to handle path completion, since the shell is
better at that than Click.
.. code-block:: python
class MyshComplete(ShellComplete):
...
def format_completion(self, item):
return f"{item.type}\t{item.value}"
With those three things implemented, the new shell support is ready. In
case those weren't sufficient, there are more parts that can be
overridden, but that probably isn't necessary.
The activation instructions will again depend on how your shell works.
Use the following to generate the completion script, then load it into
the shell somehow.
.. code-block:: text
_FOO_BAR_COMPLETE=mysh_source foo-bar

View File

@ -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

112
docs/unicode-support.rst Normal file
View File

@ -0,0 +1,112 @@
Unicode Support
===============
.. currentmodule:: click
Click has to take extra care to support Unicode text in different
environments.
* The command line in Unix is traditionally bytes, not Unicode. While
there are encoding hints, there are some situations where this can
break. The most common one is SSH connections to machines with
different locales.
Misconfigured environments can cause a wide range of Unicode
problems due to the lack of support for roundtripping surrogate
escapes. This will not be fixed in Click itself!
* Standard input and output is opened in text mode by default. Click
has to reopen the stream in binary mode in certain situations.
Because there is no standard way to do this, it might not always
work. Primarily this can become a problem when testing command-line
applications.
This is not supported::
sys.stdin = io.StringIO('Input here')
sys.stdout = io.StringIO()
Instead you need to do this::
input = 'Input here'
in_stream = io.BytesIO(input.encode('utf-8'))
sys.stdin = io.TextIOWrapper(in_stream, encoding='utf-8')
out_stream = io.BytesIO()
sys.stdout = io.TextIOWrapper(out_stream, encoding='utf-8')
Remember in that case, you need to use ``out_stream.getvalue()``
and not ``sys.stdout.getvalue()`` if you want to access the buffer
contents as the wrapper will not forward that method.
* ``sys.stdin``, ``sys.stdout`` and ``sys.stderr`` are by default
text-based. When Click needs a binary stream, it attempts to
discover the underlying binary stream.
* ``sys.argv`` is always text. This means that the native type for
input values to the types in Click is Unicode, not bytes.
This causes problems if the terminal is incorrectly set and Python
does not figure out the encoding. In that case, the Unicode string
will contain error bytes encoded as surrogate escapes.
* When dealing with files, Click will always use the Unicode file
system API by using the operating system's reported or guessed
filesystem encoding. Surrogates are supported for filenames, so it
should be possible to open files through the :class:`File` type even
if the environment is misconfigured.
Surrogate Handling
------------------
Click does all the Unicode handling in the standard library and is
subject to its behavior. Unicode requires extra care. The reason for
this is that the encoding detection is done in the interpreter, and on
Linux and certain other operating systems, its encoding handling is
problematic.
The biggest source of frustration is that Click scripts invoked by init
systems, deployment tools, or cron jobs will refuse to work unless a
Unicode locale is exported.
If Click encounters such an environment it will prevent further
execution to force you to set a locale. This is done because Click
cannot know about the state of the system once it's invoked and restore
the values before Python's Unicode handling kicked in.
If you see something like this error::
Traceback (most recent call last):
...
RuntimeError: Click will abort further execution because Python was
configured to use ASCII as encoding for the environment. Consult
https://click.palletsprojects.com/unicode-support/ for mitigation
steps.
You are dealing with an environment where Python thinks you are
restricted to ASCII data. The solution to these problems is different
depending on which locale your computer is running in.
For instance, if you have a German Linux machine, you can fix the
problem by exporting the locale to ``de_DE.utf-8``::
export LC_ALL=de_DE.utf-8
export LANG=de_DE.utf-8
If you are on a US machine, ``en_US.utf-8`` is the encoding of choice.
On some newer Linux systems, you could also try ``C.UTF-8`` as the
locale::
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
On some systems it was reported that ``UTF-8`` has to be written as
``UTF8`` and vice versa. To see which locales are supported you can
invoke ``locale -a``.
You need to export the values before you invoke your Python script.
In Python 3.7 and later you will no longer get a ``RuntimeError`` in
many cases thanks to :pep:`538` and :pep:`540`, which changed the
default assumption in unconfigured environments. This doesn't change the
general issue that your locale may be misconfigured.

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}'")

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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))

View File

@ -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

View File

@ -0,0 +1,28 @@
$ completion
============
Demonstrates Click's shell completion support.
.. code-block:: bash
pip install --editable .
For Bash:
.. code-block:: bash
eval "$(_COMPLETION_COMPLETE=bash_source completion)"
For Zsh:
.. code-block:: zsh
eval "$(_COMPLETION_COMPLETE=zsh_source completion)"
For Fish:
.. code-block:: fish
eval (env _COMPLETION_COMPLETE=fish_source completion)
Now press tab (maybe twice) after typing something to see completions.

View File

@ -0,0 +1,56 @@
import os
import click
from click.shell_completion import CompletionItem
@click.group()
def cli():
pass
@cli.command()
@click.option("--dir", type=click.Path(file_okay=False))
def ls(dir):
click.echo("\n".join(os.listdir(dir)))
def get_env_vars(ctx, param, incomplete):
# Returning a list of values is a shortcut to returning a list of
# CompletionItem(value).
return [k for k in os.environ if incomplete in k]
@cli.command(help="A command to print environment variables")
@click.argument("envvar", shell_complete=get_env_vars)
def show_env(envvar):
click.echo(f"Environment variable: {envvar}")
click.echo(f"Value: {os.environ[envvar]}")
@cli.group(help="A group that holds a subcommand")
def group():
pass
def list_users(ctx, param, incomplete):
# You can generate completions with help strings by returning a list
# of CompletionItem. You can match on whatever you want, including
# the help.
items = [("bob", "butcher"), ("alice", "baker"), ("jerry", "candlestick maker")]
out = []
for value, help in items:
if incomplete in value or incomplete in help:
out.append(CompletionItem(value, help=help))
return out
@group.command(help="Choose a user")
@click.argument("user", shell_complete=list_users)
def select_user(user):
click.echo(f"Chosen user is {user}")
cli.add_command(group)

View File

@ -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
""",
)

View File

@ -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

View File

@ -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)}")

View File

@ -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

View File

@ -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}")

View File

@ -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}")

View File

@ -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

View File

@ -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")

View File

@ -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}")

6
requirements/dev.in Normal file
View File

@ -0,0 +1,6 @@
-r docs.in
-r tests.in
-r typing.in
pip-tools
pre-commit
tox

143
requirements/dev.txt Normal file
View File

@ -0,0 +1,143 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile requirements/dev.in
#
alabaster==0.7.12
# via sphinx
attrs==21.2.0
# via pytest
babel==2.9.1
# via sphinx
backports.entry-points-selectable==1.1.0
# via virtualenv
certifi==2021.5.30
# via requests
cfgv==3.3.1
# via pre-commit
charset-normalizer==2.0.6
# via requests
click==8.0.1
# via pip-tools
distlib==0.3.3
# via virtualenv
docutils==0.16
# via
# sphinx
# sphinx-tabs
filelock==3.3.0
# via
# tox
# virtualenv
identify==2.3.0
# via pre-commit
idna==3.2
# via requests
imagesize==1.2.0
# via sphinx
iniconfig==1.1.1
# via pytest
jinja2==3.0.2
# via sphinx
markupsafe==2.0.1
# via jinja2
mypy-extensions==0.4.3
# via mypy
mypy==0.910
# via -r requirements/typing.in
nodeenv==1.6.0
# via pre-commit
packaging==21.0
# via
# pallets-sphinx-themes
# pytest
# sphinx
# tox
pallets-sphinx-themes==2.0.1
# via -r requirements/docs.in
pep517==0.11.0
# via pip-tools
pip-tools==6.3.0
# via -r requirements/dev.in
platformdirs==2.4.0
# via virtualenv
pluggy==1.0.0
# via
# pytest
# tox
pre-commit==2.15.0
# via -r requirements/dev.in
py==1.10.0
# via
# pytest
# tox
pygments==2.10.0
# via
# sphinx
# sphinx-tabs
pyparsing==2.4.7
# via packaging
pytest==6.2.5
# via -r requirements/tests.in
pytz==2021.3
# via babel
pyyaml==5.4.1
# via pre-commit
requests==2.26.0
# via sphinx
six==1.16.0
# via
# tox
# virtualenv
snowballstemmer==2.1.0
# via sphinx
sphinx-issues==1.2.0
# via -r requirements/docs.in
sphinx-tabs==3.2.0
# via -r requirements/docs.in
sphinx==4.2.0
# via
# -r requirements/docs.in
# pallets-sphinx-themes
# sphinx-issues
# sphinx-tabs
# sphinxcontrib-log-cabinet
sphinxcontrib-applehelp==1.0.2
# via sphinx
sphinxcontrib-devhelp==1.0.2
# via sphinx
sphinxcontrib-htmlhelp==2.0.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-log-cabinet==1.0.1
# via -r requirements/docs.in
sphinxcontrib-qthelp==1.0.3
# via sphinx
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
toml==0.10.2
# via
# mypy
# pre-commit
# pytest
# tox
tomli==1.2.1
# via pep517
tox==3.24.4
# via -r requirements/dev.in
typing-extensions==3.10.0.2
# via mypy
urllib3==1.26.7
# via requests
virtualenv==20.8.1
# via
# pre-commit
# tox
wheel==0.37.0
# via pip-tools
# The following packages are considered to be unsafe in a requirements file:
# pip
# setuptools

5
requirements/docs.in Normal file
View File

@ -0,0 +1,5 @@
Pallets-Sphinx-Themes
Sphinx
sphinx-issues
sphinxcontrib-log-cabinet
sphinx-tabs

74
requirements/docs.txt Normal file
View File

@ -0,0 +1,74 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile requirements/docs.in
#
alabaster==0.7.12
# via sphinx
babel==2.9.1
# via sphinx
certifi==2021.5.30
# via requests
charset-normalizer==2.0.6
# via requests
docutils==0.16
# via
# sphinx
# sphinx-tabs
idna==3.2
# via requests
imagesize==1.2.0
# via sphinx
jinja2==3.0.2
# via sphinx
markupsafe==2.0.1
# via jinja2
packaging==21.0
# via
# pallets-sphinx-themes
# sphinx
pallets-sphinx-themes==2.0.1
# via -r requirements/docs.in
pygments==2.10.0
# via
# sphinx
# sphinx-tabs
pyparsing==2.4.7
# via packaging
pytz==2021.3
# via babel
requests==2.26.0
# via sphinx
snowballstemmer==2.1.0
# via sphinx
sphinx-issues==1.2.0
# via -r requirements/docs.in
sphinx-tabs==3.2.0
# via -r requirements/docs.in
sphinx==4.2.0
# via
# -r requirements/docs.in
# pallets-sphinx-themes
# sphinx-issues
# sphinx-tabs
# sphinxcontrib-log-cabinet
sphinxcontrib-applehelp==1.0.2
# via sphinx
sphinxcontrib-devhelp==1.0.2
# via sphinx
sphinxcontrib-htmlhelp==2.0.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-log-cabinet==1.0.1
# via -r requirements/docs.in
sphinxcontrib-qthelp==1.0.3
# via sphinx
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
urllib3==1.26.7
# via requests
# The following packages are considered to be unsafe in a requirements file:
# setuptools

1
requirements/tests.in Normal file
View File

@ -0,0 +1 @@
pytest

22
requirements/tests.txt Normal file
View File

@ -0,0 +1,22 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile requirements/tests.in
#
attrs==21.2.0
# via pytest
iniconfig==1.1.1
# via pytest
packaging==21.0
# via pytest
pluggy==1.0.0
# via pytest
py==1.10.0
# via pytest
pyparsing==2.4.7
# via packaging
pytest==6.2.5
# via -r requirements/tests.in
toml==0.10.2
# via pytest

1
requirements/typing.in Normal file
View File

@ -0,0 +1 @@
mypy

14
requirements/typing.txt Normal file
View File

@ -0,0 +1,14 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile requirements/typing.in
#
mypy-extensions==0.4.3
# via mypy
mypy==0.910
# via -r requirements/typing.in
toml==0.10.2
# via mypy
typing-extensions==3.10.0.2
# via mypy

107
setup.cfg
View File

@ -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

View File

@ -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'",
],
)

View File

@ -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.*

View File

@ -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

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@
click

View File

@ -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"

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
src/click/py.typed Normal file
View File

View File

@ -0,0 +1,581 @@
import os
import re
import typing as t
from gettext import gettext as _
from .core import Argument
from .core import BaseCommand
from .core import Context
from .core import MultiCommand
from .core import Option
from .core import Parameter
from .core import ParameterSource
from .parser import split_arg_string
from .utils import echo
def shell_complete(
cli: BaseCommand,
ctx_args: t.Dict[str, t.Any],
prog_name: str,
complete_var: str,
instruction: str,
) -> int:
"""Perform shell completion for the given CLI program.
:param cli: Command being called.
:param ctx_args: Extra arguments to pass to
``cli.make_context``.
:param prog_name: Name of the executable in the shell.
:param complete_var: Name of the environment variable that holds
the completion instruction.
:param instruction: Value of ``complete_var`` with the completion
instruction and shell, in the form ``instruction_shell``.
:return: Status code to exit with.
"""
shell, _, instruction = instruction.partition("_")
comp_cls = get_completion_class(shell)
if comp_cls is None:
return 1
comp = comp_cls(cli, ctx_args, prog_name, complete_var)
if instruction == "source":
echo(comp.source())
return 0
if instruction == "complete":
echo(comp.complete())
return 0
return 1
class CompletionItem:
"""Represents a completion value and metadata about the value. The
default metadata is ``type`` to indicate special shell handling,
and ``help`` if a shell supports showing a help string next to the
value.
Arbitrary parameters can be passed when creating the object, and
accessed using ``item.attr``. If an attribute wasn't passed,
accessing it returns ``None``.
:param value: The completion suggestion.
:param type: Tells the shell script to provide special completion
support for the type. Click uses ``"dir"`` and ``"file"``.
:param help: String shown next to the value if supported.
:param kwargs: Arbitrary metadata. The built-in implementations
don't use this, but custom type completions paired with custom
shell support could use it.
"""
__slots__ = ("value", "type", "help", "_info")
def __init__(
self,
value: t.Any,
type: str = "plain",
help: t.Optional[str] = None,
**kwargs: t.Any,
) -> None:
self.value = value
self.type = type
self.help = help
self._info = kwargs
def __getattr__(self, name: str) -> t.Any:
return self._info.get(name)
# Only Bash >= 4.4 has the nosort option.
_SOURCE_BASH = """\
%(complete_func)s() {
local IFS=$'\\n'
local response
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \
%(complete_var)s=bash_complete $1)
for completion in $response; do
IFS=',' read type value <<< "$completion"
if [[ $type == 'dir' ]]; then
COMREPLY=()
compopt -o dirnames
elif [[ $type == 'file' ]]; then
COMREPLY=()
compopt -o default
elif [[ $type == 'plain' ]]; then
COMPREPLY+=($value)
fi
done
return 0
}
%(complete_func)s_setup() {
complete -o nosort -F %(complete_func)s %(prog_name)s
}
%(complete_func)s_setup;
"""
_SOURCE_ZSH = """\
#compdef %(prog_name)s
%(complete_func)s() {
local -a completions
local -a completions_with_descriptions
local -a response
(( ! $+commands[%(prog_name)s] )) && return 1
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \
%(complete_var)s=zsh_complete %(prog_name)s)}")
for type key descr in ${response}; do
if [[ "$type" == "plain" ]]; then
if [[ "$descr" == "_" ]]; then
completions+=("$key")
else
completions_with_descriptions+=("$key":"$descr")
fi
elif [[ "$type" == "dir" ]]; then
_path_files -/
elif [[ "$type" == "file" ]]; then
_path_files -f
fi
done
if [ -n "$completions_with_descriptions" ]; then
_describe -V unsorted completions_with_descriptions -U
fi
if [ -n "$completions" ]; then
compadd -U -V unsorted -a completions
fi
}
compdef %(complete_func)s %(prog_name)s;
"""
_SOURCE_FISH = """\
function %(complete_func)s;
set -l response;
for value in (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \
COMP_CWORD=(commandline -t) %(prog_name)s);
set response $response $value;
end;
for completion in $response;
set -l metadata (string split "," $completion);
if test $metadata[1] = "dir";
__fish_complete_directories $metadata[2];
else if test $metadata[1] = "file";
__fish_complete_path $metadata[2];
else if test $metadata[1] = "plain";
echo $metadata[2];
end;
end;
end;
complete --no-files --command %(prog_name)s --arguments \
"(%(complete_func)s)";
"""
class ShellComplete:
"""Base class for providing shell completion support. A subclass for
a given shell will override attributes and methods to implement the
completion instructions (``source`` and ``complete``).
:param cli: Command being called.
:param prog_name: Name of the executable in the shell.
:param complete_var: Name of the environment variable that holds
the completion instruction.
.. versionadded:: 8.0
"""
name: t.ClassVar[str]
"""Name to register the shell as with :func:`add_completion_class`.
This is used in completion instructions (``{name}_source`` and
``{name}_complete``).
"""
source_template: t.ClassVar[str]
"""Completion script template formatted by :meth:`source`. This must
be provided by subclasses.
"""
def __init__(
self,
cli: BaseCommand,
ctx_args: t.Dict[str, t.Any],
prog_name: str,
complete_var: str,
) -> None:
self.cli = cli
self.ctx_args = ctx_args
self.prog_name = prog_name
self.complete_var = complete_var
@property
def func_name(self) -> str:
"""The name of the shell function defined by the completion
script.
"""
safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), re.ASCII)
return f"_{safe_name}_completion"
def source_vars(self) -> t.Dict[str, t.Any]:
"""Vars for formatting :attr:`source_template`.
By default this provides ``complete_func``, ``complete_var``,
and ``prog_name``.
"""
return {
"complete_func": self.func_name,
"complete_var": self.complete_var,
"prog_name": self.prog_name,
}
def source(self) -> str:
"""Produce the shell script that defines the completion
function. By default this ``%``-style formats
:attr:`source_template` with the dict returned by
:meth:`source_vars`.
"""
return self.source_template % self.source_vars()
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
"""Use the env vars defined by the shell script to return a
tuple of ``args, incomplete``. This must be implemented by
subclasses.
"""
raise NotImplementedError
def get_completions(
self, args: t.List[str], incomplete: str
) -> t.List[CompletionItem]:
"""Determine the context and last complete command or parameter
from the complete args. Call that object's ``shell_complete``
method to get the completions for the incomplete value.
:param args: List of complete args before the incomplete value.
:param incomplete: Value being completed. May be empty.
"""
ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args)
obj, incomplete = _resolve_incomplete(ctx, args, incomplete)
return obj.shell_complete(ctx, incomplete)
def format_completion(self, item: CompletionItem) -> str:
"""Format a completion item into the form recognized by the
shell script. This must be implemented by subclasses.
:param item: Completion item to format.
"""
raise NotImplementedError
def complete(self) -> str:
"""Produce the completion data to send back to the shell.
By default this calls :meth:`get_completion_args`, gets the
completions, then calls :meth:`format_completion` for each
completion.
"""
args, incomplete = self.get_completion_args()
completions = self.get_completions(args, incomplete)
out = [self.format_completion(item) for item in completions]
return "\n".join(out)
class BashComplete(ShellComplete):
"""Shell completion for Bash."""
name = "bash"
source_template = _SOURCE_BASH
def _check_version(self) -> None:
import subprocess
output = subprocess.run(
["bash", "-c", "echo ${BASH_VERSION}"], stdout=subprocess.PIPE
)
match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode())
if match is not None:
major, minor = match.groups()
if major < "4" or major == "4" and minor < "4":
raise RuntimeError(
_(
"Shell completion is not supported for Bash"
" versions older than 4.4."
)
)
else:
raise RuntimeError(
_("Couldn't detect Bash version, shell completion is not supported.")
)
def source(self) -> str:
self._check_version()
return super().source()
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
cwords = split_arg_string(os.environ["COMP_WORDS"])
cword = int(os.environ["COMP_CWORD"])
args = cwords[1:cword]
try:
incomplete = cwords[cword]
except IndexError:
incomplete = ""
return args, incomplete
def format_completion(self, item: CompletionItem) -> str:
return f"{item.type},{item.value}"
class ZshComplete(ShellComplete):
"""Shell completion for Zsh."""
name = "zsh"
source_template = _SOURCE_ZSH
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
cwords = split_arg_string(os.environ["COMP_WORDS"])
cword = int(os.environ["COMP_CWORD"])
args = cwords[1:cword]
try:
incomplete = cwords[cword]
except IndexError:
incomplete = ""
return args, incomplete
def format_completion(self, item: CompletionItem) -> str:
return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}"
class FishComplete(ShellComplete):
"""Shell completion for Fish."""
name = "fish"
source_template = _SOURCE_FISH
def get_completion_args(self) -> t.Tuple[t.List[str], str]:
cwords = split_arg_string(os.environ["COMP_WORDS"])
incomplete = os.environ["COMP_CWORD"]
args = cwords[1:]
# Fish stores the partial word in both COMP_WORDS and
# COMP_CWORD, remove it from complete args.
if incomplete and args and args[-1] == incomplete:
args.pop()
return args, incomplete
def format_completion(self, item: CompletionItem) -> str:
if item.help:
return f"{item.type},{item.value}\t{item.help}"
return f"{item.type},{item.value}"
_available_shells: t.Dict[str, t.Type[ShellComplete]] = {
"bash": BashComplete,
"fish": FishComplete,
"zsh": ZshComplete,
}
def add_completion_class(
cls: t.Type[ShellComplete], name: t.Optional[str] = None
) -> None:
"""Register a :class:`ShellComplete` subclass under the given name.
The name will be provided by the completion instruction environment
variable during completion.
:param cls: The completion class that will handle completion for the
shell.
:param name: Name to register the class under. Defaults to the
class's ``name`` attribute.
"""
if name is None:
name = cls.name
_available_shells[name] = cls
def get_completion_class(shell: str) -> t.Optional[t.Type[ShellComplete]]:
"""Look up a registered :class:`ShellComplete` subclass by the name
provided by the completion instruction environment variable. If the
name isn't registered, returns ``None``.
:param shell: Name the class is registered under.
"""
return _available_shells.get(shell)
def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool:
"""Determine if the given parameter is an argument that can still
accept values.
:param ctx: Invocation context for the command represented by the
parsed complete args.
:param param: Argument object being checked.
"""
if not isinstance(param, Argument):
return False
assert param.name is not None
value = ctx.params[param.name]
return (
param.nargs == -1
or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE
or (
param.nargs > 1
and isinstance(value, (tuple, list))
and len(value) < param.nargs
)
)
def _start_of_option(value: str) -> bool:
"""Check if the value looks like the start of an option."""
if not value:
return False
c = value[0]
# Allow "/" since that starts a path.
return not c.isalnum() and c != "/"
def _is_incomplete_option(args: t.List[str], param: Parameter) -> bool:
"""Determine if the given parameter is an option that needs a value.
:param args: List of complete args before the incomplete value.
:param param: Option object being checked.
"""
if not isinstance(param, Option):
return False
if param.is_flag:
return False
last_option = None
for index, arg in enumerate(reversed(args)):
if index + 1 > param.nargs:
break
if _start_of_option(arg):
last_option = arg
return last_option is not None and last_option in param.opts
def _resolve_context(
cli: BaseCommand, ctx_args: t.Dict[str, t.Any], prog_name: str, args: t.List[str]
) -> Context:
"""Produce the context hierarchy starting with the command and
traversing the complete arguments. This only follows the commands,
it doesn't trigger input prompts or callbacks.
:param cli: Command being called.
:param prog_name: Name of the executable in the shell.
:param args: List of complete args before the incomplete value.
"""
ctx_args["resilient_parsing"] = True
ctx = cli.make_context(prog_name, args.copy(), **ctx_args)
args = ctx.protected_args + ctx.args
while args:
command = ctx.command
if isinstance(command, MultiCommand):
if not command.chain:
name, cmd, args = command.resolve_command(ctx, args)
if cmd is None:
return ctx
ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True)
args = ctx.protected_args + ctx.args
else:
while args:
name, cmd, args = command.resolve_command(ctx, args)
if cmd is None:
return ctx
sub_ctx = cmd.make_context(
name,
args,
parent=ctx,
allow_extra_args=True,
allow_interspersed_args=False,
resilient_parsing=True,
)
args = sub_ctx.args
ctx = sub_ctx
args = [*sub_ctx.protected_args, *sub_ctx.args]
else:
break
return ctx
def _resolve_incomplete(
ctx: Context, args: t.List[str], incomplete: str
) -> t.Tuple[t.Union[BaseCommand, Parameter], str]:
"""Find the Click object that will handle the completion of the
incomplete value. Return the object and the incomplete value.
:param ctx: Invocation context for the command represented by
the parsed complete args.
:param args: List of complete args before the incomplete value.
:param incomplete: Value being completed. May be empty.
"""
# Different shells treat an "=" between a long option name and
# value differently. Might keep the value joined, return the "="
# as a separate item, or return the split name and value. Always
# split and discard the "=" to make completion easier.
if incomplete == "=":
incomplete = ""
elif "=" in incomplete and _start_of_option(incomplete):
name, _, incomplete = incomplete.partition("=")
args.append(name)
# The "--" marker tells Click to stop treating values as options
# even if they start with the option character. If it hasn't been
# given and the incomplete arg looks like an option, the current
# command will provide option name completions.
if "--" not in args and _start_of_option(incomplete):
return ctx.command, incomplete
params = ctx.command.get_params(ctx)
# If the last complete arg is an option name with an incomplete
# value, the option will provide value completions.
for param in params:
if _is_incomplete_option(args, param):
return param, incomplete
# It's not an option name or value. The first argument without a
# parsed value will provide value completions.
for param in params:
if _is_incomplete_argument(ctx, param):
return param, incomplete
# There were no unparsed arguments, the command may be a group that
# will provide command name completions.
return ctx.command, incomplete

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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():

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,112 @@
import click
def test_command_context_class():
"""A command with a custom ``context_class`` should produce a
context using that type.
"""
class CustomContext(click.Context):
pass
class CustomCommand(click.Command):
context_class = CustomContext
command = CustomCommand("test")
context = command.make_context("test", [])
assert isinstance(context, CustomContext)
def test_context_invoke_type(runner):
"""A command invoked from a custom context should have a new
context with the same type.
"""
class CustomContext(click.Context):
pass
class CustomCommand(click.Command):
context_class = CustomContext
@click.command()
@click.argument("first_id", type=int)
@click.pass_context
def second(ctx, first_id):
assert isinstance(ctx, CustomContext)
assert id(ctx) != first_id
@click.command(cls=CustomCommand)
@click.pass_context
def first(ctx):
assert isinstance(ctx, CustomContext)
ctx.invoke(second, first_id=id(ctx))
assert not runner.invoke(first).exception
def test_context_formatter_class():
"""A context with a custom ``formatter_class`` should format help
using that type.
"""
class CustomFormatter(click.HelpFormatter):
def write_heading(self, heading):
heading = click.style(heading, fg="yellow")
return super().write_heading(heading)
class CustomContext(click.Context):
formatter_class = CustomFormatter
context = CustomContext(
click.Command("test", params=[click.Option(["--value"])]), color=True
)
assert "\x1b[33mOptions\x1b[0m:" in context.get_help()
def test_group_command_class(runner):
"""A group with a custom ``command_class`` should create subcommands
of that type by default.
"""
class CustomCommand(click.Command):
pass
class CustomGroup(click.Group):
command_class = CustomCommand
group = CustomGroup()
subcommand = group.command()(lambda: None)
assert type(subcommand) is CustomCommand
subcommand = group.command(cls=click.Command)(lambda: None)
assert type(subcommand) is click.Command
def test_group_group_class(runner):
"""A group with a custom ``group_class`` should create subgroups
of that type by default.
"""
class CustomSubGroup(click.Group):
pass
class CustomGroup(click.Group):
group_class = CustomSubGroup
group = CustomGroup()
subgroup = group.group()(lambda: None)
assert type(subgroup) is CustomSubGroup
subgroup = group.command(cls=click.Group)(lambda: None)
assert type(subgroup) is click.Group
def test_group_group_class_self(runner):
"""A group with ``group_class = type`` should create subgroups of
the same type as itself.
"""
class CustomGroup(click.Group):
group_class = type
group = CustomGroup()
subgroup = group.group()(lambda: None)
assert type(subgroup) is CustomGroup

View File

@ -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