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 .. currentmodule:: click
Version 8.0.2
-------------
Released 2021-10-08
- ``is_bool_flag`` is not set to ``True`` if ``is_flag`` is ``False``.
:issue:`1925`
- Bash version detection is locale independent. :issue:`1940`
- Empty ``default`` value is not shown for ``multiple=True``.
:issue:`1969`
- Fix shell completion for arguments that start with a forward slash
such as absolute file paths. :issue:`1929`
- ``Path`` type with ``resolve_path=True`` resolves relative symlinks
to be relative to the containing directory. :issue:`1921`
- Completion does not skip Python's resource cleanup when exiting,
avoiding some unexpected warning output. :issue:`1738, 2017`
- Fix type annotation for ``type`` argument in ``prompt`` function.
:issue:`2062`
- Fix overline and italic styles, which were incorrectly added when
adding underline. :pr:`2058`
- An option with ``count=True`` will not show "[x>=0]" in help text.
:issue:`2072`
- Default values are not cast to the parameter type twice during
processing. :issue:`2085`
- Options with ``multiple`` and ``flag_value`` use the flag value
instead of leaving an internal placeholder. :issue:`2001`
Version 8.0.1
-------------
Released 2021-05-19
- Mark top-level names as exported so type checking understand imports
in user projects. :issue:`1879`
- Annotate ``Context.obj`` as ``Any`` so type checking allows all
operations on the arbitrary object. :issue:`1885`
- Fix some types that weren't available in Python 3.6.0. :issue:`1882`
- Fix type checking for iterating over ``ProgressBar`` object.
:issue:`1892`
- The ``importlib_metadata`` backport package is installed on Python <
3.8. :issue:`1889`
- Arguments with ``nargs=-1`` only use env var value if no command
line values are given. :issue:`1903`
- Flag options guess their type from ``flag_value`` if given, like
regular options do from ``default``. :issue:`1886`
- Added documentation that custom parameter types may be passed
already valid values in addition to strings. :issue:`1898`
- Resolving commands returns the name that was given, not
``command.name``, fixing an unintended change to help text and
``default_map`` lookups. When using patterns like ``AliasedGroup``,
override ``resolve_command`` to change the name that is returned if
needed. :issue:`1895`
- If a default value is invalid, it does not prevent showing help
text. :issue:`1889`
- Pass ``windows_expand_args=False`` when calling the main command to
disable pattern expansion on Windows. There is no way to escape
patterns in CMD, so if the program needs to pass them on as-is then
expansion must be disabled. :issue:`1901`
Version 8.0.0
-------------
Released 2021-05-11
- Drop support for Python 2 and 3.5.
- Colorama is always installed on Windows in order to provide style
and color support. :pr:`1784`
- Adds a repr to Command, showing the command name for friendlier
debugging. :issue:`1267`, :pr:`1295`
- Add support for distinguishing the source of a command line
parameter. :issue:`1264`, :pr:`1329`
- Add an optional parameter to ``ProgressBar.update`` to set the
``current_item``. :issue:`1226`, :pr:`1332`
- ``version_option`` uses ``importlib.metadata`` (or the
``importlib_metadata`` backport) instead of ``pkg_resources``. The
version is detected based on the package name, not the entry point
name. The Python package name must match the installed package
name, or be passed with ``package_name=``. :issue:`1582`
- If validation fails for a prompt with ``hide_input=True``, the value
is not shown in the error message. :issue:`1460`
- An ``IntRange`` or ``FloatRange`` option shows the accepted range in
its help text. :issue:`1525`, :pr:`1303`
- ``IntRange`` and ``FloatRange`` bounds can be open (``<``) instead
of closed (``<=``) by setting ``min_open`` and ``max_open``. Error
messages have changed to reflect this. :issue:`1100`
- An option defined with duplicate flag names (``"--foo/--foo"``)
raises a ``ValueError``. :issue:`1465`
- ``echo()`` will not fail when using pytest's ``capsys`` fixture on
Windows. :issue:`1590`
- Resolving commands returns the canonical command name instead of the
matched name. This makes behavior such as help text and
``Context.invoked_subcommand`` consistent when using patterns like
``AliasedGroup``. :issue:`1422`
- The ``BOOL`` type accepts the values "on" and "off". :issue:`1629`
- A ``Group`` with ``invoke_without_command=True`` will always invoke
its result callback. :issue:`1178`
- ``nargs == -1`` and ``nargs > 1`` is parsed and validated for
values from environment variables and defaults. :issue:`729`
- Detect the program name when executing a module or package with
``python -m name``. :issue:`1603`
- Include required parent arguments in help synopsis of subcommands.
:issue:`1475`
- Help for boolean flags with ``show_default=True`` shows the flag
name instead of ``True`` or ``False``. :issue:`1538`
- Non-string objects passed to ``style()`` and ``secho()`` will be
converted to string. :pr:`1146`
- ``edit(require_save=True)`` will detect saves for editors that exit
very fast on filesystems with 1 second resolution. :pr:`1050`
- New class attributes make it easier to use custom core objects
throughout an entire application. :pr:`938`
- ``Command.context_class`` controls the context created when
running the command.
- ``Context.invoke`` creates new contexts of the same type, so a
custom type will persist to invoked subcommands.
- ``Context.formatter_class`` controls the formatter used to
generate help and usage.
- ``Group.command_class`` changes the default type for
subcommands with ``@group.command()``.
- ``Group.group_class`` changes the default type for subgroups
with ``@group.group()``. Setting it to ``type`` will create
subgroups of the same type as the group itself.
- Core objects use ``super()`` consistently for better support of
subclassing.
- Use ``Context.with_resource()`` to manage resources that would
normally be used in a ``with`` statement, allowing them to be used
across subcommands and callbacks, then cleaned up when the context
ends. :pr:`1191`
- The result object returned by the test runner's ``invoke()`` method
has a ``return_value`` attribute with the value returned by the
invoked command. :pr:`1312`
- Required arguments with the ``Choice`` type show the choices in
curly braces to indicate that one is required (``{a|b|c}``).
:issue:`1272`
- If only a name is passed to ``option()``, Click suggests renaming it
to ``--name``. :pr:`1355`
- A context's ``show_default`` parameter defaults to the value from
the parent context. :issue:`1565`
- ``click.style()`` can output 256 and RGB color codes. Most modern
terminals support these codes. :pr:`1429`
- When using ``CliRunner.invoke()``, the replaced ``stdin`` file has
``name`` and ``mode`` attributes. This lets ``File`` options with
the ``-`` value match non-testing behavior. :issue:`1064`
- When creating a ``Group``, allow passing a list of commands instead
of a dict. :issue:`1339`
- When a long option name isn't valid, use ``difflib`` to make better
suggestions for possible corrections. :issue:`1446`
- Core objects have a ``to_info_dict()`` method. This gathers
information about the object's structure that could be useful for a
tool generating user-facing documentation. To get the structure of
an entire CLI, use ``Context(cli).to_info_dict()``. :issue:`461`
- Redesign the shell completion system. :issue:`1484`, :pr:`1622`
- Support Bash >= 4.4, Zsh, and Fish, with the ability for
extensions to add support for other shells.
- Allow commands, groups, parameters, and types to override their
completions suggestions.
- Groups complete the names commands were registered with, which
can differ from the name they were created with.
- The ``autocompletion`` parameter for options and arguments is
renamed to ``shell_complete``. The function must take
``ctx, param, incomplete``, must do matching rather than return
all values, and must return a list of strings or a list of
``CompletionItem``. The old name and behavior is deprecated and
will be removed in 8.1.
- The env var values used to start completion have changed order.
The shell now comes first, such as ``{shell}_source`` rather
than ``source_{shell}``, and is always required.
- Completion correctly parses command line strings with incomplete
quoting or escape sequences. :issue:`1708`
- Extra context settings (``obj=...``, etc.) are passed on to the
completion system. :issue:`942`
- Include ``--help`` option in completion. :pr:`1504`
- ``ParameterSource`` is an ``enum.Enum`` subclass. :issue:`1530`
- Boolean and UUID types strip surrounding space before converting.
:issue:`1605`
- Adjusted error message from parameter type validation to be more
consistent. Quotes are used to distinguish the invalid value.
:issue:`1605`
- The default value for a parameter with ``nargs`` > 1 and
``multiple=True`` must be a list of tuples. :issue:`1649`
- When getting the value for a parameter, the default is tried in the
same section as other sources to ensure consistent processing.
:issue:`1649`
- All parameter types accept a value that is already the correct type.
:issue:`1649`
- For shell completion, an argument is considered incomplete if its
value did not come from the command line args. :issue:`1649`
- Added ``ParameterSource.PROMPT`` to track parameter values that were
prompted for. :issue:`1649`
- Options with ``nargs`` > 1 no longer raise an error if a default is
not given. Parameters with ``nargs`` > 1 default to ``None``, and
parameters with ``multiple=True`` or ``nargs=-1`` default to an
empty tuple. :issue:`472`
- Handle empty env vars as though the option were not passed. This
extends the change introduced in 7.1 to be consistent in more cases.
:issue:`1285`
- ``Parameter.get_default()`` checks ``Context.default_map`` to
handle overrides consistently in help text, ``invoke()``, and
prompts. :issue:`1548`
- Add ``prompt_required`` param to ``Option``. When set to ``False``,
the user will only be prompted for an input if no value was passed.
:issue:`736`
- Providing the value to an option can be made optional through
``is_flag=False``, and the value can instead be prompted for or
passed in as a default value.
:issue:`549, 736, 764, 921, 1015, 1618`
- Fix formatting when ``Command.options_metavar`` is empty. :pr:`1551`
- Revert adding space between option help text that wraps.
:issue:`1831`
- The default value passed to ``prompt`` will be cast to the correct
type like an input value would be. :pr:`1517`
- Automatically generated short help messages will stop at the first
ending of a phrase or double linebreak. :issue:`1082`
- Skip progress bar render steps for efficiency with very fast
iterators by setting ``update_min_steps``. :issue:`676`
- Respect ``case_sensitive=False`` when doing shell completion for
``Choice`` :issue:`1692`
- Use ``mkstemp()`` instead of ``mktemp()`` in pager implementation.
:issue:`1752`
- If ``Option.show_default`` is a string, it is displayed even if
``default`` is ``None``. :issue:`1732`
- ``click.get_terminal_size()`` is deprecated and will be removed in
8.1. Use :func:`shutil.get_terminal_size` instead. :issue:`1736`
- Control the location of the temporary directory created by
``CLIRunner.isolated_filesystem`` by passing ``temp_dir``. A custom
directory will not be removed automatically. :issue:`395`
- ``click.confirm()`` will prompt until input is given if called with
``default=None``. :issue:`1381`
- Option prompts validate the value with the option's callback in
addition to its type. :issue:`457`
- ``confirmation_prompt`` can be set to a custom string. :issue:`723`
- Allow styled output in Jupyter on Windows. :issue:`1271`
- ``style()`` supports the ``strikethrough``, ``italic``, and
``overline`` styles. :issue:`805, 1821`
- Multiline marker is removed from short help text. :issue:`1597`
- Restore progress bar behavior of echoing only the label if the file
is not a TTY. :issue:`1138`
- Progress bar output is shown even if execution time is less than 0.5
seconds. :issue:`1648`
- Progress bar ``item_show_func`` shows the current item, not the
previous item. :issue:`1353`
- The ``Path`` param type can be passed ``path_type=pathlib.Path`` to
return a path object instead of a string. :issue:`405`
- ``TypeError`` is raised when parameter with ``multiple=True`` or
``nargs > 1`` has non-iterable default. :issue:`1749`
- Add a ``pass_meta_key`` decorator for passing a key from
``Context.meta``. This is useful for extensions using ``meta`` to
store information. :issue:`1739`
- ``Path`` ``resolve_path`` resolves symlinks on Windows Python < 3.8.
:issue:`1813`
- Command deprecation notice appears at the start of the help text, as
well as in the short help. The notice is not in all caps.
:issue:`1791`
- When taking arguments from ``sys.argv`` on Windows, glob patterns,
user dir, and env vars are expanded. :issue:`1096`
- Marked messages shown by the CLI with ``gettext()`` to allow
applications to translate Click's built-in strings. :issue:`303`
- Writing invalid characters to ``stderr`` when using the test runner
does not raise a ``UnicodeEncodeError``. :issue:`848`
- Fix an issue where ``readline`` would clear the entire ``prompt()``
line instead of only the input when pressing backspace. :issue:`665`
- Add all kwargs passed to ``Context.invoke()`` to ``ctx.params``.
Fixes an inconsistency when nesting ``Context.forward()`` calls.
:issue:`1568`
- The ``MultiCommand.resultcallback`` decorator is renamed to
``result_callback``. The old name is deprecated. :issue:`1160`
- Fix issues with ``CliRunner`` output when using ``echo_stdin=True``.
:issue:`1101`
- Fix a bug of ``click.utils.make_default_short_help`` for which the
returned string could be as long as ``max_width + 3``. :issue:`1849`
- When defining a parameter, ``default`` is validated with
``multiple`` and ``nargs``. More validation is done for values being
processed as well. :issue:`1806`
- ``HelpFormatter.write_text`` uses the full line width when wrapping
text. :issue:`1871`
Version 7.1.2 Version 7.1.2
------------- -------------
@ -46,7 +328,7 @@ Released 2020-03-09
:issue:`1277`, :pr:`1318` :issue:`1277`, :pr:`1318`
- Add ``no_args_is_help`` option to ``click.Command``, defaults to - Add ``no_args_is_help`` option to ``click.Command``, defaults to
False :pr:`1167` False :pr:`1167`
- Add ``show_defaults`` parameter to ``Context`` to enable showing - Add ``show_default`` parameter to ``Context`` to enable showing
defaults globally. :issue:`1018` defaults globally. :issue:`1018`
- Handle ``env MYPATH=''`` as though the option were not passed. - Handle ``env MYPATH=''`` as though the option were not passed.
:issue:`1196` :issue:`1196`
@ -90,6 +372,8 @@ Released 2020-03-09
- Make the warning about old 2-arg parameter callbacks a deprecation - Make the warning about old 2-arg parameter callbacks a deprecation
warning, to be removed in 8.0. This has been a warning since Click warning, to be removed in 8.0. This has been a warning since Click
2.0. :pr:`1492` 2.0. :pr:`1492`
- Adjust error messages to standardize the types of quotes used so
they match error messages from Python.
Version 7.0 Version 7.0
@ -480,15 +764,16 @@ Version 3.0
Released 2014-08-12, codename "clonk clonk" Released 2014-08-12, codename "clonk clonk"
- Formatter now no longer attempts to accomodate for terminals smaller - Formatter now no longer attempts to accommodate for terminals
than 50 characters. If that happens it just assumes a minimal width. smaller than 50 characters. If that happens it just assumes a
minimal width.
- Added a way to not swallow exceptions in the test system. - Added a way to not swallow exceptions in the test system.
- Added better support for colors with pagers and ways to override the - Added better support for colors with pagers and ways to override the
autodetection. autodetection.
- The CLI runner's result object now has a traceback attached. - The CLI runner's result object now has a traceback attached.
- Improved automatic short help detection to work better with dots - Improved automatic short help detection to work better with dots
that do not terminate sentences. that do not terminate sentences.
- When definining options without actual valid option strings now, - When defining options without actual valid option strings now,
Click will give an error message instead of silently passing. This Click will give an error message instead of silently passing. This
should catch situations where users wanted to created arguments should catch situations where users wanted to created arguments
instead of options. instead of options.

76
CODE_OF_CONDUCT.md Normal file
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 CHANGES.rst
include tox.ini include tox.ini
include requirements/*.txt
graft artwork graft artwork
graft docs graft docs
prune docs/_build prune docs/_build
graft examples graft examples
graft tests graft tests
include src/click/py.typed
global-exclude *.pyc global-exclude *.pyc

100
PKG-INFO
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 install -U click
.. _pip: https://pip.pypa.io/en/stable/quickstart/ .. _pip: https://pip.pypa.io/en/stable/getting-started/
A Simple Example A Simple Example
@ -70,10 +70,11 @@ donate today`_.
Links Links
----- -----
- Website: https://palletsprojects.com/p/click/
- Documentation: https://click.palletsprojects.com/ - Documentation: https://click.palletsprojects.com/
- Releases: https://pypi.org/project/click/ - Changes: https://click.palletsprojects.com/changes/
- Code: https://github.com/pallets/click - PyPI Releases: https://pypi.org/project/click/
- Issue tracker: https://github.com/pallets/click/issues - Source Code: https://github.com/pallets/click
- Test status: https://dev.azure.com/pallets/click/_build - Issue Tracker: https://github.com/pallets/click/issues
- Official chat: https://discord.gg/t6rrQZH - Website: https://palletsprojects.com/p/click
- Twitter: https://twitter.com/PalletsTeam
- Chat: https://discord.gg/pallets

BIN
docs/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -13,7 +13,7 @@ Command Aliases
--------------- ---------------
Many tools support aliases for commands (see `Command alias example Many tools support aliases for commands (see `Command alias example
<https://github.com/pallets/click/tree/master/examples/aliases>`_). <https://github.com/pallets/click/tree/main/examples/aliases>`_).
For instance, you can configure ``git`` to accept ``git ci`` as alias for For instance, you can configure ``git`` to accept ``git ci`` as alias for
``git commit``. Other tools also support auto-discovery for aliases by ``git commit``. Other tools also support auto-discovery for aliases by
automatically shortening them. automatically shortening them.
@ -35,7 +35,6 @@ it would accept ``pus`` as an alias (so long as it was unique):
.. click:example:: .. click:example::
class AliasedGroup(click.Group): class AliasedGroup(click.Group):
def get_command(self, ctx, cmd_name): def get_command(self, ctx, cmd_name):
rv = click.Group.get_command(self, ctx, cmd_name) rv = click.Group.get_command(self, ctx, cmd_name)
if rv is not None: if rv is not None:
@ -46,7 +45,12 @@ it would accept ``pus`` as an alias (so long as it was unique):
return None return None
elif len(matches) == 1: elif len(matches) == 1:
return click.Group.get_command(self, ctx, matches[0]) return click.Group.get_command(self, ctx, matches[0])
ctx.fail('Too many matches: %s' % ', '.join(sorted(matches))) ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")
def resolve_command(self, ctx, args):
# always return the full command name
_, cmd, args = super().resolve_command(ctx, args)
return cmd.name, cmd, args
And it can then be used like this: And it can then be used like this:
@ -92,7 +96,7 @@ it's good to know that the system works this way.
@click.option('--url', callback=open_url) @click.option('--url', callback=open_url)
def cli(url, fp=None): def cli(url, fp=None):
if fp is not None: if fp is not None:
click.echo('%s: %s' % (url, fp.code)) click.echo(f"{url}: {fp.code}")
In this case the callback returns the URL unchanged but also passes a In this case the callback returns the URL unchanged but also passes a
second ``fp`` value to the callback. What's more recommended is to pass second ``fp`` value to the callback. What's more recommended is to pass
@ -116,7 +120,7 @@ the information in a wrapper however:
@click.option('--url', callback=open_url) @click.option('--url', callback=open_url)
def cli(url): def cli(url):
if url is not None: if url is not None:
click.echo('%s: %s' % (url.url, url.fp.code)) click.echo(f"{url.url}: {url.fp.code}")
Token Normalization Token Normalization
@ -140,7 +144,7 @@ function that converts the token to lowercase:
@click.command(context_settings=CONTEXT_SETTINGS) @click.command(context_settings=CONTEXT_SETTINGS)
@click.option('--name', default='Pete') @click.option('--name', default='Pete')
def cli(name): def cli(name):
click.echo('Name: %s' % name) click.echo(f"Name: {name}")
And how it works on the command line: And how it works on the command line:
@ -171,7 +175,7 @@ Example:
@cli.command() @cli.command()
@click.option('--count', default=1) @click.option('--count', default=1)
def test(count): def test(count):
click.echo('Count: %d' % count) click.echo(f'Count: {count}')
@cli.command() @cli.command()
@click.option('--count', default=1) @click.option('--count', default=1)
@ -300,7 +304,7 @@ In the end you end up with something like this:
"""A fake wrapper around Python's timeit.""" """A fake wrapper around Python's timeit."""
cmdline = ['echo', 'python', '-mtimeit'] + list(timeit_args) cmdline = ['echo', 'python', '-mtimeit'] + list(timeit_args)
if verbose: if verbose:
click.echo('Invoking: %s' % ' '.join(cmdline)) click.echo(f"Invoking: {' '.join(cmdline)}")
call(cmdline) call(cmdline)
And what it looks like: And what it looks like:
@ -379,3 +383,106 @@ do. However if you do use this for threading you need to be very careful
as the vast majority of the context is not thread safe! You are only as the vast majority of the context is not thread safe! You are only
allowed to read from the context, but not to perform any modifications on allowed to read from the context, but not to perform any modifications on
it. it.
Detecting the Source of a Parameter
-----------------------------------
In some situations it's helpful to understand whether or not an option
or parameter came from the command line, the environment, the default
value, or :attr:`Context.default_map`. The
:meth:`Context.get_parameter_source` method can be used to find this
out. It will return a member of the :class:`~click.core.ParameterSource`
enum.
.. click:example::
@click.command()
@click.argument('port', nargs=1, default=8080, envvar="PORT")
@click.pass_context
def cli(ctx, port):
source = ctx.get_parameter_source("port")
click.echo(f"Port came from {source.name}")
.. click:run::
invoke(cli, prog_name='cli', args=['8080'])
println()
invoke(cli, prog_name='cli', args=[], env={"PORT": "8080"})
println()
invoke(cli, prog_name='cli', args=[])
println()
Managing Resources
------------------
It can be useful to open a resource in a group, to be made available to
subcommands. Many types of resources need to be closed or otherwise
cleaned up after use. The standard way to do this in Python is by using
a context manager with the ``with`` statement.
For example, the ``Repo`` class from :doc:`complex` might actually be
defined as a context manager:
.. code-block:: python
class Repo:
def __init__(self, home=None):
self.home = os.path.abspath(home or ".")
self.db = None
def __enter__(self):
path = os.path.join(self.home, "repo.db")
self.db = open_database(path)
def __exit__(self, exc_type, exc_value, tb):
self.db.close()
Ordinarily, it would be used with the ``with`` statement:
.. code-block:: python
with Repo() as repo:
repo.db.query(...)
However, a ``with`` block in a group would exit and close the database
before it could be used by a subcommand.
Instead, use the context's :meth:`~click.Context.with_resource` method
to enter the context manager and return the resource. When the group and
any subcommands finish, the context's resources are cleaned up.
.. code-block:: python
@click.group()
@click.option("--repo-home", default=".repo")
@click.pass_context
def cli(ctx, repo_home):
ctx.obj = ctx.with_resource(Repo(repo_home))
@cli.command()
@click.pass_obj
def log(obj):
# obj is the repo opened in the cli group
for entry in obj.db.query(...):
click.echo(entry)
If the resource isn't a context manager, usually it can be wrapped in
one using something from :mod:`contextlib`. If that's not possible, use
the context's :meth:`~click.Context.call_on_close` method to register a
cleanup function.
.. code-block:: python
@click.group()
@click.option("--name", default="repo.db")
@click.pass_context
def cli(ctx, repo_home):
ctx.obj = db = open_db(repo_home)
@ctx.call_on_close
def close_db():
db.record_use()
db.save()
db.close()

View file

@ -31,6 +31,9 @@ Decorators
.. autofunction:: make_pass_decorator .. autofunction:: make_pass_decorator
.. autofunction:: click.decorators.pass_meta_key
Utilities Utilities
--------- ---------
@ -108,6 +111,11 @@ Context
.. autofunction:: get_current_context .. autofunction:: get_current_context
.. autoclass:: click.core.ParameterSource
:members:
:member-order: bysource
Types Types
----- -----
@ -131,6 +139,10 @@ Types
.. autoclass:: IntRange .. autoclass:: IntRange
.. autoclass:: FloatRange
.. autoclass:: DateTime
.. autoclass:: Tuple .. autoclass:: Tuple
.. autoclass:: ParamType .. autoclass:: ParamType
@ -169,6 +181,24 @@ Parsing
.. autoclass:: OptionParser .. autoclass:: OptionParser
:members: :members:
Shell Completion
----------------
See :doc:`/shell-completion` for information about enabling and
customizing Click's shell completion system.
.. currentmodule:: click.shell_completion
.. autoclass:: CompletionItem
.. autoclass:: ShellComplete
:members:
:member-order: bysource
.. autofunction:: add_completion_class
Testing Testing
------- -------

View file

@ -55,7 +55,7 @@ Example:
def copy(src, dst): def copy(src, dst):
"""Move file SRC to DST.""" """Move file SRC to DST."""
for fn in src: for fn in src:
click.echo('move %s to folder %s' % (fn, dst)) click.echo(f"move {fn} to folder {dst}")
And what it looks like: And what it looks like:

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.group()
@click.option('--debug/--no-debug', default=False) @click.option('--debug/--no-debug', default=False)
def cli(debug): def cli(debug):
click.echo('Debug mode is %s' % ('on' if debug else 'off')) click.echo(f"Debug mode is {'on' if debug else 'off'}")
@cli.command() # @cli, not @click! @cli.command() # @cli, not @click!
def sync(): def sync():
@ -96,7 +96,7 @@ script like this:
@cli.command() @cli.command()
@click.pass_context @click.pass_context
def sync(ctx): def sync(ctx):
click.echo('Debug is %s' % (ctx.obj['DEBUG'] and 'on' or 'off')) click.echo(f"Debug is {'on' if ctx.obj['DEBUG'] else 'off'}")
if __name__ == '__main__': if __name__ == '__main__':
cli(obj={}) cli(obj={})
@ -166,7 +166,7 @@ Example:
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
click.echo('I was invoked without subcommand') click.echo('I was invoked without subcommand')
else: else:
click.echo('I am about to invoke %s' % ctx.invoked_subcommand) click.echo(f"I am about to invoke {ctx.invoked_subcommand}")
@cli.command() @cli.command()
def sync(): def sync():
@ -202,7 +202,7 @@ A custom multi command just needs to implement a list and load method:
def list_commands(self, ctx): def list_commands(self, ctx):
rv = [] rv = []
for filename in os.listdir(plugin_folder): for filename in os.listdir(plugin_folder):
if filename.endswith('.py'): if filename.endswith('.py') and filename != '__init__.py':
rv.append(filename[:-3]) rv.append(filename[:-3])
rv.sort() rv.sort()
return rv return rv
@ -287,7 +287,7 @@ Multi Command Chaining
Sometimes it is useful to be allowed to invoke more than one subcommand in Sometimes it is useful to be allowed to invoke more than one subcommand in
one go. For instance if you have installed a setuptools package before one go. For instance if you have installed a setuptools package before
you might be familiar with the ``setup.py sdist bdist_wheel upload`` you might be familiar with the ``setup.py sdist bdist_wheel upload``
command chain which invokes ``dist`` before ``bdist_wheel`` before command chain which invokes ``sdist`` before ``bdist_wheel`` before
``upload``. Starting with Click 3.0 this is very simple to implement. ``upload``. Starting with Click 3.0 this is very simple to implement.
All you have to do is to pass ``chain=True`` to your multicommand: All you have to do is to pass ``chain=True`` to your multicommand:
@ -351,7 +351,7 @@ how to do its processing. At that point it then returns a processing
function and returns. function and returns.
Where do the returned functions go? The chained multicommand can register Where do the returned functions go? The chained multicommand can register
a callback with :meth:`MultiCommand.resultcallback` that goes over all a callback with :meth:`MultiCommand.result_callback` that goes over all
these functions and then invoke them. these functions and then invoke them.
To make this a bit more concrete consider this example: To make this a bit more concrete consider this example:
@ -363,7 +363,7 @@ To make this a bit more concrete consider this example:
def cli(input): def cli(input):
pass pass
@cli.resultcallback() @cli.result_callback()
def process_pipeline(processors, input): def process_pipeline(processors, input):
iterator = (x.rstrip('\r\n') for x in input) iterator = (x.rstrip('\r\n') for x in input)
for processor in processors: for processor in processors:
@ -422,7 +422,7 @@ to not use the file type and manually open the file through
For a more complex example that also improves upon handling of the For a more complex example that also improves upon handling of the
pipelines have a look at the `imagepipe multi command chaining demo pipelines have a look at the `imagepipe multi command chaining demo
<https://github.com/pallets/click/tree/master/examples/imagepipe>`__ in <https://github.com/pallets/click/tree/main/examples/imagepipe>`__ in
the Click repository. It implements a pipeline based image editing tool the Click repository. It implements a pipeline based image editing tool
that has a nice internal structure for the pipelines. that has a nice internal structure for the pipelines.
@ -466,7 +466,7 @@ Example usage:
@cli.command() @cli.command()
@click.option('--port', default=8000) @click.option('--port', default=8000)
def runserver(port): def runserver(port):
click.echo('Serving on http://127.0.0.1:%d/' % port) click.echo(f"Serving on http://127.0.0.1:{port}/")
if __name__ == '__main__': if __name__ == '__main__':
cli(default_map={ cli(default_map={
@ -512,7 +512,7 @@ This example does the same as the previous example:
@cli.command() @cli.command()
@click.option('--port', default=8000) @click.option('--port', default=8000)
def runserver(port): def runserver(port):
click.echo('Serving on http://127.0.0.1:%d/' % port) click.echo(f"Serving on http://127.0.0.1:{port}/")
if __name__ == '__main__': if __name__ == '__main__':
cli() cli()

View file

@ -1,12 +1,17 @@
from pallets_sphinx_themes import get_version from pallets_sphinx_themes import get_version
from pallets_sphinx_themes import ProjectLink from pallets_sphinx_themes import ProjectLink
import click._compat
# compat until pallets-sphinx-themes is updated
click._compat.text_type = str
# Project -------------------------------------------------------------- # Project --------------------------------------------------------------
project = "Click" project = "Click"
copyright = "2014 Pallets" copyright = "2014 Pallets"
author = "Pallets" author = "Pallets"
release, version = get_version("Click", version_length=1) release, version = get_version("Click")
# General -------------------------------------------------------------- # General --------------------------------------------------------------
@ -17,7 +22,9 @@ extensions = [
"sphinxcontrib.log_cabinet", "sphinxcontrib.log_cabinet",
"pallets_sphinx_themes", "pallets_sphinx_themes",
"sphinx_issues", "sphinx_issues",
"sphinx_tabs.tabs",
] ]
autodoc_typehints = "description"
intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
issues_github_path = "pallets/click" issues_github_path = "pallets/click"
@ -27,18 +34,20 @@ html_theme = "click"
html_theme_options = {"index_sidebar_logo": False} html_theme_options = {"index_sidebar_logo": False}
html_context = { html_context = {
"project_links": [ "project_links": [
ProjectLink("Donate to Pallets", "https://palletsprojects.com/donate"), ProjectLink("Donate", "https://palletsprojects.com/donate"),
ProjectLink("Click Website", "https://palletsprojects.com/p/click/"), ProjectLink("PyPI Releases", "https://pypi.org/project/click/"),
ProjectLink("PyPI releases", "https://pypi.org/project/click/"),
ProjectLink("Source Code", "https://github.com/pallets/click/"), ProjectLink("Source Code", "https://github.com/pallets/click/"),
ProjectLink("Issue Tracker", "https://github.com/pallets/click/issues/"), ProjectLink("Issue Tracker", "https://github.com/pallets/click/issues/"),
ProjectLink("Website", "https://palletsprojects.com/p/click"),
ProjectLink("Twitter", "https://twitter.com/PalletsTeam"),
ProjectLink("Chat", "https://discord.gg/pallets"),
] ]
} }
html_sidebars = { html_sidebars = {
"index": ["project.html", "localtoc.html", "searchbox.html"], "index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"],
"**": ["localtoc.html", "relations.html", "searchbox.html"], "**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"],
} }
singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]} singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]}
html_static_path = ["_static"] html_static_path = ["_static"]
html_favicon = "_static/click-icon.png" html_favicon = "_static/click-icon.png"
html_logo = "_static/click-logo-sidebar.png" html_logo = "_static/click-logo-sidebar.png"

View file

@ -24,7 +24,7 @@ Simple example:
def hello(count, name): def hello(count, name):
"""This script prints hello NAME COUNT times.""" """This script prints hello NAME COUNT times."""
for x in range(count): for x in range(count):
click.echo('Hello %s!' % name) click.echo(f"Hello {name}!")
And what it looks like: And what it looks like:
@ -173,7 +173,7 @@ desired. This can be customized at all levels:
def hello(count, name): def hello(count, name):
"""This script prints hello <name> <int> times.""" """This script prints hello <name> <int> times."""
for x in range(count): for x in range(count):
click.echo('Hello %s!' % name) click.echo(f"Hello {name}!")
Example: Example:

View file

@ -36,7 +36,7 @@ What does it look like? Here is an example of a simple Click program:
def hello(count, name): def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times.""" """Simple program that greets NAME for a total of COUNT times."""
for x in range(count): for x in range(count):
click.echo('Hello %s!' % name) click.echo(f"Hello {name}!")
if __name__ == '__main__': if __name__ == '__main__':
hello() hello()
@ -79,9 +79,9 @@ usage patterns.
advanced advanced
testing testing
utils utils
bashcomplete shell-completion
exceptions exceptions
python3 unicode-support
wincmd wincmd
API Reference API Reference
@ -102,6 +102,6 @@ Miscellaneous Pages
:maxdepth: 2 :maxdepth: 2
contrib contrib
changelog
upgrading upgrading
license license
changes

View file

@ -85,7 +85,7 @@ simply pass in `required=True` as an argument to the decorator.
@click.option('--from', '-f', 'from_') @click.option('--from', '-f', 'from_')
@click.option('--to', '-t') @click.option('--to', '-t')
def reserved_param_name(from_, to): def reserved_param_name(from_, to):
click.echo('from %s to %s' % (from_, to)) click.echo(f"from {from_} to {to}")
And on the command line: And on the command line:
@ -121,7 +121,8 @@ the ``nargs`` parameter. The values are then stored as a tuple.
@click.command() @click.command()
@click.option('--pos', nargs=2, type=float) @click.option('--pos', nargs=2, type=float)
def findme(pos): def findme(pos):
click.echo('%s / %s' % pos) a, b = pos
click.echo(f"{a} / {b}")
And on the command line: And on the command line:
@ -146,7 +147,8 @@ the tuple. For this you can directly specify a tuple as type:
@click.command() @click.command()
@click.option('--item', type=(str, int)) @click.option('--item', type=(str, int))
def putitem(item): def putitem(item):
click.echo('name=%s id=%d' % item) name, id = item
click.echo(f"name={name} id={id}")
And on the command line: And on the command line:
@ -163,7 +165,8 @@ used. The above example is thus equivalent to this:
@click.command() @click.command()
@click.option('--item', nargs=2, type=click.Tuple([str, int])) @click.option('--item', nargs=2, type=click.Tuple([str, int]))
def putitem(item): def putitem(item):
click.echo('name=%s id=%d' % item) name, id = item
click.echo(f"name={name} id={id}")
.. _multiple-options: .. _multiple-options:
@ -212,7 +215,7 @@ for instance:
@click.command() @click.command()
@click.option('-v', '--verbose', count=True) @click.option('-v', '--verbose', count=True)
def log(verbose): def log(verbose):
click.echo('Verbosity: %s' % verbose) click.echo(f"Verbosity: {verbose}")
And on the command line: And on the command line:
@ -250,6 +253,7 @@ And on the command line:
invoke(info, args=['--shout']) invoke(info, args=['--shout'])
invoke(info, args=['--no-shout']) invoke(info, args=['--no-shout'])
invoke(info)
If you really don't want an off-switch, you can just define one and If you really don't want an off-switch, you can just define one and
manually inform Click that something is a flag: manually inform Click that something is a flag:
@ -271,6 +275,7 @@ And on the command line:
.. click:run:: .. click:run::
invoke(info, args=['--shout']) invoke(info, args=['--shout'])
invoke(info)
Note that if a slash is contained in your option already (for instance, if Note that if a slash is contained in your option already (for instance, if
you use Windows-style parameters where ``/`` is the prefix character), you you use Windows-style parameters where ``/`` is the prefix character), you
@ -281,7 +286,7 @@ can alternatively split the parameters through ``;`` instead:
@click.command() @click.command()
@click.option('/debug;/no-debug') @click.option('/debug;/no-debug')
def log(debug): def log(debug):
click.echo('debug=%s' % debug) click.echo(f"debug={debug}")
if __name__ == '__main__': if __name__ == '__main__':
log() log()
@ -402,7 +407,7 @@ Example:
@click.command() @click.command()
@click.option('--name', prompt=True) @click.option('--name', prompt=True)
def hello(name): def hello(name):
click.echo('Hello %s!' % name) click.echo(f"Hello {name}!")
And what it looks like: And what it looks like:
@ -419,7 +424,7 @@ a different one:
@click.command() @click.command()
@click.option('--name', prompt='Your name please') @click.option('--name', prompt='Your name please')
def hello(name): def hello(name):
click.echo('Hello %s!' % name) click.echo(f"Hello {name}!")
What it looks like: What it looks like:
@ -430,6 +435,10 @@ What it looks like:
It is advised that prompt not be used in conjunction with the multiple It is advised that prompt not be used in conjunction with the multiple
flag set to True. Instead, prompt in the function interactively. flag set to True. Instead, prompt in the function interactively.
By default, the user will be prompted for an input if one was not passed
through the command line. To turn this behavior off, see
:ref:`optional-value`.
Password Prompts Password Prompts
---------------- ----------------
@ -439,27 +448,30 @@ useful for password input:
.. click:example:: .. click:example::
@click.command() import codecs
@click.option('--password', prompt=True, hide_input=True,
confirmation_prompt=True)
def encrypt(password):
click.echo('Encrypting password to %s' % password.encode('rot13'))
What it looks like: @click.command()
@click.option(
"--password", prompt=True, hide_input=True,
confirmation_prompt=True
)
def encode(password):
click.echo(f"encoded: {codecs.encode(password, 'rot13')}")
.. click:run:: .. click:run::
invoke(encrypt, input=['secret', 'secret']) invoke(encode, input=['secret', 'secret'])
Because this combination of parameters is quite common, this can also be Because this combination of parameters is quite common, this can also be
replaced with the :func:`password_option` decorator: replaced with the :func:`password_option` decorator:
.. click:example:: .. code-block:: python
@click.command() @click.command()
@click.password_option() @click.password_option()
def encrypt(password): def encrypt(password):
click.echo('Encrypting password to %s' % password.encode('rot13')) click.echo(f"encoded: to {codecs.encode(password, 'rot13')}")
Dynamic Defaults for Prompts Dynamic Defaults for Prompts
---------------------------- ----------------------------
@ -474,28 +486,37 @@ prompted if the option isn't specified on the command line, you can do so
by supplying a callable as the default value. For example, to get a default by supplying a callable as the default value. For example, to get a default
from the environment: from the environment:
.. click:example:: .. code-block:: python
import os
@click.command() @click.command()
@click.option('--username', prompt=True, @click.option(
default=lambda: os.environ.get('USER', '')) "--username", prompt=True,
default=lambda: os.environ.get("USER", "")
)
def hello(username): def hello(username):
print("Hello,", username) click.echo(f"Hello, {username}!")
To describe what the default value will be, set it in ``show_default``. To describe what the default value will be, set it in ``show_default``.
.. click:example:: .. click:example::
import os
@click.command() @click.command()
@click.option('--username', prompt=True, @click.option(
default=lambda: os.environ.get('USER', ''), "--username", prompt=True,
show_default='current user') default=lambda: os.environ.get("USER", ""),
show_default="current user"
)
def hello(username): def hello(username):
print("Hello,", username) click.echo(f"Hello, {username}!")
.. click:run:: .. click:run::
invoke(hello, args=['--help']) invoke(hello, args=["--help"])
Callbacks and Eager Options Callbacks and Eager Options
--------------------------- ---------------------------
@ -625,7 +646,7 @@ Example usage:
@click.command() @click.command()
@click.option('--username') @click.option('--username')
def greet(username): def greet(username):
click.echo('Hello %s!' % username) click.echo(f'Hello {username}!')
if __name__ == '__main__': if __name__ == '__main__':
greet(auto_envvar_prefix='GREETER') greet(auto_envvar_prefix='GREETER')
@ -650,12 +671,12 @@ Example:
@click.group() @click.group()
@click.option('--debug/--no-debug') @click.option('--debug/--no-debug')
def cli(debug): def cli(debug):
click.echo('Debug mode is %s' % ('on' if debug else 'off')) click.echo(f"Debug mode is {'on' if debug else 'off'}")
@cli.command() @cli.command()
@click.option('--username') @click.option('--username')
def greet(username): def greet(username):
click.echo('Hello %s!' % username) click.echo(f"Hello {username}!")
if __name__ == '__main__': if __name__ == '__main__':
cli(auto_envvar_prefix='GREETER') cli(auto_envvar_prefix='GREETER')
@ -677,7 +698,7 @@ Example usage:
@click.command() @click.command()
@click.option('--username', envvar='USERNAME') @click.option('--username', envvar='USERNAME')
def greet(username): def greet(username):
click.echo('Hello %s!' % username) click.echo(f"Hello {username}!")
if __name__ == '__main__': if __name__ == '__main__':
greet() greet()
@ -726,7 +747,7 @@ And from the command line:
.. click:run:: .. click:run::
import os import os
invoke(perform, env={'PATHS': './foo/bar%s./test' % os.path.pathsep}) invoke(perform, env={"PATHS": f"./foo/bar{os.path.pathsep}./test"})
Other Prefix Characters Other Prefix Characters
----------------------- -----------------------
@ -742,7 +763,7 @@ POSIX semantics. However in certain situations this can be useful:
@click.command() @click.command()
@click.option('+w/-w') @click.option('+w/-w')
def chmod(w): def chmod(w):
click.echo('writable=%s' % w) click.echo(f"writable={w}")
if __name__ == '__main__': if __name__ == '__main__':
chmod() chmod()
@ -762,7 +783,7 @@ boolean flag you need to separate it with ``;`` instead of ``/``:
@click.command() @click.command()
@click.option('/debug;/no-debug') @click.option('/debug;/no-debug')
def log(debug): def log(debug):
click.echo('debug=%s' % debug) click.echo(f"debug={debug}")
if __name__ == '__main__': if __name__ == '__main__':
log() log()
@ -772,39 +793,34 @@ boolean flag you need to separate it with ``;`` instead of ``/``:
Range Options Range Options
------------- -------------
A special mention should go to the :class:`IntRange` type, which works very The :class:`IntRange` type extends the :data:`INT` type to ensure the
similarly to the :data:`INT` type, but restricts the value to fall into a value is contained in the given range. The :class:`FloatRange` type does
specific range (inclusive on both edges). It has two modes: the same for :data:`FLOAT`.
- the default mode (non-clamping mode) where a value that falls outside If ``min`` or ``max`` is omitted, that side is *unbounded*. Any value in
of the range will cause an error. that direction is accepted. By default, both bounds are *closed*, which
- an optional clamping mode where a value that falls outside of the means the boundary value is included in the accepted range. ``min_open``
range will be clamped. This means that a range of ``0-5`` would and ``max_open`` can be used to exclude that boundary from the range.
return ``5`` for the value ``10`` or ``0`` for the value ``-1`` (for
example).
Example: If ``clamp`` mode is enabled, a value that is outside the range is set
to the boundary instead of failing. For example, the range ``0, 5``
would return ``5`` for the value ``10``, or ``0`` for the value ``-1``.
When using :class:`FloatRange`, ``clamp`` can only be enabled if both
bounds are *closed* (the default).
.. click:example:: .. click:example::
@click.command() @click.command()
@click.option('--count', type=click.IntRange(0, 20, clamp=True)) @click.option("--count", type=click.IntRange(0, 20, clamp=True))
@click.option('--digit', type=click.IntRange(0, 10)) @click.option("--digit", type=click.IntRange(0, 9))
def repeat(count, digit): def repeat(count, digit):
click.echo(str(digit) * count) click.echo(str(digit) * count)
if __name__ == '__main__':
repeat()
And from the command line:
.. click:run:: .. click:run::
invoke(repeat, args=['--count=1000', '--digit=5']) invoke(repeat, args=['--count=100', '--digit=5'])
invoke(repeat, args=['--count=1000', '--digit=12']) invoke(repeat, args=['--count=6', '--digit=12'])
If you pass ``None`` for any of the edges, it means that the range is open
at that side.
Callbacks for Validation Callbacks for Validation
------------------------ ------------------------
@ -813,36 +829,86 @@ Callbacks for Validation
If you want to apply custom validation logic, you can do this in the If you want to apply custom validation logic, you can do this in the
parameter callbacks. These callbacks can both modify values as well as parameter callbacks. These callbacks can both modify values as well as
raise errors if the validation does not work. raise errors if the validation does not work. The callback runs after
type conversion. It is called for all sources, including prompts.
In Click 1.0, you can only raise the :exc:`UsageError` but starting with In Click 1.0, you can only raise the :exc:`UsageError` but starting with
Click 2.0, you can also raise the :exc:`BadParameter` error, which has the Click 2.0, you can also raise the :exc:`BadParameter` error, which has the
added advantage that it will automatically format the error message to added advantage that it will automatically format the error message to
also contain the parameter name. also contain the parameter name.
Example:
.. click:example:: .. click:example::
def validate_rolls(ctx, param, value): def validate_rolls(ctx, param, value):
if isinstance(value, tuple):
return value
try: try:
rolls, dice = map(int, value.split('d', 2)) rolls, _, dice = value.partition("d")
return (dice, rolls) return int(dice), int(rolls)
except ValueError: except ValueError:
raise click.BadParameter('rolls need to be in format NdM') raise click.BadParameter("format must be 'NdM'")
@click.command() @click.command()
@click.option('--rolls', callback=validate_rolls, default='1d6') @click.option(
"--rolls", type=click.UNPROCESSED, callback=validate_rolls,
default="1d6", prompt=True,
)
def roll(rolls): def roll(rolls):
click.echo('Rolling a %d-sided dice %d time(s)' % rolls) sides, times = rolls
click.echo(f"Rolling a {sides}-sided dice {times} time(s)")
if __name__ == '__main__':
roll()
And what it looks like:
.. click:run:: .. click:run::
invoke(roll, args=['--rolls=42']) invoke(roll, args=["--rolls=42"])
println() println()
invoke(roll, args=['--rolls=2d12']) invoke(roll, args=["--rolls=2d12"])
println()
invoke(roll, input=["42", "2d12"])
.. _optional-value:
Optional Value
--------------
Providing the value to an option can be made optional, in which case
providing only the option's flag without a value will either show a
prompt or use its ``flag_value``.
Setting ``is_flag=False, flag_value=value`` tells Click that the option
can still be passed a value, but if only the flag is given the
``flag_value`` is used.
.. click:example::
@click.command()
@click.option("--name", is_flag=False, flag_value="Flag", default="Default")
def hello(name):
click.echo(f"Hello, {name}!")
.. click:run::
invoke(hello, args=[])
invoke(hello, args=["--name", "Value"])
invoke(hello, args=["--name"])
If the option has ``prompt`` enabled, then setting
``prompt_required=False`` tells Click to only show the prompt if the
option's flag is given, instead of if the option is not provided at all.
.. click:example::
@click.command()
@click.option('--name', prompt=True, prompt_required=False, default="Default")
def hello(name):
click.echo(f"Hello {name}!")
.. click:run::
invoke(hello)
invoke(hello, args=["--name", "Value"])
invoke(hello, args=["--name"], input="Prompt")
If ``required=True``, then the option will still prompt if it is not
given, but it will also prompt if only the flag is given.

View file

@ -48,9 +48,9 @@ different behavior and some are supported out of the box:
``bool`` / :data:`click.BOOL`: ``bool`` / :data:`click.BOOL`:
A parameter that accepts boolean values. This is automatically used A parameter that accepts boolean values. This is automatically used
for boolean flags. If used with string values ``1``, ``yes``, ``y``, ``t`` for boolean flags. The string values "1", "true", "t", "yes", "y",
and ``true`` convert to `True` and ``0``, ``no``, ``n``, ``f`` and ``false`` and "on" convert to ``True``. "0", "false", "f", "no", "n", and
convert to `False`. "off" convert to ``False``.
:data:`click.UUID`: :data:`click.UUID`:
A parameter that accepts UUID values. This is not automatically A parameter that accepts UUID values. This is not automatically
@ -118,19 +118,15 @@ integers.
name = "integer" name = "integer"
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
if isinstance(value, int):
return value
try: try:
if value[:2].lower() == "0x": if value[:2].lower() == "0x":
return int(value[2:], 16) return int(value[2:], 16)
elif value[:1] == "0": elif value[:1] == "0":
return int(value, 8) return int(value, 8)
return int(value, 10) return int(value, 10)
except TypeError:
self.fail(
"expected string for int() conversion, got "
f"{value!r} of type {type(value).__name__}",
param,
ctx,
)
except ValueError: except ValueError:
self.fail(f"{value!r} is not a valid integer", param, ctx) self.fail(f"{value!r} is not a valid integer", param, ctx)
@ -140,3 +136,8 @@ The :attr:`~ParamType.name` attribute is optional and is used for
documentation. Call :meth:`~ParamType.fail` if conversion fails. The documentation. Call :meth:`~ParamType.fail` if conversion fails. The
``param`` and ``ctx`` arguments may be ``None`` in some cases such as ``param`` and ``ctx`` arguments may be ``None`` in some cases such as
prompts. prompts.
Values from user input or the command line will be strings, but default
values and Python arguments may already be the correct type. The custom
type should check at the top if the value is already valid and pass it
through to support those cases.

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 keep different project environments isolated. Let's see how virtualenv
works. works.
If you are on Mac OS X or Linux, chances are that one of the following two If you are on Mac OS X or Linux::
commands will work for you::
$ sudo easy_install virtualenv
or even better::
$ pip install virtualenv --user $ pip install virtualenv --user
@ -84,7 +79,7 @@ After doing this, the prompt of your shell should be as familiar as before.
Now, let's move on. Enter the following command to get Click activated in your Now, let's move on. Enter the following command to get Click activated in your
virtualenv:: virtualenv::
$ pip install Click $ pip install click
A few seconds later and you are good to go. A few seconds later and you are good to go.
@ -102,23 +97,23 @@ Examples of Click applications can be found in the documentation as well
as in the GitHub repository together with readme files: as in the GitHub repository together with readme files:
* ``inout``: `File input and output * ``inout``: `File input and output
<https://github.com/pallets/click/tree/master/examples/inout>`_ <https://github.com/pallets/click/tree/main/examples/inout>`_
* ``naval``: `Port of docopt naval example * ``naval``: `Port of docopt naval example
<https://github.com/pallets/click/tree/master/examples/naval>`_ <https://github.com/pallets/click/tree/main/examples/naval>`_
* ``aliases``: `Command alias example * ``aliases``: `Command alias example
<https://github.com/pallets/click/tree/master/examples/aliases>`_ <https://github.com/pallets/click/tree/main/examples/aliases>`_
* ``repo``: `Git-/Mercurial-like command line interface * ``repo``: `Git-/Mercurial-like command line interface
<https://github.com/pallets/click/tree/master/examples/repo>`_ <https://github.com/pallets/click/tree/main/examples/repo>`_
* ``complex``: `Complex example with plugin loading * ``complex``: `Complex example with plugin loading
<https://github.com/pallets/click/tree/master/examples/complex>`_ <https://github.com/pallets/click/tree/main/examples/complex>`_
* ``validation``: `Custom parameter validation example * ``validation``: `Custom parameter validation example
<https://github.com/pallets/click/tree/master/examples/validation>`_ <https://github.com/pallets/click/tree/main/examples/validation>`_
* ``colors``: `Colorama ANSI color support * ``colors``: `Color support demo
<https://github.com/pallets/click/tree/master/examples/colors>`_ <https://github.com/pallets/click/tree/main/examples/colors>`_
* ``termui``: `Terminal UI functions demo * ``termui``: `Terminal UI functions demo
<https://github.com/pallets/click/tree/master/examples/termui>`_ <https://github.com/pallets/click/tree/main/examples/termui>`_
* ``imagepipe``: `Multi command chaining demo * ``imagepipe``: `Multi command chaining demo
<https://github.com/pallets/click/tree/master/examples/imagepipe>`_ <https://github.com/pallets/click/tree/main/examples/imagepipe>`_
Basic Concepts - Creating a Command Basic Concepts - Creating a Command
----------------------------------- -----------------------------------
@ -162,7 +157,7 @@ Echoing
Why does this example use :func:`echo` instead of the regular Why does this example use :func:`echo` instead of the regular
:func:`print` function? The answer to this question is that Click :func:`print` function? The answer to this question is that Click
attempts to support both Python 2 and Python 3 the same way and to be very attempts to support different environments consistently and to be very
robust even when the environment is misconfigured. Click wants to be robust even when the environment is misconfigured. Click wants to be
functional at least on a basic level even if everything is completely functional at least on a basic level even if everything is completely
broken. broken.
@ -171,12 +166,10 @@ What this means is that the :func:`echo` function applies some error
correction in case the terminal is misconfigured instead of dying with an correction in case the terminal is misconfigured instead of dying with an
:exc:`UnicodeError`. :exc:`UnicodeError`.
As an added benefit, starting with Click 2.0, the echo function also The echo function also supports color and other styles in output. It
has good support for ANSI colors. It will automatically strip ANSI codes will automatically remove styles if the output stream is a file. On
if the output stream is a file and if colorama is supported, ANSI colors Windows, colorama is automatically installed and used. See
will also work on Windows. Note that in Python 2, the :func:`echo` function :ref:`ansi-colors`.
does not parse color code information from bytearrays. See :ref:`ansi-colors`
for more information.
If you don't need this, you can also use the `print()` construct / If you don't need this, you can also use the `print()` construct /
function. function.
@ -233,6 +226,30 @@ other invocations::
if __name__ == '__main__': if __name__ == '__main__':
cli() cli()
Registering Commands Later
--------------------------
Instead of using the ``@group.command()`` decorator, commands can be
decorated with the plain ``@click.command()`` decorator and registered
with a group later with ``group.add_command()``. This could be used to
split commands into multiple Python modules.
.. code-block:: python
@click.command()
def greet():
click.echo("Hello, World!")
.. code-block:: python
@click.group()
def group():
pass
group.add_command(greet)
Adding Parameters Adding Parameters
----------------- -----------------
@ -245,7 +262,7 @@ To add parameters, use the :func:`option` and :func:`argument` decorators:
@click.argument('name') @click.argument('name')
def hello(count, name): def hello(count, name):
for x in range(count): for x in range(count):
click.echo('Hello %s!' % name) click.echo(f"Hello {name}!")
What it looks like: What it looks like:

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 To bundle your script with setuptools, all you need is the script in a
Python package and a ``setup.py`` file. Python package and a ``setup.py`` file.
Imagine this directory structure:: Imagine this directory structure:
.. code-block:: text
yourscript.py yourscript.py
setup.py setup.py
@ -59,21 +61,24 @@ Contents of ``yourscript.py``:
"""Example script.""" """Example script."""
click.echo('Hello World!') click.echo('Hello World!')
Contents of ``setup.py``:: Contents of ``setup.py``:
.. code-block:: python
from setuptools import setup from setuptools import setup
setup( setup(
name='yourscript', name='yourscript',
version='0.1', version='0.1.0',
py_modules=['yourscript'], py_modules=['yourscript'],
install_requires=[ install_requires=[
'Click', 'Click',
], ],
entry_points=''' entry_points={
[console_scripts] 'console_scripts': [
yourscript=yourscript:cli 'yourscript = yourscript:cli',
''', ],
},
) )
The magic is in the ``entry_points`` parameter. Below The magic is in the ``entry_points`` parameter. Below
@ -88,7 +93,9 @@ Testing The Script
------------------ ------------------
To test the script, you can make a new virtualenv and then install your To test the script, you can make a new virtualenv and then install your
package:: package:
.. code-block:: console
$ virtualenv venv $ virtualenv venv
$ . venv/bin/activate $ . venv/bin/activate
@ -105,8 +112,11 @@ Scripts in Packages
If your script is growing and you want to switch over to your script being If your script is growing and you want to switch over to your script being
contained in a Python package the changes necessary are minimal. Let's contained in a Python package the changes necessary are minimal. Let's
assume your directory structure changed to this:: assume your directory structure changed to this:
.. code-block:: text
project/
yourpackage/ yourpackage/
__init__.py __init__.py
main.py main.py
@ -114,26 +124,30 @@ assume your directory structure changed to this::
scripts/ scripts/
__init__.py __init__.py
yourscript.py yourscript.py
setup.py
In this case instead of using ``py_modules`` in your ``setup.py`` file you In this case instead of using ``py_modules`` in your ``setup.py`` file you
can use ``packages`` and the automatic package finding support of can use ``packages`` and the automatic package finding support of
setuptools. In addition to that it's also recommended to include other setuptools. In addition to that it's also recommended to include other
package data. package data.
These would be the modified contents of ``setup.py``:: These would be the modified contents of ``setup.py``:
.. code-block:: python
from setuptools import setup, find_packages from setuptools import setup, find_packages
setup( setup(
name='yourpackage', name='yourpackage',
version='0.1', version='0.1.0',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
'Click', 'Click',
], ],
entry_points=''' entry_points={
[console_scripts] 'console_scripts': [
yourscript=yourpackage.scripts.yourscript:cli 'yourscript = yourpackage.scripts.yourscript:cli',
''', ],
},
) )

285
docs/shell-completion.rst Normal file
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.command()
@click.argument('name') @click.argument('name')
def hello(name): def hello(name):
click.echo('Hello %s!' % name) click.echo(f'Hello {name}!')
.. code-block:: python .. code-block:: python
:caption: test_hello.py :caption: test_hello.py
@ -54,7 +54,7 @@ For subcommand testing, a subcommand name must be specified in the `args` parame
@click.group() @click.group()
@click.option('--debug/--no-debug', default=False) @click.option('--debug/--no-debug', default=False)
def cli(debug): def cli(debug):
click.echo('Debug mode is %s' % ('on' if debug else 'off')) click.echo(f"Debug mode is {'on' if debug else 'off'}")
@cli.command() @cli.command()
def sync(): def sync():
@ -112,6 +112,19 @@ current working directory to a new, empty folder.
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == 'Hello World!\n' assert result.output == 'Hello World!\n'
Pass ``temp_dir`` to control where the temporary directory is created.
The directory will not be removed by Click in this case. This is useful
to integrate with a framework like Pytest that manages temporary files.
.. code-block:: python
def test_keep_dir(tmp_path):
runner = CliRunner()
with runner.isolated_filesystem(temp_dir=tmp_path) as td:
...
Input Streams Input Streams
------------- -------------
@ -126,7 +139,7 @@ stream (stdin). This is very useful for testing prompts, for instance:
@click.command() @click.command()
@click.option('--foo', prompt=True) @click.option('--foo', prompt=True)
def prompt(foo): def prompt(foo):
click.echo('foo=%s' % foo) click.echo(f"foo={foo}")
.. code-block:: python .. code-block:: python
:caption: test_prompt.py :caption: test_prompt.py

112
docs/unicode-support.rst Normal file
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 If you do require the know which exact commands will be invoked there are
different ways to cope with this. The first one is to let the subcommands different ways to cope with this. The first one is to let the subcommands
all return functions and then to invoke the functions in a all return functions and then to invoke the functions in a
:meth:`Context.resultcallback`. :meth:`Context.result_callback`.
.. _upgrade-to-2.0: .. _upgrade-to-2.0:

View file

@ -13,9 +13,7 @@ Printing to Stdout
The most obvious helper is the :func:`echo` function, which in many ways The most obvious helper is the :func:`echo` function, which in many ways
works like the Python ``print`` statement or function. The main difference is works like the Python ``print`` statement or function. The main difference is
that it works the same in Python 2 and 3, it intelligently detects that it works the same in many different terminal environments.
misconfigured output streams, and it will never fail (except in Python 3; for
more information see :ref:`python3-limitations`).
Example:: Example::
@ -23,10 +21,8 @@ Example::
click.echo('Hello World!') click.echo('Hello World!')
Most importantly, it can print both Unicode and binary data, unlike the It can output both text and binary data. It will emit a trailing newline
built-in ``print`` function in Python 3, which cannot output any bytes. It by default, which needs to be suppressed by passing ``nl=False``::
will, however, emit a trailing newline by default, which needs to be
suppressed by passing ``nl=False``::
click.echo(b'\xe2\x98\x83', nl=False) click.echo(b'\xe2\x98\x83', nl=False)
@ -34,19 +30,17 @@ Last but not least :func:`echo` uses click's intelligent internal output
streams to stdout and stderr which support unicode output on the Windows streams to stdout and stderr which support unicode output on the Windows
console. This means for as long as you are using `click.echo` you can console. This means for as long as you are using `click.echo` you can
output unicode characters (there are some limitations on the default font output unicode characters (there are some limitations on the default font
with regards to which characters can be displayed). This functionality is with regards to which characters can be displayed).
new in Click 6.0.
.. versionadded:: 6.0 .. versionadded:: 6.0
Click now emulates output streams on Windows to support unicode to the Click emulates output streams on Windows to support unicode to the
Windows console through separate APIs. For more information see Windows console through separate APIs. For more information see
:doc:`wincmd`. :doc:`wincmd`.
.. versionadded:: 3.0 .. versionadded:: 3.0
Starting with Click 3.0 you can also easily print to standard error by You can also easily print to standard error by passing ``err=True``::
passing ``err=True``::
click.echo('Hello World!', err=True) click.echo('Hello World!', err=True)
@ -58,11 +52,8 @@ ANSI Colors
.. versionadded:: 2.0 .. versionadded:: 2.0
Starting with Click 2.0, the :func:`echo` function gained extra The :func:`echo` function supports ANSI colors and styles. On Windows
functionality to deal with ANSI colors and styles. Note that on Windows, this uses `colorama`_.
this functionality is only available if `colorama`_ is installed. If it
is installed, then ANSI codes are intelligently handled. Note that in Python
2, the echo function doesn't parse color code information from bytearrays.
Primarily this means that: Primarily this means that:
@ -73,12 +64,8 @@ Primarily this means that:
that colors will work on Windows the same way they do on other that colors will work on Windows the same way they do on other
operating systems. operating systems.
Note for `colorama` support: Click will automatically detect when `colorama` On Windows, Click uses colorama without calling ``colorama.init()``. You
is available and use it. Do *not* call ``colorama.init()``! can still call that in your code, but it's not required for Click.
To install `colorama`, run this command::
$ pip install colorama
For styling a string, the :func:`style` function can be used:: For styling a string, the :func:`style` function can be used::
@ -112,15 +99,14 @@ Example:
@click.command() @click.command()
def less(): def less():
click.echo_via_pager('\n'.join('Line %d' % idx click.echo_via_pager("\n".join(f"Line {idx}" for idx in range(200)))
for idx in range(200)))
If you want to use the pager for a lot of text, especially if generating everything in advance would take a lot of time, you can pass a generator (or generator function) instead of a string: If you want to use the pager for a lot of text, especially if generating everything in advance would take a lot of time, you can pass a generator (or generator function) instead of a string:
.. click:example:: .. click:example::
def _generate_output(): def _generate_output():
for idx in range(50000): for idx in range(50000):
yield "Line %d\n" % idx yield f"Line {idx}\n"
@click.command() @click.command()
def less(): def less():
@ -253,9 +239,7 @@ Printing Filenames
------------------ ------------------
Because filenames might not be Unicode, formatting them can be a bit Because filenames might not be Unicode, formatting them can be a bit
tricky. Generally, this is easier in Python 2 than on 3, as you can just tricky.
write the bytes to stdout with the ``print`` function, but in Python 3, you
will always need to operate in Unicode.
The way this works with click is through the :func:`format_filename` The way this works with click is through the :func:`format_filename`
function. It does a best-effort conversion of the filename to Unicode and function. It does a best-effort conversion of the filename to Unicode and
@ -264,7 +248,7 @@ context of a full Unicode string.
Example:: Example::
click.echo('Path: %s' % click.format_filename(b'foo.txt')) click.echo(f"Path: {click.format_filename(b'foo.txt')}")
Standard Streams Standard Streams
@ -281,8 +265,7 @@ Because of this, click provides the :func:`get_binary_stream` and
different Python versions and for a wide variety of terminal configurations. different Python versions and for a wide variety of terminal configurations.
The end result is that these functions will always return a functional The end result is that these functions will always return a functional
stream object (except in very odd cases in Python 3; see stream object (except in very odd cases; see :doc:`/unicode-support`).
:ref:`python3-limitations`).
Example:: Example::
@ -349,20 +332,25 @@ Example usage::
rv = {} rv = {}
for section in parser.sections(): for section in parser.sections():
for key, value in parser.items(section): for key, value in parser.items(section):
rv['%s.%s' % (section, key)] = value rv[f"{section}.{key}"] = value
return rv return rv
Showing Progress Bars Showing Progress Bars
--------------------- ---------------------
.. versionadded:: 2.0
Sometimes, you have command line scripts that need to process a lot of data, Sometimes, you have command line scripts that need to process a lot of data,
but you want to quickly show the user some progress about how long that but you want to quickly show the user some progress about how long that
will take. Click supports simple progress bar rendering for that through will take. Click supports simple progress bar rendering for that through
the :func:`progressbar` function. the :func:`progressbar` function.
.. note::
If you find that you have requirements beyond what Click's progress
bar supports, try using `tqdm`_.
.. _tqdm: https://tqdm.github.io/
The basic usage is very simple: the idea is that you have an iterable that The basic usage is very simple: the idea is that you have an iterable that
you want to operate on. For each item in the iterable it might take some you want to operate on. For each item in the iterable it might take some
time to do processing. So say you have a loop like this:: time to do processing. So say you have a loop like this::
@ -396,7 +384,7 @@ loop. So code like this will render correctly::
with click.progressbar([1, 2, 3]) as bar: with click.progressbar([1, 2, 3]) as bar:
for x in bar: for x in bar:
print('sleep({})...'.format(x)) print(f"sleep({x})...")
time.sleep(x) time.sleep(x)
Another useful feature is to associate a label with the progress bar which Another useful feature is to associate a label with the progress bar which

View file

@ -7,16 +7,15 @@ why does Click exist?
This question is easy to answer: because there is not a single command This question is easy to answer: because there is not a single command
line utility for Python out there which ticks the following boxes: line utility for Python out there which ticks the following boxes:
* is lazily composable without restrictions * Is lazily composable without restrictions.
* supports implementation of Unix/POSIX command line conventions * Supports implementation of Unix/POSIX command line conventions.
* supports loading values from environment variables out of the box * Supports loading values from environment variables out of the box.
* support for prompting of custom values * Support for prompting of custom values.
* is fully nestable and composable * Is fully nestable and composable.
* works the same in Python 2 and 3 * Supports file handling out of the box.
* supports file handling out of the box * Comes with useful common helpers (getting terminal dimensions,
* comes with useful common helpers (getting terminal dimensions,
ANSI colors, fetching direct keyboard input, screen clearing, ANSI colors, fetching direct keyboard input, screen clearing,
finding config paths, launching apps and editors, etc.) finding config paths, launching apps and editors, etc.).
There are many alternatives to Click; the obvious ones are ``optparse`` There are many alternatives to Click; the obvious ones are ``optparse``
and ``argparse`` from the standard library. Have a look to see if something and ``argparse`` from the standard library. Have a look to see if something
@ -47,15 +46,15 @@ Why not Argparse?
Click is internally based on ``optparse`` instead of ``argparse``. This Click is internally based on ``optparse`` instead of ``argparse``. This
is an implementation detail that a user does not have to be concerned is an implementation detail that a user does not have to be concerned
with. Click is not based on argparse because it has some behaviors that with. Click is not based on ``argparse`` because it has some behaviors that
make handling arbitrary command line interfaces hard: make handling arbitrary command line interfaces hard:
* argparse has built-in behavior to guess if something is an * ``argparse`` has built-in behavior to guess if something is an
argument or an option. This becomes a problem when dealing with argument or an option. This becomes a problem when dealing with
incomplete command lines; the behaviour becomes unpredictable incomplete command lines; the behaviour becomes unpredictable
without full knowledge of a command line. This goes against Click's without full knowledge of a command line. This goes against Click's
ambitions of dispatching to subparsers. ambitions of dispatching to subparsers.
* argparse does not support disabling interspersed arguments. Without * ``argparse`` does not support disabling interspersed arguments. Without
this feature, it's not possible to safely implement Click's nested this feature, it's not possible to safely implement Click's nested
parsing. parsing.
@ -135,7 +134,7 @@ Why No Auto Correction?
----------------------- -----------------------
The question came up why Click does not auto correct parameters given that The question came up why Click does not auto correct parameters given that
even optparse and argparse support automatic expansion of long arguments. even optparse and ``argparse`` support automatic expansion of long arguments.
The reason for this is that it's a liability for backwards compatibility. The reason for this is that it's a liability for backwards compatibility.
If people start relying on automatically modified parameters and someone If people start relying on automatically modified parameters and someone
adds a new parameter in the future, the script might stop working. These adds a new parameter in the future, the script might stop working. These

View file

@ -3,11 +3,7 @@ Windows Console Notes
.. versionadded:: 6.0 .. versionadded:: 6.0
Until Click 6.0 there are various bugs and limitations with using Click on Click emulates output streams on Windows to support unicode to the
a Windows console. Most notably the decoding of command line arguments
was performed with the wrong encoding on Python 2 and on all versions of
Python output of unicode characters was impossible. Starting with Click
6.0 we now emulate output streams on Windows to support unicode to the
Windows console through separate APIs and we perform different decoding of Windows console through separate APIs and we perform different decoding of
parameters. parameters.
@ -22,18 +18,10 @@ performed to the type expected value as late as possible. This has some
advantages as it allows us to accept the data in the most appropriate form advantages as it allows us to accept the data in the most appropriate form
for the operating system and Python version. for the operating system and Python version.
For instance paths are left as bytes on Python 2 unless you explicitly
tell it otherwise.
This caused some problems on Windows where initially the wrong encoding This caused some problems on Windows where initially the wrong encoding
was used and garbage ended up in your input data. We not only fixed the was used and garbage ended up in your input data. We not only fixed the
encoding part, but we also now extract unicode parameters from `sys.argv`. encoding part, but we also now extract unicode parameters from `sys.argv`.
This means that on Python 2 under Windows, the arguments processed will
*most likely* be of unicode nature and not bytes. This was something that
previously did not really happen unless you explicitly passed in unicode
parameters so your custom types need to be aware of this.
There is also another limitation with this: if `sys.argv` was modified There is also another limitation with this: if `sys.argv` was modified
prior to invoking a click handler, we have to fall back to the regular prior to invoking a click handler, we have to fall back to the regular
byte input in which case not all unicode values are available but only a byte input in which case not all unicode values are available but only a
@ -55,10 +43,6 @@ stream will also use ``utf-16-le`` as internal encoding. However there is
some hackery going on that the underlying raw IO buffer is still bypassing some hackery going on that the underlying raw IO buffer is still bypassing
the unicode APIs and byte output through an indirection is still possible. the unicode APIs and byte output through an indirection is still possible.
This hackery is used on both Python 2 and Python 3 as neither version of
Python has native support for cmd.exe with unicode characters. There are
some limitations you need to be aware of:
* This unicode support is limited to ``click.echo``, ``click.prompt`` as * This unicode support is limited to ``click.echo``, ``click.prompt`` as
well as ``click.get_text_stream``. well as ``click.get_text_stream``.
* Depending on if unicode values or byte strings are passed the control * Depending on if unicode values or byte strings are passed the control

View file

@ -1,14 +1,10 @@
import configparser
import os import os
import click import click
try:
import configparser
except ImportError:
import ConfigParser as configparser
class Config:
class Config(object):
"""The config in this example only holds aliases.""" """The config in this example only holds aliases."""
def __init__(self): def __init__(self):
@ -53,7 +49,7 @@ class AliasedGroup(click.Group):
# will create the config object is missing. # will create the config object is missing.
cfg = ctx.ensure_object(Config) cfg = ctx.ensure_object(Config)
# Step three: lookup an explicit command aliase in the config # Step three: look up an explicit command alias in the config
if cmd_name in cfg.aliases: if cmd_name in cfg.aliases:
actual_cmd = cfg.aliases[cmd_name] actual_cmd = cfg.aliases[cmd_name]
return click.Group.get_command(self, ctx, actual_cmd) return click.Group.get_command(self, ctx, actual_cmd)
@ -69,7 +65,12 @@ class AliasedGroup(click.Group):
return None return None
elif len(matches) == 1: elif len(matches) == 1:
return click.Group.get_command(self, ctx, matches[0]) return click.Group.get_command(self, ctx, matches[0])
ctx.fail("Too many matches: {}".format(", ".join(sorted(matches)))) ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")
def resolve_command(self, ctx, args):
# always return the command's name, not the alias
_, cmd, args = super().resolve_command(ctx, args)
return cmd.name, cmd, args
def read_config(ctx, param, value): def read_config(ctx, param, value):
@ -125,7 +126,7 @@ def commit():
@pass_config @pass_config
def status(config): def status(config):
"""Shows the status.""" """Shows the status."""
click.echo("Status for {}".format(config.path)) click.echo(f"Status for {config.path}")
@cli.command() @cli.command()
@ -139,4 +140,4 @@ def alias(config, alias_, cmd, config_file):
"""Adds an alias to the specified configuration file.""" """Adds an alias to the specified configuration file."""
config.add_alias(alias_, cmd) config.add_alias(alias_, cmd)
config.write_config(config_file) config.write_config(config_file)
click.echo("Added '{}' as alias for '{}'".format(alias_, cmd)) click.echo(f"Added '{alias_}' as alias for '{cmd}'")

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 colors is a simple example that shows how you can
colorize text. colorize text.
For this to work on Windows, colorama is required. Uses colorama on Windows.
Usage: Usage:

View file

@ -23,22 +23,17 @@ all_colors = (
@click.command() @click.command()
def cli(): def cli():
"""This script prints some colors. If colorama is installed this will """This script prints some colors. It will also automatically remove
also work on Windows. It will also automatically remove all ANSI all ANSI styles if data is piped into a file.
styles if data is piped into a file.
Give it a try! Give it a try!
""" """
for color in all_colors: for color in all_colors:
click.echo(click.style("I am colored {}".format(color), fg=color)) click.echo(click.style(f"I am colored {color}", fg=color))
for color in all_colors: for color in all_colors:
click.echo( click.echo(click.style(f"I am colored {color} and bold", fg=color, bold=True))
click.style("I am colored {} and bold".format(color), fg=color, bold=True)
)
for color in all_colors: for color in all_colors:
click.echo( click.echo(click.style(f"I am reverse colored {color}", fg=color, reverse=True))
click.style("I am reverse colored {}".format(color), fg=color, reverse=True)
)
click.echo(click.style("I am blinking", blink=True)) click.echo(click.style("I am blinking", blink=True))
click.echo(click.style("I am underlined", underline=True)) click.echo(click.style("I am underlined", underline=True))

View file

@ -5,11 +5,7 @@ setup(
version="1.0", version="1.0",
py_modules=["colors"], py_modules=["colors"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=["click"],
"click",
# Colorama is only required for Windows.
"colorama",
],
entry_points=""" entry_points="""
[console_scripts] [console_scripts]
colors=colors:cli colors=colors:cli

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 from setuptools import setup
setup( setup(
name="click-example-bashcompletion", name="click-example-completion",
version="1.0", version="1.0",
py_modules=["bashcompletion"], py_modules=["completion"],
include_package_data=True, include_package_data=True,
install_requires=["click"], install_requires=["click"],
entry_points=""" entry_points="""
[console_scripts] [console_scripts]
bashcompletion=bashcompletion:cli completion=completion:cli
""", """,
) )

View file

@ -7,7 +7,7 @@ import click
CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX") CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX")
class Environment(object): class Environment:
def __init__(self): def __init__(self):
self.verbose = False self.verbose = False
self.home = os.getcwd() self.home = os.getcwd()
@ -39,11 +39,7 @@ class ComplexCLI(click.MultiCommand):
def get_command(self, ctx, name): def get_command(self, ctx, name):
try: try:
if sys.version_info[0] == 2: mod = __import__(f"complex.commands.cmd_{name}", None, None, ["cli"])
name = name.encode("ascii", "replace")
mod = __import__(
"complex.commands.cmd_{}".format(name), None, None, ["cli"]
)
except ImportError: except ImportError:
return return
return mod.cli return mod.cli

View file

@ -10,4 +10,4 @@ def cli(ctx, path):
"""Initializes a repository.""" """Initializes a repository."""
if path is None: if path is None:
path = ctx.home path = ctx.home
ctx.log("Initialized the repository in %s", click.format_filename(path)) ctx.log(f"Initialized the repository in {click.format_filename(path)}")

View file

@ -20,7 +20,7 @@ def cli():
""" """
@cli.resultcallback() @cli.result_callback()
def process_commands(processors): def process_commands(processors):
"""This result callback is invoked with an iterable of all the chained """This result callback is invoked with an iterable of all the chained
subcommands. As in this example each subcommand returns a function subcommands. As in this example each subcommand returns a function
@ -60,10 +60,8 @@ def generator(f):
@processor @processor
def new_func(stream, *args, **kwargs): def new_func(stream, *args, **kwargs):
for item in stream: yield from stream
yield item yield from f(*args, **kwargs)
for item in f(*args, **kwargs):
yield item
return update_wrapper(new_func, f) return update_wrapper(new_func, f)
@ -89,7 +87,7 @@ def open_cmd(images):
""" """
for image in images: for image in images:
try: try:
click.echo("Opening '{}'".format(image)) click.echo(f"Opening '{image}'")
if image == "-": if image == "-":
img = Image.open(click.get_binary_stdin()) img = Image.open(click.get_binary_stdin())
img.filename = "-" img.filename = "-"
@ -97,7 +95,7 @@ def open_cmd(images):
img = Image.open(image) img = Image.open(image)
yield img yield img
except Exception as e: except Exception as e:
click.echo("Could not open image '{}': {}".format(image, e), err=True) click.echo(f"Could not open image '{image}': {e}", err=True)
@cli.command("save") @cli.command("save")
@ -114,12 +112,10 @@ def save_cmd(images, filename):
for idx, image in enumerate(images): for idx, image in enumerate(images):
try: try:
fn = filename.format(idx + 1) fn = filename.format(idx + 1)
click.echo("Saving '{}' as '{}'".format(image.filename, fn)) click.echo(f"Saving '{image.filename}' as '{fn}'")
yield image.save(fn) yield image.save(fn)
except Exception as e: except Exception as e:
click.echo( click.echo(f"Could not save image '{image.filename}': {e}", err=True)
"Could not save image '{}': {}".format(image.filename, e), err=True
)
@cli.command("display") @cli.command("display")
@ -127,7 +123,7 @@ def save_cmd(images, filename):
def display_cmd(images): def display_cmd(images):
"""Opens all images in an image viewer.""" """Opens all images in an image viewer."""
for image in images: for image in images:
click.echo("Displaying '{}'".format(image.filename)) click.echo(f"Displaying '{image.filename}'")
image.show() image.show()
yield image yield image
@ -142,7 +138,7 @@ def resize_cmd(images, width, height):
""" """
for image in images: for image in images:
w, h = (width or image.size[0], height or image.size[1]) w, h = (width or image.size[0], height or image.size[1])
click.echo("Resizing '{}' to {}x{}".format(image.filename, w, h)) click.echo(f"Resizing '{image.filename}' to {w}x{h}")
image.thumbnail((w, h)) image.thumbnail((w, h))
yield image yield image
@ -160,7 +156,7 @@ def crop_cmd(images, border):
if border is not None: if border is not None:
for idx, val in enumerate(box): for idx, val in enumerate(box):
box[idx] = max(0, val - border) box[idx] = max(0, val - border)
click.echo("Cropping '{}' by {}px".format(image.filename, border)) click.echo(f"Cropping '{image.filename}' by {border}px")
yield copy_filename(image.crop(box), image) yield copy_filename(image.crop(box), image)
else: else:
yield image yield image
@ -176,7 +172,7 @@ def convert_rotation(ctx, param, value):
return (Image.ROTATE_180, 180) return (Image.ROTATE_180, 180)
if value in ("-90", "270", "l", "left"): if value in ("-90", "270", "l", "left"):
return (Image.ROTATE_270, 270) return (Image.ROTATE_270, 270)
raise click.BadParameter("invalid rotation '{}'".format(value)) raise click.BadParameter(f"invalid rotation '{value}'")
def convert_flip(ctx, param, value): def convert_flip(ctx, param, value):
@ -187,7 +183,7 @@ def convert_flip(ctx, param, value):
return (Image.FLIP_LEFT_RIGHT, "left to right") return (Image.FLIP_LEFT_RIGHT, "left to right")
if value in ("tb", "topbottom", "upsidedown", "ud"): if value in ("tb", "topbottom", "upsidedown", "ud"):
return (Image.FLIP_LEFT_RIGHT, "top to bottom") return (Image.FLIP_LEFT_RIGHT, "top to bottom")
raise click.BadParameter("invalid flip '{}'".format(value)) raise click.BadParameter(f"invalid flip '{value}'")
@cli.command("transpose") @cli.command("transpose")
@ -201,11 +197,11 @@ def transpose_cmd(images, rotate, flip):
for image in images: for image in images:
if rotate is not None: if rotate is not None:
mode, degrees = rotate mode, degrees = rotate
click.echo("Rotate '{}' by {}deg".format(image.filename, degrees)) click.echo(f"Rotate '{image.filename}' by {degrees}deg")
image = copy_filename(image.transpose(mode), image) image = copy_filename(image.transpose(mode), image)
if flip is not None: if flip is not None:
mode, direction = flip mode, direction = flip
click.echo("Flip '{}' {}".format(image.filename, direction)) click.echo(f"Flip '{image.filename}' {direction}")
image = copy_filename(image.transpose(mode), image) image = copy_filename(image.transpose(mode), image)
yield image yield image
@ -217,7 +213,7 @@ def blur_cmd(images, radius):
"""Applies gaussian blur.""" """Applies gaussian blur."""
blur = ImageFilter.GaussianBlur(radius) blur = ImageFilter.GaussianBlur(radius)
for image in images: for image in images:
click.echo("Blurring '{}' by {}px".format(image.filename, radius)) click.echo(f"Blurring '{image.filename}' by {radius}px")
yield copy_filename(image.filter(blur), image) yield copy_filename(image.filter(blur), image)
@ -234,9 +230,8 @@ def smoothen_cmd(images, iterations):
"""Applies a smoothening filter.""" """Applies a smoothening filter."""
for image in images: for image in images:
click.echo( click.echo(
"Smoothening '{}' {} time{}".format( f"Smoothening {image.filename!r} {iterations}"
image.filename, iterations, "s" if iterations != 1 else "" f" time{'s' if iterations != 1 else ''}"
)
) )
for _ in range(iterations): for _ in range(iterations):
image = copy_filename(image.filter(ImageFilter.BLUR), image) image = copy_filename(image.filter(ImageFilter.BLUR), image)
@ -248,7 +243,7 @@ def smoothen_cmd(images, iterations):
def emboss_cmd(images): def emboss_cmd(images):
"""Embosses an image.""" """Embosses an image."""
for image in images: for image in images:
click.echo("Embossing '{}'".format(image.filename)) click.echo(f"Embossing '{image.filename}'")
yield copy_filename(image.filter(ImageFilter.EMBOSS), image) yield copy_filename(image.filter(ImageFilter.EMBOSS), image)
@ -260,7 +255,7 @@ def emboss_cmd(images):
def sharpen_cmd(images, factor): def sharpen_cmd(images, factor):
"""Sharpens an image.""" """Sharpens an image."""
for image in images: for image in images:
click.echo("Sharpen '{}' by {}".format(image.filename, factor)) click.echo(f"Sharpen '{image.filename}' by {factor}")
enhancer = ImageEnhance.Sharpness(image) enhancer = ImageEnhance.Sharpness(image)
yield copy_filename(enhancer.enhance(max(1.0, factor)), image) yield copy_filename(enhancer.enhance(max(1.0, factor)), image)
@ -282,13 +277,12 @@ def paste_cmd(images, left, right):
yield image yield image
return return
click.echo("Paste '{}' on '{}'".format(to_paste.filename, image.filename)) click.echo(f"Paste '{to_paste.filename}' on '{image.filename}'")
mask = None mask = None
if to_paste.mode == "RGBA" or "transparency" in to_paste.info: if to_paste.mode == "RGBA" or "transparency" in to_paste.info:
mask = to_paste mask = to_paste
image.paste(to_paste, (left, right), mask) image.paste(to_paste, (left, right), mask)
image.filename += "+{}".format(to_paste.filename) image.filename += f"+{to_paste.filename}"
yield image yield image
for image in imageiter: yield from imageiter
yield image

View file

@ -21,7 +21,7 @@ def ship():
@click.argument("name") @click.argument("name")
def ship_new(name): def ship_new(name):
"""Creates a new ship.""" """Creates a new ship."""
click.echo("Created ship {}".format(name)) click.echo(f"Created ship {name}")
@ship.command("move") @ship.command("move")
@ -31,7 +31,7 @@ def ship_new(name):
@click.option("--speed", metavar="KN", default=10, help="Speed in knots.") @click.option("--speed", metavar="KN", default=10, help="Speed in knots.")
def ship_move(ship, x, y, speed): def ship_move(ship, x, y, speed):
"""Moves SHIP to the new location X,Y.""" """Moves SHIP to the new location X,Y."""
click.echo("Moving ship {} to {},{} with speed {}".format(ship, x, y, speed)) click.echo(f"Moving ship {ship} to {x},{y} with speed {speed}")
@ship.command("shoot") @ship.command("shoot")
@ -40,7 +40,7 @@ def ship_move(ship, x, y, speed):
@click.argument("y", type=float) @click.argument("y", type=float)
def ship_shoot(ship, x, y): def ship_shoot(ship, x, y):
"""Makes SHIP fire to X,Y.""" """Makes SHIP fire to X,Y."""
click.echo("Ship {} fires to {},{}".format(ship, x, y)) click.echo(f"Ship {ship} fires to {x},{y}")
@cli.group("mine") @cli.group("mine")
@ -61,7 +61,7 @@ def mine():
@click.option("ty", "--drifting", flag_value="drifting", help="Drifting mine.") @click.option("ty", "--drifting", flag_value="drifting", help="Drifting mine.")
def mine_set(x, y, ty): def mine_set(x, y, ty):
"""Sets a mine at a specific coordinate.""" """Sets a mine at a specific coordinate."""
click.echo("Set {} mine at {},{}".format(ty, x, y)) click.echo(f"Set {ty} mine at {x},{y}")
@mine.command("remove") @mine.command("remove")
@ -69,4 +69,4 @@ def mine_set(x, y, ty):
@click.argument("y", type=float) @click.argument("y", type=float)
def mine_remove(x, y): def mine_remove(x, y):
"""Removes a mine at a specific coordinate.""" """Removes a mine at a specific coordinate."""
click.echo("Removed mine at {},{}".format(x, y)) click.echo(f"Removed mine at {x},{y}")

View file

@ -5,7 +5,7 @@ import sys
import click import click
class Repo(object): class Repo:
def __init__(self, home): def __init__(self, home):
self.home = home self.home = home
self.config = {} self.config = {}
@ -14,10 +14,10 @@ class Repo(object):
def set_config(self, key, value): def set_config(self, key, value):
self.config[key] = value self.config[key] = value
if self.verbose: if self.verbose:
click.echo(" config[{}] = {}".format(key, value), file=sys.stderr) click.echo(f" config[{key}] = {value}", file=sys.stderr)
def __repr__(self): def __repr__(self):
return "<Repo {}>".format(self.home) return f"<Repo {self.home}>"
pass_repo = click.make_pass_decorator(Repo) pass_repo = click.make_pass_decorator(Repo)
@ -78,11 +78,11 @@ def clone(repo, src, dest, shallow, rev):
""" """
if dest is None: if dest is None:
dest = posixpath.split(src)[-1] or "." dest = posixpath.split(src)[-1] or "."
click.echo("Cloning repo {} to {}".format(src, os.path.abspath(dest))) click.echo(f"Cloning repo {src} to {os.path.basename(dest)}")
repo.home = dest repo.home = dest
if shallow: if shallow:
click.echo("Making shallow checkout") click.echo("Making shallow checkout")
click.echo("Checking out revision {}".format(rev)) click.echo(f"Checking out revision {rev}")
@cli.command() @cli.command()
@ -93,7 +93,7 @@ def delete(repo):
This will throw away the current repository. This will throw away the current repository.
""" """
click.echo("Destroying repo {}".format(repo.home)) click.echo(f"Destroying repo {repo.home}")
click.echo("Deleted!") click.echo("Deleted!")
@ -136,7 +136,7 @@ def commit(repo, files, message):
marker = "# Files to be committed:" marker = "# Files to be committed:"
hint = ["", "", marker, "#"] hint = ["", "", marker, "#"]
for file in files: for file in files:
hint.append("# U {}".format(file)) hint.append(f"# U {file}")
message = click.edit("\n".join(hint)) message = click.edit("\n".join(hint))
if message is None: if message is None:
click.echo("Aborted!") click.echo("Aborted!")
@ -147,8 +147,8 @@ def commit(repo, files, message):
return return
else: else:
msg = "\n".join(message) msg = "\n".join(message)
click.echo("Files to be committed: {}".format(files)) click.echo(f"Files to be committed: {files}")
click.echo("Commit message:\n{}".format(msg)) click.echo(f"Commit message:\n{msg}")
@cli.command(short_help="Copies files.") @cli.command(short_help="Copies files.")
@ -163,4 +163,4 @@ def copy(repo, src, dst, force):
files from SRC to DST. files from SRC to DST.
""" """
for fn in src: for fn in src:
click.echo("Copy from {} -> {}".format(fn, dst)) click.echo(f"Copy from {fn} -> {dst}")

View file

@ -5,11 +5,7 @@ setup(
version="1.0", version="1.0",
py_modules=["termui"], py_modules=["termui"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=["click"],
"click",
# Colorama is only required for Windows.
"colorama",
],
entry_points=""" entry_points="""
[console_scripts] [console_scripts]
termui=termui:cli termui=termui:cli

View file

@ -1,4 +1,3 @@
# coding: utf-8
import math import math
import random import random
import time import time
@ -16,8 +15,8 @@ def cli():
def colordemo(): def colordemo():
"""Demonstrates ANSI color support.""" """Demonstrates ANSI color support."""
for color in "red", "green", "blue": for color in "red", "green", "blue":
click.echo(click.style("I am colored {}".format(color), fg=color)) click.echo(click.style(f"I am colored {color}", fg=color))
click.echo(click.style("I am background colored {}".format(color), bg=color)) click.echo(click.style(f"I am background colored {color}", bg=color))
@cli.command() @cli.command()
@ -25,7 +24,7 @@ def pager():
"""Demonstrates using the pager.""" """Demonstrates using the pager."""
lines = [] lines = []
for x in range(200): for x in range(200):
lines.append("{}. Hello World!".format(click.style(str(x), fg="green"))) lines.append(f"{click.style(str(x), fg='green')}. Hello World!")
click.echo_via_pager("\n".join(lines)) click.echo_via_pager("\n".join(lines))
@ -56,7 +55,7 @@ def progress(count):
def show_item(item): def show_item(item):
if item is not None: if item is not None:
return "Item #{}".format(item) return f"Item #{item}"
with click.progressbar( with click.progressbar(
filter(items), filter(items),
@ -71,7 +70,7 @@ def progress(count):
length=count, length=count,
label="Counting", label="Counting",
bar_template="%(label)s %(bar)s | %(info)s", bar_template="%(label)s %(bar)s | %(info)s",
fill_char=click.style(u"", fg="cyan"), fill_char=click.style("", fg="cyan"),
empty_char=" ", empty_char=" ",
) as bar: ) as bar:
for item in bar: for item in bar:
@ -94,7 +93,7 @@ def progress(count):
length=count, length=count,
show_percent=False, show_percent=False,
label="Slowing progress bar", label="Slowing progress bar",
fill_char=click.style(u"", fg="green"), fill_char=click.style("", fg="green"),
) as bar: ) as bar:
for item in steps: for item in steps:
time.sleep(item) time.sleep(item)
@ -119,13 +118,13 @@ def locate(url):
def edit(): def edit():
"""Opens an editor with some text in it.""" """Opens an editor with some text in it."""
MARKER = "# Everything below is ignored\n" MARKER = "# Everything below is ignored\n"
message = click.edit("\n\n{}".format(MARKER)) message = click.edit(f"\n\n{MARKER}")
if message is not None: if message is not None:
msg = message.split(MARKER, 1)[0].rstrip("\n") msg = message.split(MARKER, 1)[0].rstrip("\n")
if not msg: if not msg:
click.echo("Empty message!") click.echo("Empty message!")
else: else:
click.echo("Message:\n{}".format(msg)) click.echo(f"Message:\n{msg}")
else: else:
click.echo("You did not enter anything!") click.echo("You did not enter anything!")
@ -146,7 +145,7 @@ def pause():
def menu(): def menu():
"""Shows a simple menu.""" """Shows a simple menu."""
menu = "main" menu = "main"
while 1: while True:
if menu == "main": if menu == "main":
click.echo("Main menu:") click.echo("Main menu:")
click.echo(" d: debug menu") click.echo(" d: debug menu")

View file

@ -1,9 +1,6 @@
import click from urllib import parse as urlparse
try: import click
from urllib import parse as urlparse
except ImportError:
import urlparse
def validate_count(ctx, param, value): def validate_count(ctx, param, value):
@ -20,8 +17,7 @@ class URL(click.ParamType):
value = urlparse.urlparse(value) value = urlparse.urlparse(value)
if value.scheme not in ("http", "https"): if value.scheme not in ("http", "https"):
self.fail( self.fail(
"invalid URL scheme ({}). Only HTTP URLs are" f"invalid URL scheme ({value.scheme}). Only HTTP URLs are allowed",
" allowed".format(value.scheme),
param, param,
ctx, ctx,
) )
@ -47,6 +43,6 @@ def cli(count, foo, url):
'If a value is provided it needs to be the value "wat".', 'If a value is provided it needs to be the value "wat".',
param_hint=["--foo"], param_hint=["--foo"],
) )
click.echo("count: {}".format(count)) click.echo(f"count: {count}")
click.echo("foo: {}".format(foo)) click.echo(f"foo: {foo}")
click.echo("url: {!r}".format(url)) click.echo(f"url: {url!r}")

6
requirements/dev.in Normal file
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

View file

@ -1,8 +1,40 @@
[metadata] [metadata]
license_file = LICENSE.rst name = click
version = attr: click.__version__
url = https://palletsprojects.com/p/click/
project_urls =
Donate = https://palletsprojects.com/donate
Documentation = https://click.palletsprojects.com/
Changes = https://click.palletsprojects.com/changes/
Source Code = https://github.com/pallets/click/
Issue Tracker = https://github.com/pallets/click/issues/
Twitter = https://twitter.com/PalletsTeam
Chat = https://discord.gg/pallets
license = BSD-3-Clause
license_files = LICENSE.rst
author = Armin Ronacher
author_email = armin.ronacher@active-4.com
maintainer = Pallets
maintainer_email = contact@palletsprojects.com
description = Composable command line interface toolkit
long_description = file: README.rst
long_description_content_type = text/x-rst
classifiers =
Development Status :: 5 - Production/Stable
Intended Audience :: Developers
License :: OSI Approved :: BSD License
Operating System :: OS Independent
Programming Language :: Python
[bdist_wheel] [options]
universal = 1 packages = find:
package_dir = = src
include_package_data = true
python_requires = >= 3.6
# Dependencies are in setup.py for GitHub's dependency graph.
[options.packages.find]
where = src
[tool:pytest] [tool:pytest]
testpaths = tests testpaths = tests
@ -10,9 +42,9 @@ filterwarnings =
error error
[coverage:run] [coverage:run]
branch = True branch = true
source = source =
src click
tests tests
[coverage:paths] [coverage:paths]
@ -21,17 +53,48 @@ source =
*/site-packages */site-packages
[flake8] [flake8]
# B = bugbear
# E = pycodestyle errors
# F = flake8 pyflakes
# W = pycodestyle warnings
# B9 = bugbear opinions,
# ISC = implicit str concat
select = B, E, F, W, B9, ISC select = B, E, F, W, B9, ISC
ignore = ignore =
# slice notation whitespace, invalid
E203 E203
# line length, handled by bugbear B950
E501 E501
# bare except, handled by bugbear B001
E722 E722
# bin op line break, invalid
W503 W503
# up to 88 allowed by bugbear B950
max-line-length = 80 max-line-length = 80
per-file-ignores = per-file-ignores =
# __init__ module exports names
src/click/__init__.py: F401 src/click/__init__.py: F401
[egg_info] [mypy]
tag_build = files = src/click
tag_date = 0 python_version = 3.6
disallow_subclassing_any = True
disallow_untyped_calls = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
no_implicit_optional = True
local_partial_types = True
no_implicit_reexport = True
strict_equality = True
warn_redundant_casts = True
warn_unused_configs = True
warn_unused_ignores = True
warn_return_any = True
warn_unreachable = True
[mypy-colorama.*]
ignore_missing_imports = True
[mypy-importlib_metadata.*]
ignore_missing_imports = True

View file

@ -1,40 +1,9 @@
import io
import re
from setuptools import find_packages
from setuptools import setup from setuptools import setup
with io.open("README.rst", "rt", encoding="utf8") as f:
readme = f.read()
with io.open("src/click/__init__.py", "rt", encoding="utf8") as f:
version = re.search(r'__version__ = "(.*?)"', f.read()).group(1)
setup( setup(
name="click", name="click",
version=version, install_requires=[
url="https://palletsprojects.com/p/click/", "colorama; platform_system == 'Windows'",
project_urls={ "importlib-metadata; python_version < '3.8'",
"Documentation": "https://click.palletsprojects.com/",
"Code": "https://github.com/pallets/click",
"Issue tracker": "https://github.com/pallets/click/issues",
},
license="BSD-3-Clause",
maintainer="Pallets",
maintainer_email="contact@palletsprojects.com",
description="Composable command line interface toolkit",
long_description=readme,
packages=find_packages("src"),
package_dir={"": "src"},
include_package_data=True,
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
], ],
) )

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 around a simple API that does not come with too much magic and is
composable. composable.
""" """
from .core import Argument from .core import Argument as Argument
from .core import BaseCommand from .core import BaseCommand as BaseCommand
from .core import Command from .core import Command as Command
from .core import CommandCollection from .core import CommandCollection as CommandCollection
from .core import Context from .core import Context as Context
from .core import Group from .core import Group as Group
from .core import MultiCommand from .core import MultiCommand as MultiCommand
from .core import Option from .core import Option as Option
from .core import Parameter from .core import Parameter as Parameter
from .decorators import argument from .decorators import argument as argument
from .decorators import command from .decorators import command as command
from .decorators import confirmation_option from .decorators import confirmation_option as confirmation_option
from .decorators import group from .decorators import group as group
from .decorators import help_option from .decorators import help_option as help_option
from .decorators import make_pass_decorator from .decorators import make_pass_decorator as make_pass_decorator
from .decorators import option from .decorators import option as option
from .decorators import pass_context from .decorators import pass_context as pass_context
from .decorators import pass_obj from .decorators import pass_obj as pass_obj
from .decorators import password_option from .decorators import password_option as password_option
from .decorators import version_option from .decorators import version_option as version_option
from .exceptions import Abort from .exceptions import Abort as Abort
from .exceptions import BadArgumentUsage from .exceptions import BadArgumentUsage as BadArgumentUsage
from .exceptions import BadOptionUsage from .exceptions import BadOptionUsage as BadOptionUsage
from .exceptions import BadParameter from .exceptions import BadParameter as BadParameter
from .exceptions import ClickException from .exceptions import ClickException as ClickException
from .exceptions import FileError from .exceptions import FileError as FileError
from .exceptions import MissingParameter from .exceptions import MissingParameter as MissingParameter
from .exceptions import NoSuchOption from .exceptions import NoSuchOption as NoSuchOption
from .exceptions import UsageError from .exceptions import UsageError as UsageError
from .formatting import HelpFormatter from .formatting import HelpFormatter as HelpFormatter
from .formatting import wrap_text from .formatting import wrap_text as wrap_text
from .globals import get_current_context from .globals import get_current_context as get_current_context
from .parser import OptionParser from .parser import OptionParser as OptionParser
from .termui import clear from .termui import clear as clear
from .termui import confirm from .termui import confirm as confirm
from .termui import echo_via_pager from .termui import echo_via_pager as echo_via_pager
from .termui import edit from .termui import edit as edit
from .termui import get_terminal_size from .termui import get_terminal_size as get_terminal_size
from .termui import getchar from .termui import getchar as getchar
from .termui import launch from .termui import launch as launch
from .termui import pause from .termui import pause as pause
from .termui import progressbar from .termui import progressbar as progressbar
from .termui import prompt from .termui import prompt as prompt
from .termui import secho from .termui import secho as secho
from .termui import style from .termui import style as style
from .termui import unstyle from .termui import unstyle as unstyle
from .types import BOOL from .types import BOOL as BOOL
from .types import Choice from .types import Choice as Choice
from .types import DateTime from .types import DateTime as DateTime
from .types import File from .types import File as File
from .types import FLOAT from .types import FLOAT as FLOAT
from .types import FloatRange from .types import FloatRange as FloatRange
from .types import INT from .types import INT as INT
from .types import IntRange from .types import IntRange as IntRange
from .types import ParamType from .types import ParamType as ParamType
from .types import Path from .types import Path as Path
from .types import STRING from .types import STRING as STRING
from .types import Tuple from .types import Tuple as Tuple
from .types import UNPROCESSED from .types import UNPROCESSED as UNPROCESSED
from .types import UUID from .types import UUID as UUID
from .utils import echo from .utils import echo as echo
from .utils import format_filename from .utils import format_filename as format_filename
from .utils import get_app_dir from .utils import get_app_dir as get_app_dir
from .utils import get_binary_stream from .utils import get_binary_stream as get_binary_stream
from .utils import get_os_args from .utils import get_os_args as get_os_args
from .utils import get_text_stream from .utils import get_text_stream as get_text_stream
from .utils import open_file from .utils import open_file as open_file
# Controls if click should emit the warning about the use of unicode __version__ = "8.0.2"
# literals.
disable_unicode_literals_warning = False
__version__ = "7.1.2"

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 codecs
import io import io
import os import os
import re import re
import sys import sys
import typing as t
from weakref import WeakKeyDictionary from weakref import WeakKeyDictionary
PY2 = sys.version_info[0] == 2
CYGWIN = sys.platform.startswith("cygwin") CYGWIN = sys.platform.startswith("cygwin")
MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version) MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version)
# Determine local App Engine environment, per Google's own suggestion # Determine local App Engine environment, per Google's own suggestion
@ -14,19 +13,21 @@ APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ.
"SERVER_SOFTWARE", "" "SERVER_SOFTWARE", ""
) )
WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2 WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2
DEFAULT_COLUMNS = 80 auto_wrap_for_ansi: t.Optional[t.Callable[[t.TextIO], t.TextIO]] = None
_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") _ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
def get_filesystem_encoding(): def get_filesystem_encoding() -> str:
return sys.getfilesystemencoding() or sys.getdefaultencoding() return sys.getfilesystemencoding() or sys.getdefaultencoding()
def _make_text_stream( def _make_text_stream(
stream, encoding, errors, force_readable=False, force_writable=False stream: t.BinaryIO,
): encoding: t.Optional[str],
errors: t.Optional[str],
force_readable: bool = False,
force_writable: bool = False,
) -> t.TextIO:
if encoding is None: if encoding is None:
encoding = get_best_encoding(stream) encoding = get_best_encoding(stream)
if errors is None: if errors is None:
@ -41,7 +42,7 @@ def _make_text_stream(
) )
def is_ascii_encoding(encoding): def is_ascii_encoding(encoding: str) -> bool:
"""Checks if a given encoding is ascii.""" """Checks if a given encoding is ascii."""
try: try:
return codecs.lookup(encoding).name == "ascii" return codecs.lookup(encoding).name == "ascii"
@ -49,7 +50,7 @@ def is_ascii_encoding(encoding):
return False return False
def get_best_encoding(stream): def get_best_encoding(stream: t.IO) -> str:
"""Returns the default stream encoding if not found.""" """Returns the default stream encoding if not found."""
rv = getattr(stream, "encoding", None) or sys.getdefaultencoding() rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
if is_ascii_encoding(rv): if is_ascii_encoding(rv):
@ -60,46 +61,30 @@ def get_best_encoding(stream):
class _NonClosingTextIOWrapper(io.TextIOWrapper): class _NonClosingTextIOWrapper(io.TextIOWrapper):
def __init__( def __init__(
self, self,
stream, stream: t.BinaryIO,
encoding, encoding: t.Optional[str],
errors, errors: t.Optional[str],
force_readable=False, force_readable: bool = False,
force_writable=False, force_writable: bool = False,
**extra **extra: t.Any,
): ) -> None:
self._stream = stream = _FixupStream(stream, force_readable, force_writable) self._stream = stream = t.cast(
io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra) t.BinaryIO, _FixupStream(stream, force_readable, force_writable)
)
super().__init__(stream, encoding, errors, **extra)
# The io module is a place where the Python 3 text behavior def __del__(self) -> None:
# was forced upon Python 2, so we need to unbreak
# it to look like Python 2.
if PY2:
def write(self, x):
if isinstance(x, str) or is_bytes(x):
try:
self.flush()
except Exception:
pass
return self.buffer.write(str(x))
return io.TextIOWrapper.write(self, x)
def writelines(self, lines):
for line in lines:
self.write(line)
def __del__(self):
try: try:
self.detach() self.detach()
except Exception: except Exception:
pass pass
def isatty(self): def isatty(self) -> bool:
# https://bitbucket.org/pypy/pypy/issue/1803 # https://bitbucket.org/pypy/pypy/issue/1803
return self._stream.isatty() return self._stream.isatty()
class _FixupStream(object): class _FixupStream:
"""The new io interface needs more from streams than streams """The new io interface needs more from streams than streams
traditionally implement. As such, this fix-up code is necessary in traditionally implement. As such, this fix-up code is necessary in
some circumstances. some circumstances.
@ -109,45 +94,47 @@ class _FixupStream(object):
of jupyter notebook). of jupyter notebook).
""" """
def __init__(self, stream, force_readable=False, force_writable=False): def __init__(
self,
stream: t.BinaryIO,
force_readable: bool = False,
force_writable: bool = False,
):
self._stream = stream self._stream = stream
self._force_readable = force_readable self._force_readable = force_readable
self._force_writable = force_writable self._force_writable = force_writable
def __getattr__(self, name): def __getattr__(self, name: str) -> t.Any:
return getattr(self._stream, name) return getattr(self._stream, name)
def read1(self, size): def read1(self, size: int) -> bytes:
f = getattr(self._stream, "read1", None) f = getattr(self._stream, "read1", None)
if f is not None: if f is not None:
return f(size) return t.cast(bytes, f(size))
# We only dispatch to readline instead of read in Python 2 as we
# do not want cause problems with the different implementation
# of line buffering.
if PY2:
return self._stream.readline(size)
return self._stream.read(size) return self._stream.read(size)
def readable(self): def readable(self) -> bool:
if self._force_readable: if self._force_readable:
return True return True
x = getattr(self._stream, "readable", None) x = getattr(self._stream, "readable", None)
if x is not None: if x is not None:
return x() return t.cast(bool, x())
try: try:
self._stream.read(0) self._stream.read(0)
except Exception: except Exception:
return False return False
return True return True
def writable(self): def writable(self) -> bool:
if self._force_writable: if self._force_writable:
return True return True
x = getattr(self._stream, "writable", None) x = getattr(self._stream, "writable", None)
if x is not None: if x is not None:
return x() return t.cast(bool, x())
try: try:
self._stream.write("") self._stream.write("") # type: ignore
except Exception: except Exception:
try: try:
self._stream.write(b"") self._stream.write(b"")
@ -155,10 +142,10 @@ class _FixupStream(object):
return False return False
return True return True
def seekable(self): def seekable(self) -> bool:
x = getattr(self._stream, "seekable", None) x = getattr(self._stream, "seekable", None)
if x is not None: if x is not None:
return x() return t.cast(bool, x())
try: try:
self._stream.seek(self._stream.tell()) self._stream.seek(self._stream.tell())
except Exception: except Exception:
@ -166,126 +153,7 @@ class _FixupStream(object):
return True return True
if PY2: def _is_binary_reader(stream: t.IO, default: bool = False) -> bool:
text_type = unicode
raw_input = raw_input
string_types = (str, unicode)
int_types = (int, long)
iteritems = lambda x: x.iteritems()
range_type = xrange
def is_bytes(x):
return isinstance(x, (buffer, bytearray))
_identifier_re = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
# For Windows, we need to force stdout/stdin/stderr to binary if it's
# fetched for that. This obviously is not the most correct way to do
# it as it changes global state. Unfortunately, there does not seem to
# be a clear better way to do it as just reopening the file in binary
# mode does not change anything.
#
# An option would be to do what Python 3 does and to open the file as
# binary only, patch it back to the system, and then use a wrapper
# stream that converts newlines. It's not quite clear what's the
# correct option here.
#
# This code also lives in _winconsole for the fallback to the console
# emulation stream.
#
# There are also Windows environments where the `msvcrt` module is not
# available (which is why we use try-catch instead of the WIN variable
# here), such as the Google App Engine development server on Windows. In
# those cases there is just nothing we can do.
def set_binary_mode(f):
return f
try:
import msvcrt
except ImportError:
pass
else:
def set_binary_mode(f):
try:
fileno = f.fileno()
except Exception:
pass
else:
msvcrt.setmode(fileno, os.O_BINARY)
return f
try:
import fcntl
except ImportError:
pass
else:
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
def isidentifier(x):
return _identifier_re.search(x) is not None
def get_binary_stdin():
return set_binary_mode(sys.stdin)
def get_binary_stdout():
_wrap_std_stream("stdout")
return set_binary_mode(sys.stdout)
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: try:
return isinstance(stream.read(0), bytes) return isinstance(stream.read(0), bytes)
except Exception: except Exception:
@ -293,7 +161,8 @@ else:
# This happens in some cases where the stream was already # This happens in some cases where the stream was already
# closed. In this case, we assume the default. # closed. In this case, we assume the default.
def _is_binary_writer(stream, default=False):
def _is_binary_writer(stream: t.IO, default: bool = False) -> bool:
try: try:
stream.write(b"") stream.write(b"")
except Exception: except Exception:
@ -305,37 +174,44 @@ else:
return default return default
return True return True
def _find_binary_reader(stream):
def _find_binary_reader(stream: t.IO) -> t.Optional[t.BinaryIO]:
# We need to figure out if the given stream is already binary. # We need to figure out if the given stream is already binary.
# This can happen because the official docs recommend detaching # This can happen because the official docs recommend detaching
# the streams to get binary streams. Some code might do this, so # the streams to get binary streams. Some code might do this, so
# we need to deal with this case explicitly. # we need to deal with this case explicitly.
if _is_binary_reader(stream, False): if _is_binary_reader(stream, False):
return stream return t.cast(t.BinaryIO, stream)
buf = getattr(stream, "buffer", None) buf = getattr(stream, "buffer", None)
# Same situation here; this time we assume that the buffer is # Same situation here; this time we assume that the buffer is
# actually binary in case it's closed. # actually binary in case it's closed.
if buf is not None and _is_binary_reader(buf, True): if buf is not None and _is_binary_reader(buf, True):
return buf return t.cast(t.BinaryIO, buf)
def _find_binary_writer(stream): 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. # We need to figure out if the given stream is already binary.
# This can happen because the official docs recommend detatching # This can happen because the official docs recommend detaching
# the streams to get binary streams. Some code might do this, so # the streams to get binary streams. Some code might do this, so
# we need to deal with this case explicitly. # we need to deal with this case explicitly.
if _is_binary_writer(stream, False): if _is_binary_writer(stream, False):
return stream return t.cast(t.BinaryIO, stream)
buf = getattr(stream, "buffer", None) buf = getattr(stream, "buffer", None)
# Same situation here; this time we assume that the buffer is # Same situation here; this time we assume that the buffer is
# actually binary in case it's closed. # actually binary in case it's closed.
if buf is not None and _is_binary_writer(buf, True): if buf is not None and _is_binary_writer(buf, True):
return buf return t.cast(t.BinaryIO, buf)
def _stream_is_misconfigured(stream): return None
def _stream_is_misconfigured(stream: t.TextIO) -> bool:
"""A stream is misconfigured if its encoding is ASCII.""" """A stream is misconfigured if its encoding is ASCII."""
# If the stream does not have an encoding set, we assume it's set # If the stream does not have an encoding set, we assume it's set
# to ASCII. This appears to happen in certain unittest # to ASCII. This appears to happen in certain unittest
@ -343,7 +219,8 @@ else:
# but this at least will force Click to recover somehow. # but this at least will force Click to recover somehow.
return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii") return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
def _is_compat_stream_attr(stream, attr, value):
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 """A stream attribute is compatible if it is equal to the
desired value or the desired value is unset and the attribute desired value or the desired value is unset and the attribute
has a value. has a value.
@ -351,7 +228,10 @@ else:
stream_value = getattr(stream, attr, None) stream_value = getattr(stream, attr, None)
return stream_value == value or (value is None and stream_value is not None) return stream_value == value or (value is None and stream_value is not None)
def _is_compatible_text_stream(stream, encoding, errors):
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 """Check if a stream's encoding and errors attributes are
compatible with the desired values. compatible with the desired values.
""" """
@ -359,18 +239,20 @@ else:
stream, "encoding", encoding stream, "encoding", encoding
) and _is_compat_stream_attr(stream, "errors", errors) ) and _is_compat_stream_attr(stream, "errors", errors)
def _force_correct_text_stream(
text_stream, def _force_correct_text_stream(
encoding, text_stream: t.IO,
errors, encoding: t.Optional[str],
is_binary, errors: t.Optional[str],
find_binary, is_binary: t.Callable[[t.IO, bool], bool],
force_readable=False, find_binary: t.Callable[[t.IO], t.Optional[t.BinaryIO]],
force_writable=False, force_readable: bool = False,
): force_writable: bool = False,
) -> t.TextIO:
if is_binary(text_stream, False): if is_binary(text_stream, False):
binary_reader = text_stream binary_reader = t.cast(t.BinaryIO, text_stream)
else: else:
text_stream = t.cast(t.TextIO, text_stream)
# If the stream looks compatible, and won't default to a # If the stream looks compatible, and won't default to a
# misconfigured ascii encoding, return it as-is. # misconfigured ascii encoding, return it as-is.
if _is_compatible_text_stream(text_stream, encoding, errors) and not ( if _is_compatible_text_stream(text_stream, encoding, errors) and not (
@ -379,13 +261,15 @@ else:
return text_stream return text_stream
# Otherwise, get the underlying binary reader. # Otherwise, get the underlying binary reader.
binary_reader = find_binary(text_stream) possible_binary_reader = find_binary(text_stream)
# If that's not possible, silently use the original reader # If that's not possible, silently use the original reader
# and get mojibake instead of exceptions. # and get mojibake instead of exceptions.
if binary_reader is None: if possible_binary_reader is None:
return text_stream return text_stream
binary_reader = possible_binary_reader
# Default errors to replace instead of strict in order to get # Default errors to replace instead of strict in order to get
# something that works. # something that works.
if errors is None: if errors is None:
@ -401,7 +285,13 @@ else:
force_writable=force_writable, force_writable=force_writable,
) )
def _force_correct_text_reader(text_reader, encoding, errors, force_readable=False):
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( return _force_correct_text_stream(
text_reader, text_reader,
encoding, encoding,
@ -411,7 +301,13 @@ else:
force_readable=force_readable, force_readable=force_readable,
) )
def _force_correct_text_writer(text_writer, encoding, errors, force_writable=False):
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( return _force_correct_text_stream(
text_writer, text_writer,
encoding, encoding,
@ -421,96 +317,75 @@ else:
force_writable=force_writable, force_writable=force_writable,
) )
def get_binary_stdin():
def get_binary_stdin() -> t.BinaryIO:
reader = _find_binary_reader(sys.stdin) reader = _find_binary_reader(sys.stdin)
if reader is None: if reader is None:
raise RuntimeError("Was not able to determine binary stream for sys.stdin.") raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
return reader return reader
def get_binary_stdout():
def get_binary_stdout() -> t.BinaryIO:
writer = _find_binary_writer(sys.stdout) writer = _find_binary_writer(sys.stdout)
if writer is None: if writer is None:
raise RuntimeError( raise RuntimeError("Was not able to determine binary stream for sys.stdout.")
"Was not able to determine binary stream for sys.stdout."
)
return writer return writer
def get_binary_stderr():
def get_binary_stderr() -> t.BinaryIO:
writer = _find_binary_writer(sys.stderr) writer = _find_binary_writer(sys.stderr)
if writer is None: if writer is None:
raise RuntimeError( raise RuntimeError("Was not able to determine binary stream for sys.stderr.")
"Was not able to determine binary stream for sys.stderr."
)
return writer return writer
def get_text_stdin(encoding=None, errors=None):
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) rv = _get_windows_console_stream(sys.stdin, encoding, errors)
if rv is not None: if rv is not None:
return rv return rv
return _force_correct_text_reader( return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True)
sys.stdin, encoding, errors, force_readable=True
)
def get_text_stdout(encoding=None, errors=None):
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) rv = _get_windows_console_stream(sys.stdout, encoding, errors)
if rv is not None: if rv is not None:
return rv return rv
return _force_correct_text_writer( return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True)
sys.stdout, encoding, errors, force_writable=True
)
def get_text_stderr(encoding=None, errors=None):
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) rv = _get_windows_console_stream(sys.stderr, encoding, errors)
if rv is not None: if rv is not None:
return rv return rv
return _force_correct_text_writer( return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True)
sys.stderr, encoding, errors, force_writable=True
)
def filename_to_ui(value):
if isinstance(value, bytes):
value = value.decode(get_filesystem_encoding(), "replace")
else:
value = value.encode("utf-8", "surrogateescape").decode("utf-8", "replace")
return value
def get_streerror(e, default=None): def _wrap_io_open(
if hasattr(e, "strerror"): file: t.Union[str, os.PathLike, int],
msg = e.strerror mode: str,
else: encoding: t.Optional[str],
if default is not None: errors: t.Optional[str],
msg = default ) -> t.IO:
else: """Handles not passing ``encoding`` and ``errors`` in binary mode."""
msg = str(e) if "b" in mode:
if isinstance(msg, bytes): return open(file, mode)
msg = msg.decode("utf-8", "replace")
return msg return open(file, mode, encoding=encoding, errors=errors)
def _wrap_io_open(file, mode, encoding, errors): def open_stream(
"""On Python 2, :func:`io.open` returns a text file wrapper that filename: str,
requires passing ``unicode`` to ``write``. Need to open the file in mode: str = "r",
binary mode then wrap it in a subclass that can write ``str`` and encoding: t.Optional[str] = None,
``unicode``. errors: t.Optional[str] = "strict",
atomic: bool = False,
Also handles not passing ``encoding`` and ``errors`` in binary mode. ) -> t.Tuple[t.IO, bool]:
"""
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 open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False):
binary = "b" in mode binary = "b" in mode
# Standard streams first. These are simple because they don't need # Standard streams first. These are simple because they don't need
@ -549,7 +424,7 @@ def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False
import random import random
try: try:
perm = os.stat(filename).st_mode perm: t.Optional[int] = os.stat(filename).st_mode
except OSError: except OSError:
perm = None perm = None
@ -561,7 +436,7 @@ def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False
while True: while True:
tmp_filename = os.path.join( tmp_filename = os.path.join(
os.path.dirname(filename), os.path.dirname(filename),
".__atomic-write{:08x}".format(random.randrange(1 << 32)), f".__atomic-write{random.randrange(1 << 32):08x}",
) )
try: try:
fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm) fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
@ -580,76 +455,55 @@ def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False
os.chmod(tmp_filename, perm) # in case perm includes bits in umask os.chmod(tmp_filename, perm) # in case perm includes bits in umask
f = _wrap_io_open(fd, mode, encoding, errors) f = _wrap_io_open(fd, mode, encoding, errors)
return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True af = _AtomicFile(f, tmp_filename, os.path.realpath(filename))
return t.cast(t.IO, af), True
# Used in a destructor call, needs extra protection from interpreter cleanup. class _AtomicFile:
if hasattr(os, "replace"): def __init__(self, f: t.IO, tmp_filename: str, real_filename: str) -> None:
_replace = os.replace
_can_replace = True
else:
_replace = os.rename
_can_replace = not WIN
class _AtomicFile(object):
def __init__(self, f, tmp_filename, real_filename):
self._f = f self._f = f
self._tmp_filename = tmp_filename self._tmp_filename = tmp_filename
self._real_filename = real_filename self._real_filename = real_filename
self.closed = False self.closed = False
@property @property
def name(self): def name(self) -> str:
return self._real_filename return self._real_filename
def close(self, delete=False): def close(self, delete: bool = False) -> None:
if self.closed: if self.closed:
return return
self._f.close() self._f.close()
if not _can_replace: os.replace(self._tmp_filename, self._real_filename)
try:
os.remove(self._real_filename)
except OSError:
pass
_replace(self._tmp_filename, self._real_filename)
self.closed = True self.closed = True
def __getattr__(self, name): def __getattr__(self, name: str) -> t.Any:
return getattr(self._f, name) return getattr(self._f, name)
def __enter__(self): def __enter__(self) -> "_AtomicFile":
return self return self
def __exit__(self, exc_type, exc_value, tb): def __exit__(self, exc_type, exc_value, tb): # type: ignore
self.close(delete=exc_type is not None) self.close(delete=exc_type is not None)
def __repr__(self): def __repr__(self) -> str:
return repr(self._f) return repr(self._f)
auto_wrap_for_ansi = None def strip_ansi(value: str) -> str:
colorama = None
get_winterm_size = None
def strip_ansi(value):
return _ansi_re.sub("", value) return _ansi_re.sub("", value)
def _is_jupyter_kernel_output(stream): def _is_jupyter_kernel_output(stream: t.IO) -> bool:
if WIN:
# TODO: Couldn't test on Windows, should't try to support until
# someone tests the details wrt colorama.
return
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)): while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
stream = stream._stream stream = stream._stream
return stream.__class__.__module__.startswith("ipykernel.") return stream.__class__.__module__.startswith("ipykernel.")
def should_strip_ansi(stream=None, color=None): def should_strip_ansi(
stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None
) -> bool:
if color is None: if color is None:
if stream is None: if stream is None:
stream = sys.stdin stream = sys.stdin
@ -657,99 +511,85 @@ def should_strip_ansi(stream=None, color=None):
return not color return not color
# If we're on Windows, we provide transparent integration through # On Windows, wrap the output streams with colorama to support ANSI
# colorama. This will make ANSI colors through the echo function # color codes.
# work automatically. # NOTE: double check is needed so mypy does not analyze this on Linux
if WIN: if sys.platform.startswith("win") and WIN:
# Windows has a smaller terminal from ._winconsole import _get_windows_console_stream
DEFAULT_COLUMNS = 79
from ._winconsole import _get_windows_console_stream, _wrap_std_stream def _get_argv_encoding() -> str:
def _get_argv_encoding():
import locale import locale
return locale.getpreferredencoding() return locale.getpreferredencoding()
if PY2: _ansi_stream_wrappers: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
def raw_input(prompt=""): def auto_wrap_for_ansi(
sys.stderr.flush() stream: t.TextIO, color: t.Optional[bool] = None
if prompt: ) -> t.TextIO:
stdout = _default_text_stdout() """Support ANSI color and style codes on Windows by wrapping a
stdout.write(prompt) stream with colorama.
stdin = _default_text_stdin()
return stdin.readline().rstrip("\r\n")
try:
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.
""" """
try: try:
cached = _ansi_stream_wrappers.get(stream) cached = _ansi_stream_wrappers.get(stream)
except Exception: except Exception:
cached = None cached = None
if cached is not None: if cached is not None:
return cached return cached
import colorama
strip = should_strip_ansi(stream, color) strip = should_strip_ansi(stream, color)
ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip) ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
rv = ansi_wrapper.stream rv = t.cast(t.TextIO, ansi_wrapper.stream)
_write = rv.write _write = rv.write
def _safe_write(s): def _safe_write(s):
try: try:
return _write(s) return _write(s)
except: except BaseException:
ansi_wrapper.reset_all() ansi_wrapper.reset_all()
raise raise
rv.write = _safe_write rv.write = _safe_write
try: try:
_ansi_stream_wrappers[stream] = rv _ansi_stream_wrappers[stream] = rv
except Exception: except Exception:
pass pass
return rv
def get_winterm_size(): return rv
win = colorama.win32.GetConsoleScreenBufferInfo(
colorama.win32.STDOUT
).srWindow
return win.Right - win.Left, win.Bottom - win.Top
else: else:
def _get_argv_encoding(): def _get_argv_encoding() -> str:
return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding() return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding()
_get_windows_console_stream = lambda *x: None def _get_windows_console_stream(
_wrap_std_stream = lambda *x: None f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str]
) -> t.Optional[t.TextIO]:
return None
def term_len(x): def term_len(x: str) -> int:
return len(strip_ansi(x)) return len(strip_ansi(x))
def isatty(stream): def isatty(stream: t.IO) -> bool:
try: try:
return stream.isatty() return stream.isatty()
except Exception: except Exception:
return False return False
def _make_cached_stream_func(src_func, wrapper_func): def _make_cached_stream_func(
cache = WeakKeyDictionary() src_func: t.Callable[[], t.TextIO], wrapper_func: t.Callable[[], t.TextIO]
) -> t.Callable[[], t.TextIO]:
cache: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
def func(): def func() -> t.TextIO:
stream = src_func() stream = src_func()
try: try:
rv = cache.get(stream) rv = cache.get(stream)
@ -759,7 +599,6 @@ def _make_cached_stream_func(src_func, wrapper_func):
return rv return rv
rv = wrapper_func() rv = wrapper_func()
try: try:
stream = src_func() # In case wrapper_func() modified the stream
cache[stream] = rv cache[stream] = rv
except Exception: except Exception:
pass pass
@ -773,13 +612,15 @@ _default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_std
_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr) _default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
binary_streams = { binary_streams: t.Mapping[str, t.Callable[[], t.BinaryIO]] = {
"stdin": get_binary_stdin, "stdin": get_binary_stdin,
"stdout": get_binary_stdout, "stdout": get_binary_stdout,
"stderr": get_binary_stderr, "stderr": get_binary_stderr,
} }
text_streams = { text_streams: t.Mapping[
str, t.Callable[[t.Optional[str], t.Optional[str]], t.TextIO]
] = {
"stdin": get_text_stdin, "stdin": get_text_stdin,
"stdout": get_text_stdout, "stdout": get_text_stdout,
"stderr": get_text_stderr, "stderr": get_text_stderr,

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
""" """
This module contains implementations for the termui module. To keep the This module contains implementations for the termui module. To keep the
import time of Click down, some infrequently used functionality is import time of Click down, some infrequently used functionality is
@ -9,20 +8,22 @@ import math
import os import os
import sys import sys
import time import time
import typing as t
from gettext import gettext as _
from ._compat import _default_text_stdout from ._compat import _default_text_stdout
from ._compat import CYGWIN from ._compat import CYGWIN
from ._compat import get_best_encoding from ._compat import get_best_encoding
from ._compat import int_types
from ._compat import isatty from ._compat import isatty
from ._compat import open_stream from ._compat import open_stream
from ._compat import range_type
from ._compat import strip_ansi from ._compat import strip_ansi
from ._compat import term_len from ._compat import term_len
from ._compat import WIN from ._compat import WIN
from .exceptions import ClickException from .exceptions import ClickException
from .utils import echo from .utils import echo
V = t.TypeVar("V")
if os.name == "nt": if os.name == "nt":
BEFORE_BAR = "\r" BEFORE_BAR = "\r"
AFTER_BAR = "\n" AFTER_BAR = "\n"
@ -31,42 +32,25 @@ else:
AFTER_BAR = "\033[?25h\n" AFTER_BAR = "\033[?25h\n"
def _length_hint(obj): class ProgressBar(t.Generic[V]):
"""Returns the length hint of an object."""
try:
return len(obj)
except (AttributeError, TypeError):
try:
get_hint = type(obj).__length_hint__
except AttributeError:
return None
try:
hint = get_hint(obj)
except TypeError:
return None
if hint is NotImplemented or not isinstance(hint, int_types) or hint < 0:
return None
return hint
class ProgressBar(object):
def __init__( def __init__(
self, self,
iterable, iterable: t.Optional[t.Iterable[V]],
length=None, length: t.Optional[int] = None,
fill_char="#", fill_char: str = "#",
empty_char=" ", empty_char: str = " ",
bar_template="%(bar)s", bar_template: str = "%(bar)s",
info_sep=" ", info_sep: str = " ",
show_eta=True, show_eta: bool = True,
show_percent=None, show_percent: t.Optional[bool] = None,
show_pos=False, show_pos: bool = False,
item_show_func=None, item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None,
label=None, label: t.Optional[str] = None,
file=None, file: t.Optional[t.TextIO] = None,
color=None, color: t.Optional[bool] = None,
width=30, update_min_steps: int = 1,
): width: int = 30,
) -> None:
self.fill_char = fill_char self.fill_char = fill_char
self.empty_char = empty_char self.empty_char = empty_char
self.bar_template = bar_template self.bar_template = bar_template
@ -80,45 +64,50 @@ class ProgressBar(object):
file = _default_text_stdout() file = _default_text_stdout()
self.file = file self.file = file
self.color = color self.color = color
self.update_min_steps = update_min_steps
self._completed_intervals = 0
self.width = width self.width = width
self.autowidth = width == 0 self.autowidth = width == 0
if length is None: if length is None:
length = _length_hint(iterable) from operator import length_hint
length = length_hint(iterable, -1)
if length == -1:
length = None
if iterable is None: if iterable is None:
if length is None: if length is None:
raise TypeError("iterable or length is required") raise TypeError("iterable or length is required")
iterable = range_type(length) iterable = t.cast(t.Iterable[V], range(length))
self.iter = iter(iterable) self.iter = iter(iterable)
self.length = length self.length = length
self.length_known = length is not None
self.pos = 0 self.pos = 0
self.avg = [] self.avg: t.List[float] = []
self.start = self.last_eta = time.time() self.start = self.last_eta = time.time()
self.eta_known = False self.eta_known = False
self.finished = False self.finished = False
self.max_width = None self.max_width: t.Optional[int] = None
self.entered = False self.entered = False
self.current_item = None self.current_item: t.Optional[V] = None
self.is_hidden = not isatty(self.file) self.is_hidden = not isatty(self.file)
self._last_line = None self._last_line: t.Optional[str] = None
self.short_limit = 0.5
def __enter__(self): def __enter__(self) -> "ProgressBar":
self.entered = True self.entered = True
self.render_progress() self.render_progress()
return self return self
def __exit__(self, exc_type, exc_value, tb): def __exit__(self, exc_type, exc_value, tb): # type: ignore
self.render_finish() self.render_finish()
def __iter__(self): def __iter__(self) -> t.Iterator[V]:
if not self.entered: if not self.entered:
raise RuntimeError("You need to use progress bars in a with block.") raise RuntimeError("You need to use progress bars in a with block.")
self.render_progress() self.render_progress()
return self.generator() return self.generator()
def __next__(self): def __next__(self) -> V:
# Iteration is defined in terms of a generator function, # Iteration is defined in terms of a generator function,
# returned by iter(self); use that to define next(). This works # returned by iter(self); use that to define next(). This works
# because `self.iter` is an iterable consumed by that generator, # because `self.iter` is an iterable consumed by that generator,
@ -126,37 +115,31 @@ class ProgressBar(object):
# twice works and does "what you want". # twice works and does "what you want".
return next(iter(self)) return next(iter(self))
# Python 2 compat def render_finish(self) -> None:
next = __next__ if self.is_hidden:
def is_fast(self):
return time.time() - self.start <= self.short_limit
def render_finish(self):
if self.is_hidden or self.is_fast():
return return
self.file.write(AFTER_BAR) self.file.write(AFTER_BAR)
self.file.flush() self.file.flush()
@property @property
def pct(self): def pct(self) -> float:
if self.finished: if self.finished:
return 1.0 return 1.0
return min(self.pos / (float(self.length) or 1), 1.0) return min(self.pos / (float(self.length or 1) or 1), 1.0)
@property @property
def time_per_iteration(self): def time_per_iteration(self) -> float:
if not self.avg: if not self.avg:
return 0.0 return 0.0
return sum(self.avg) / float(len(self.avg)) return sum(self.avg) / float(len(self.avg))
@property @property
def eta(self): def eta(self) -> float:
if self.length_known and not self.finished: if self.length is not None and not self.finished:
return self.time_per_iteration * (self.length - self.pos) return self.time_per_iteration * (self.length - self.pos)
return 0.0 return 0.0
def format_eta(self): def format_eta(self) -> str:
if self.eta_known: if self.eta_known:
t = int(self.eta) t = int(self.eta)
seconds = t % 60 seconds = t % 60
@ -166,44 +149,44 @@ class ProgressBar(object):
hours = t % 24 hours = t % 24
t //= 24 t //= 24
if t > 0: if t > 0:
return "{}d {:02}:{:02}:{:02}".format(t, hours, minutes, seconds) return f"{t}d {hours:02}:{minutes:02}:{seconds:02}"
else: else:
return "{:02}:{:02}:{:02}".format(hours, minutes, seconds) return f"{hours:02}:{minutes:02}:{seconds:02}"
return "" return ""
def format_pos(self): def format_pos(self) -> str:
pos = str(self.pos) pos = str(self.pos)
if self.length_known: if self.length is not None:
pos += "/{}".format(self.length) pos += f"/{self.length}"
return pos return pos
def format_pct(self): def format_pct(self) -> str:
return "{: 4}%".format(int(self.pct * 100))[1:] return f"{int(self.pct * 100): 4}%"[1:]
def format_bar(self): def format_bar(self) -> str:
if self.length_known: if self.length is not None:
bar_length = int(self.pct * self.width) bar_length = int(self.pct * self.width)
bar = self.fill_char * bar_length bar = self.fill_char * bar_length
bar += self.empty_char * (self.width - bar_length) bar += self.empty_char * (self.width - bar_length)
elif self.finished: elif self.finished:
bar = self.fill_char * self.width bar = self.fill_char * self.width
else: else:
bar = list(self.empty_char * (self.width or 1)) chars = list(self.empty_char * (self.width or 1))
if self.time_per_iteration != 0: if self.time_per_iteration != 0:
bar[ chars[
int( int(
(math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5) (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
* self.width * self.width
) )
] = self.fill_char ] = self.fill_char
bar = "".join(bar) bar = "".join(chars)
return bar return bar
def format_progress_line(self): def format_progress_line(self) -> str:
show_percent = self.show_percent show_percent = self.show_percent
info_bits = [] info_bits = []
if self.length_known and show_percent is None: if self.length is not None and show_percent is None:
show_percent = not self.show_pos show_percent = not self.show_pos
if self.show_pos: if self.show_pos:
@ -226,10 +209,16 @@ class ProgressBar(object):
} }
).rstrip() ).rstrip()
def render_progress(self): def render_progress(self) -> None:
from .termui import get_terminal_size import shutil
if self.is_hidden: if self.is_hidden:
# Only output the label as it changes if the output is not a
# TTY. Use file=stderr if you expect to be piping stdout.
if self._last_line != self.label:
self._last_line = self.label
echo(self.label, file=self.file, color=self.color)
return return
buf = [] buf = []
@ -238,10 +227,10 @@ class ProgressBar(object):
old_width = self.width old_width = self.width
self.width = 0 self.width = 0
clutter_length = term_len(self.format_progress_line()) clutter_length = term_len(self.format_progress_line())
new_width = max(0, get_terminal_size()[0] - clutter_length) new_width = max(0, shutil.get_terminal_size().columns - clutter_length)
if new_width < old_width: if new_width < old_width:
buf.append(BEFORE_BAR) buf.append(BEFORE_BAR)
buf.append(" " * self.max_width) buf.append(" " * self.max_width) # type: ignore
self.max_width = new_width self.max_width = new_width
self.width = new_width self.width = new_width
@ -260,14 +249,14 @@ class ProgressBar(object):
line = "".join(buf) line = "".join(buf)
# Render the line only if it changed. # Render the line only if it changed.
if line != self._last_line and not self.is_fast(): if line != self._last_line:
self._last_line = line self._last_line = line
echo(line, file=self.file, color=self.color, nl=False) echo(line, file=self.file, color=self.color, nl=False)
self.file.flush() self.file.flush()
def make_step(self, n_steps): def make_step(self, n_steps: int) -> None:
self.pos += n_steps self.pos += n_steps
if self.length_known and self.pos >= self.length: if self.length is not None and self.pos >= self.length:
self.finished = True self.finished = True
if (time.time() - self.last_eta) < 1.0: if (time.time() - self.last_eta) < 1.0:
@ -285,18 +274,40 @@ class ProgressBar(object):
self.avg = self.avg[-6:] + [step] self.avg = self.avg[-6:] + [step]
self.eta_known = self.length_known self.eta_known = self.length is not None
def update(self, n_steps): def update(self, n_steps: int, current_item: t.Optional[V] = None) -> None:
self.make_step(n_steps) """Update the progress bar by advancing a specified number of
steps, and optionally set the ``current_item`` for this new
position.
: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.render_progress()
self._completed_intervals = 0
def finish(self): def finish(self) -> None:
self.eta_known = 0 self.eta_known = False
self.current_item = None self.current_item = None
self.finished = True self.finished = True
def generator(self): def generator(self) -> t.Iterator[V]:
"""Return a generator which yields the items added to the bar """Return a generator which yields the items added to the bar
during construction, and updates the progress bar *after* the during construction, and updates the progress bar *after* the
yielded block returns. yielded block returns.
@ -312,18 +323,25 @@ class ProgressBar(object):
raise RuntimeError("You need to use progress bars in a with block.") raise RuntimeError("You need to use progress bars in a with block.")
if self.is_hidden: if self.is_hidden:
for rv in self.iter: yield from self.iter
yield rv
else: else:
for rv in self.iter: for rv in self.iter:
self.current_item = rv self.current_item = rv
# This allows show_item_func to be updated before the
# item is processed. Only trigger at the beginning of
# the update interval.
if self._completed_intervals == 0:
self.render_progress()
yield rv yield rv
self.update(1) self.update(1)
self.finish() self.finish()
self.render_progress() self.render_progress()
def pager(generator, color=None): def pager(generator: t.Iterable[str], color: t.Optional[bool] = None) -> None:
"""Decide what method to use for paging through text.""" """Decide what method to use for paging through text."""
stdout = _default_text_stdout() stdout = _default_text_stdout()
if not isatty(sys.stdin) or not isatty(stdout): if not isatty(sys.stdin) or not isatty(stdout):
@ -345,14 +363,14 @@ def pager(generator, color=None):
fd, filename = tempfile.mkstemp() fd, filename = tempfile.mkstemp()
os.close(fd) os.close(fd)
try: try:
if hasattr(os, "system") and os.system('more "{}"'.format(filename)) == 0: if hasattr(os, "system") and os.system(f'more "{filename}"') == 0:
return _pipepager(generator, "more", color) return _pipepager(generator, "more", color)
return _nullpager(stdout, generator, color) return _nullpager(stdout, generator, color)
finally: finally:
os.unlink(filename) os.unlink(filename)
def _pipepager(generator, cmd, color): def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> None:
"""Page through text by feeding it to another program. Invoking a """Page through text by feeding it to another program. Invoking a
pager through this might support colors. pager through this might support colors.
""" """
@ -364,7 +382,7 @@ def _pipepager(generator, cmd, color):
# condition that # condition that
cmd_detail = cmd.rsplit("/", 1)[-1].split() cmd_detail = cmd.rsplit("/", 1)[-1].split()
if color is None and cmd_detail[0] == "less": if color is None and cmd_detail[0] == "less":
less_flags = "{}{}".format(os.environ.get("LESS", ""), " ".join(cmd_detail[1:])) less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_detail[1:])}"
if not less_flags: if not less_flags:
env["LESS"] = "-R" env["LESS"] = "-R"
color = True color = True
@ -372,17 +390,18 @@ def _pipepager(generator, cmd, color):
color = True color = True
c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env) c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env)
encoding = get_best_encoding(c.stdin) stdin = t.cast(t.BinaryIO, c.stdin)
encoding = get_best_encoding(stdin)
try: try:
for text in generator: for text in generator:
if not color: if not color:
text = strip_ansi(text) text = strip_ansi(text)
c.stdin.write(text.encode(encoding, "replace")) stdin.write(text.encode(encoding, "replace"))
except (IOError, KeyboardInterrupt): except (OSError, KeyboardInterrupt):
pass pass
else: else:
c.stdin.close() stdin.close()
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting # Less doesn't respect ^C, but catches it for its own UI purposes (aborting
# search or other commands inside less). # search or other commands inside less).
@ -401,11 +420,13 @@ def _pipepager(generator, cmd, color):
break break
def _tempfilepager(generator, cmd, color): def _tempfilepager(
generator: t.Iterable[str], cmd: str, color: t.Optional[bool]
) -> None:
"""Page through text by invoking a program on a temporary file.""" """Page through text by invoking a program on a temporary file."""
import tempfile import tempfile
filename = tempfile.mktemp() fd, filename = tempfile.mkstemp()
# TODO: This never terminates if the passed generator never terminates. # TODO: This never terminates if the passed generator never terminates.
text = "".join(generator) text = "".join(generator)
if not color: if not color:
@ -414,12 +435,15 @@ def _tempfilepager(generator, cmd, color):
with open_stream(filename, "wb")[0] as f: with open_stream(filename, "wb")[0] as f:
f.write(text.encode(encoding)) f.write(text.encode(encoding))
try: try:
os.system('{} "{}"'.format(cmd, filename)) os.system(f'{cmd} "{filename}"')
finally: finally:
os.close(fd)
os.unlink(filename) os.unlink(filename)
def _nullpager(stream, generator, color): def _nullpager(
stream: t.TextIO, generator: t.Iterable[str], color: t.Optional[bool]
) -> None:
"""Simply print unformatted text. This is the ultimate fallback.""" """Simply print unformatted text. This is the ultimate fallback."""
for text in generator: for text in generator:
if not color: if not color:
@ -427,14 +451,20 @@ def _nullpager(stream, generator, color):
stream.write(text) stream.write(text)
class Editor(object): class Editor:
def __init__(self, editor=None, env=None, require_save=True, extension=".txt"): def __init__(
self,
editor: t.Optional[str] = None,
env: t.Optional[t.Mapping[str, str]] = None,
require_save: bool = True,
extension: str = ".txt",
) -> None:
self.editor = editor self.editor = editor
self.env = env self.env = env
self.require_save = require_save self.require_save = require_save
self.extension = extension self.extension = extension
def get_editor(self): def get_editor(self) -> str:
if self.editor is not None: if self.editor is not None:
return self.editor return self.editor
for key in "VISUAL", "EDITOR": for key in "VISUAL", "EDITOR":
@ -444,48 +474,62 @@ class Editor(object):
if WIN: if WIN:
return "notepad" return "notepad"
for editor in "sensible-editor", "vim", "nano": for editor in "sensible-editor", "vim", "nano":
if os.system("which {} >/dev/null 2>&1".format(editor)) == 0: if os.system(f"which {editor} >/dev/null 2>&1") == 0:
return editor return editor
return "vi" return "vi"
def edit_file(self, filename): def edit_file(self, filename: str) -> None:
import subprocess import subprocess
editor = self.get_editor() editor = self.get_editor()
environ: t.Optional[t.Dict[str, str]] = None
if self.env: if self.env:
environ = os.environ.copy() environ = os.environ.copy()
environ.update(self.env) environ.update(self.env)
else:
environ = None
try: try:
c = subprocess.Popen( c = subprocess.Popen(f'{editor} "{filename}"', env=environ, shell=True)
'{} "{}"'.format(editor, filename), env=environ, shell=True,
)
exit_code = c.wait() exit_code = c.wait()
if exit_code != 0: if exit_code != 0:
raise ClickException("{}: Editing failed!".format(editor)) raise ClickException(
_("{editor}: Editing failed").format(editor=editor)
)
except OSError as e: except OSError as e:
raise ClickException("{}: Editing failed: {}".format(editor, e)) raise ClickException(
_("{editor}: Editing failed: {e}").format(editor=editor, e=e)
) from e
def edit(self, text): def edit(self, text: t.Optional[t.AnyStr]) -> t.Optional[t.AnyStr]:
import tempfile import tempfile
text = text or "" if not text:
data = b""
elif isinstance(text, (bytes, bytearray)):
data = text
else:
if text and not text.endswith("\n"): if text and not text.endswith("\n"):
text += "\n" text += "\n"
fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
try:
if WIN: if WIN:
encoding = "utf-8-sig" data = text.replace("\n", "\r\n").encode("utf-8-sig")
text = text.replace("\n", "\r\n")
else: else:
encoding = "utf-8" data = text.encode("utf-8")
text = text.encode(encoding)
f = os.fdopen(fd, "wb") fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
f.write(text) f: t.BinaryIO
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) timestamp = os.path.getmtime(name)
self.edit_file(name) self.edit_file(name)
@ -493,26 +537,26 @@ class Editor(object):
if self.require_save and os.path.getmtime(name) == timestamp: if self.require_save and os.path.getmtime(name) == timestamp:
return None return None
f = open(name, "rb") with open(name, "rb") as f:
try:
rv = f.read() rv = f.read()
finally:
f.close() if isinstance(text, (bytes, bytearray)):
return rv.decode("utf-8-sig").replace("\r\n", "\n") return rv
return rv.decode("utf-8-sig").replace("\r\n", "\n") # type: ignore
finally: finally:
os.unlink(name) os.unlink(name)
def open_url(url, wait=False, locate=False): def open_url(url: str, wait: bool = False, locate: bool = False) -> int:
import subprocess import subprocess
def _unquote_file(url): def _unquote_file(url: str) -> str:
try: from urllib.parse import unquote
import urllib
except ImportError:
import urllib
if url.startswith("file://"): if url.startswith("file://"):
url = urllib.unquote(url[7:]) url = unquote(url[7:])
return url return url
if sys.platform == "darwin": if sys.platform == "darwin":
@ -529,19 +573,21 @@ def open_url(url, wait=False, locate=False):
null.close() null.close()
elif WIN: elif WIN:
if locate: if locate:
url = _unquote_file(url) url = _unquote_file(url.replace('"', ""))
args = 'explorer /select,"{}"'.format(_unquote_file(url.replace('"', ""))) args = f'explorer /select,"{url}"'
else: else:
args = 'start {} "" "{}"'.format( url = url.replace('"', "")
"/WAIT" if wait else "", url.replace('"', "") wait_str = "/WAIT" if wait else ""
) args = f'start {wait_str} "" "{url}"'
return os.system(args) return os.system(args)
elif CYGWIN: elif CYGWIN:
if locate: if locate:
url = _unquote_file(url) url = os.path.dirname(_unquote_file(url).replace('"', ""))
args = 'cygstart "{}"'.format(os.path.dirname(url).replace('"', "")) args = f'cygstart "{url}"'
else: else:
args = 'cygstart {} "{}"'.format("-w" if wait else "", url.replace('"', "")) url = url.replace('"', "")
wait_str = "-w" if wait else ""
args = f'cygstart {wait_str} "{url}"'
return os.system(args) return os.system(args)
try: try:
@ -562,23 +608,27 @@ def open_url(url, wait=False, locate=False):
return 1 return 1
def _translate_ch_to_exc(ch): def _translate_ch_to_exc(ch: str) -> t.Optional[BaseException]:
if ch == u"\x03": if ch == "\x03":
raise KeyboardInterrupt() raise KeyboardInterrupt()
if ch == u"\x04" and not WIN: # Unix-like, Ctrl+D
if ch == "\x04" and not WIN: # Unix-like, Ctrl+D
raise EOFError() raise EOFError()
if ch == u"\x1a" and WIN: # Windows, Ctrl+Z
if ch == "\x1a" and WIN: # Windows, Ctrl+Z
raise EOFError() raise EOFError()
return None
if WIN: if WIN:
import msvcrt import msvcrt
@contextlib.contextmanager @contextlib.contextmanager
def raw_terminal(): def raw_terminal() -> t.Iterator[int]:
yield yield -1
def getchar(echo): def getchar(echo: bool) -> str:
# The function `getch` will return a bytes object corresponding to # The function `getch` will return a bytes object corresponding to
# the pressed character. Since Windows 10 build 1803, it will also # the pressed character. Since Windows 10 build 1803, it will also
# return \x00 when called a second time after pressing a regular key. # return \x00 when called a second time after pressing a regular key.
@ -608,16 +658,20 @@ if WIN:
# #
# Anyway, Click doesn't claim to do this Right(tm), and using `getwch` # Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
# is doing the right thing in more situations than with `getch`. # is doing the right thing in more situations than with `getch`.
func: t.Callable[[], str]
if echo: if echo:
func = msvcrt.getwche func = msvcrt.getwche # type: ignore
else: else:
func = msvcrt.getwch func = msvcrt.getwch # type: ignore
rv = func() rv = func()
if rv in (u"\x00", u"\xe0"):
if rv in ("\x00", "\xe0"):
# \x00 and \xe0 are control characters that indicate special key, # \x00 and \xe0 are control characters that indicate special key,
# see above. # see above.
rv += func() rv += func()
_translate_ch_to_exc(rv) _translate_ch_to_exc(rv)
return rv return rv
@ -627,31 +681,38 @@ else:
import termios import termios
@contextlib.contextmanager @contextlib.contextmanager
def raw_terminal(): def raw_terminal() -> t.Iterator[int]:
f: t.Optional[t.TextIO]
fd: int
if not isatty(sys.stdin): if not isatty(sys.stdin):
f = open("/dev/tty") f = open("/dev/tty")
fd = f.fileno() fd = f.fileno()
else: else:
fd = sys.stdin.fileno() fd = sys.stdin.fileno()
f = None f = None
try: try:
old_settings = termios.tcgetattr(fd) old_settings = termios.tcgetattr(fd)
try: try:
tty.setraw(fd) tty.setraw(fd)
yield fd yield fd
finally: finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
sys.stdout.flush() sys.stdout.flush()
if f is not None: if f is not None:
f.close() f.close()
except termios.error: except termios.error:
pass pass
def getchar(echo): def getchar(echo: bool) -> str:
with raw_terminal() as fd: with raw_terminal() as fd:
ch = os.read(fd, 32) ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace")
ch = ch.decode(get_best_encoding(sys.stdin), "replace")
if echo and isatty(sys.stdout): if echo and isatty(sys.stdout):
sys.stdout.write(ch) sys.stdout.write(ch)
_translate_ch_to_exc(ch) _translate_ch_to_exc(ch)
return ch return ch

View file

@ -1,9 +1,16 @@
import textwrap import textwrap
import typing as t
from contextlib import contextmanager from contextlib import contextmanager
class TextWrapper(textwrap.TextWrapper): class TextWrapper(textwrap.TextWrapper):
def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): def _handle_long_word(
self,
reversed_chunks: t.List[str],
cur_line: t.List[str],
cur_len: int,
width: int,
) -> None:
space_left = max(width - cur_len, 1) space_left = max(width - cur_len, 1)
if self.break_long_words: if self.break_long_words:
@ -16,22 +23,27 @@ class TextWrapper(textwrap.TextWrapper):
cur_line.append(reversed_chunks.pop()) cur_line.append(reversed_chunks.pop())
@contextmanager @contextmanager
def extra_indent(self, indent): def extra_indent(self, indent: str) -> t.Iterator[None]:
old_initial_indent = self.initial_indent old_initial_indent = self.initial_indent
old_subsequent_indent = self.subsequent_indent old_subsequent_indent = self.subsequent_indent
self.initial_indent += indent self.initial_indent += indent
self.subsequent_indent += indent self.subsequent_indent += indent
try: try:
yield yield
finally: finally:
self.initial_indent = old_initial_indent self.initial_indent = old_initial_indent
self.subsequent_indent = old_subsequent_indent self.subsequent_indent = old_subsequent_indent
def indent_only(self, text): def indent_only(self, text: str) -> str:
rv = [] rv = []
for idx, line in enumerate(text.splitlines()): for idx, line in enumerate(text.splitlines()):
indent = self.initial_indent indent = self.initial_indent
if idx > 0: if idx > 0:
indent = self.subsequent_indent indent = self.subsequent_indent
rv.append(indent + line)
rv.append(f"{indent}{line}")
return "\n".join(rv) return "\n".join(rv)

View file

@ -1,131 +1,100 @@
import codecs import codecs
import os import os
import sys from gettext import gettext as _
from ._compat import PY2
def _find_unicode_literals_frame(): def _verify_python_env() -> None:
import __future__ """Ensures that the environment is good for Unicode."""
if not hasattr(sys, "_getframe"): # not all Python implementations have it
return 0
frm = sys._getframe(1)
idx = 1
while frm is not None:
if frm.f_globals.get("__name__", "").startswith("click."):
frm = frm.f_back
idx += 1
elif frm.f_code.co_flags & __future__.unicode_literals.compiler_flag:
return idx
else:
break
return 0
def _check_for_unicode_literals():
if not __debug__:
return
from . import disable_unicode_literals_warning
if not PY2 or disable_unicode_literals_warning:
return
bad_frame = _find_unicode_literals_frame()
if bad_frame <= 0:
return
from warnings import warn
warn(
Warning(
"Click detected the use of the unicode_literals __future__"
" import. This is heavily discouraged because it can"
" introduce subtle bugs in your code. You should instead"
' use explicit u"" literals for your unicode strings. For'
" more information see"
" https://click.palletsprojects.com/python3/"
),
stacklevel=bad_frame,
)
def _verify_python3_env():
"""Ensures that the environment is good for unicode on Python 3."""
if PY2:
return
try: try:
import locale from locale import getpreferredencoding
fs_enc = codecs.lookup(locale.getpreferredencoding()).name fs_enc = codecs.lookup(getpreferredencoding()).name
except Exception: except Exception:
fs_enc = "ascii" fs_enc = "ascii"
if fs_enc != "ascii": if fs_enc != "ascii":
return return
extra = "" extra = [
_(
"Click will abort further execution because Python was"
" configured to use ASCII as encoding for the environment."
" Consult https://click.palletsprojects.com/unicode-support/"
" for mitigation steps."
)
]
if os.name == "posix": if os.name == "posix":
import subprocess import subprocess
try: try:
rv = subprocess.Popen( rv = subprocess.Popen(
["locale", "-a"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ["locale", "-a"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="ascii",
errors="replace",
).communicate()[0] ).communicate()[0]
except OSError: except OSError:
rv = b"" rv = ""
good_locales = set() good_locales = set()
has_c_utf8 = False has_c_utf8 = False
# Make sure we're operating on text here.
if isinstance(rv, bytes):
rv = rv.decode("ascii", "replace")
for line in rv.splitlines(): for line in rv.splitlines():
locale = line.strip() locale = line.strip()
if locale.lower().endswith((".utf-8", ".utf8")): if locale.lower().endswith((".utf-8", ".utf8")):
good_locales.add(locale) good_locales.add(locale)
if locale.lower() in ("c.utf8", "c.utf-8"): if locale.lower() in ("c.utf8", "c.utf-8"):
has_c_utf8 = True has_c_utf8 = True
extra += "\n\n"
if not good_locales: if not good_locales:
extra += ( extra.append(
_(
"Additional information: on this system no suitable" "Additional information: on this system no suitable"
" UTF-8 locales were discovered. This most likely" " UTF-8 locales were discovered. This most likely"
" requires resolving by reconfiguring the locale" " requires resolving by reconfiguring the locale"
" system." " 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"
) )
elif has_c_utf8:
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: else:
extra += ( extra.append(
"This system lists a couple of UTF-8 supporting locales" _(
"This system lists some UTF-8 supporting locales"
" that you can pick from. The following suitable" " that you can pick from. The following suitable"
" locales were discovered: {}".format(", ".join(sorted(good_locales))) " locales were discovered: {locales}"
).format(locales=", ".join(sorted(good_locales)))
) )
bad_locale = None bad_locale = None
for locale in os.environ.get("LC_ALL"), os.environ.get("LANG"):
if locale and locale.lower().endswith((".utf-8", ".utf8")): for env_locale in os.environ.get("LC_ALL"), os.environ.get("LANG"):
bad_locale = locale if env_locale and env_locale.lower().endswith((".utf-8", ".utf8")):
if locale is not None: bad_locale = env_locale
if env_locale is not None:
break break
if bad_locale is not None: if bad_locale is not None:
extra += ( extra.append(
"\n\nClick discovered that you exported a UTF-8 locale" _(
"Click discovered that you exported a UTF-8 locale"
" but the locale system could not pick up from it" " but the locale system could not pick up from it"
" because it does not exist. The exported locale is" " because it does not exist. The exported locale is"
" '{}' but it is not supported".format(bad_locale) " {locale!r} but it is not supported."
).format(locale=bad_locale)
) )
raise RuntimeError( raise RuntimeError("\n\n".join(extra))
"Click will abort further execution because Python 3 was"
" configured to use ASCII as encoding for the environment."
" Consult https://click.palletsprojects.com/python3/ for"
" mitigation steps.{}".format(extra)
)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# This module is based on the excellent work by Adam Bartoš who # This module is based on the excellent work by Adam Bartoš who
# provided a lot of what went into the implementation here in # provided a lot of what went into the implementation here in
# the discussion to issue1602 in the Python bug tracker. # the discussion to issue1602 in the Python bug tracker.
@ -6,13 +5,11 @@
# There are some general differences in regards to how this works # There are some general differences in regards to how this works
# compared to the original patches as we do not need to patch # compared to the original patches as we do not need to patch
# the entire interpreter but just work in our little world of # the entire interpreter but just work in our little world of
# echo and prmopt. # echo and prompt.
import ctypes
import io import io
import os
import sys import sys
import time import time
import zlib import typing as t
from ctypes import byref from ctypes import byref
from ctypes import c_char from ctypes import c_char
from ctypes import c_char_p from ctypes import c_char_p
@ -22,28 +19,18 @@ from ctypes import c_ulong
from ctypes import c_void_p from ctypes import c_void_p
from ctypes import POINTER from ctypes import POINTER
from ctypes import py_object from ctypes import py_object
from ctypes import windll from ctypes import Structure
from ctypes import WinError
from ctypes import WINFUNCTYPE
from ctypes.wintypes import DWORD from ctypes.wintypes import DWORD
from ctypes.wintypes import HANDLE from ctypes.wintypes import HANDLE
from ctypes.wintypes import LPCWSTR from ctypes.wintypes import LPCWSTR
from ctypes.wintypes import LPWSTR from ctypes.wintypes import LPWSTR
import msvcrt
from ._compat import _NonClosingTextIOWrapper from ._compat import _NonClosingTextIOWrapper
from ._compat import PY2
from ._compat import text_type
try:
from ctypes import pythonapi
PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
PyBuffer_Release = pythonapi.PyBuffer_Release
except ImportError:
pythonapi = None
assert sys.platform == "win32"
import msvcrt # noqa: E402
from ctypes import windll # noqa: E402
from ctypes import WINFUNCTYPE # noqa: E402
c_ssize_p = POINTER(c_ssize_t) c_ssize_p = POINTER(c_ssize_t)
@ -57,16 +44,12 @@ GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
("CommandLineToArgvW", windll.shell32) ("CommandLineToArgvW", windll.shell32)
) )
LocalFree = WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)( LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32))
("LocalFree", windll.kernel32)
)
STDIN_HANDLE = GetStdHandle(-10) STDIN_HANDLE = GetStdHandle(-10)
STDOUT_HANDLE = GetStdHandle(-11) STDOUT_HANDLE = GetStdHandle(-11)
STDERR_HANDLE = GetStdHandle(-12) STDERR_HANDLE = GetStdHandle(-12)
PyBUF_SIMPLE = 0 PyBUF_SIMPLE = 0
PyBUF_WRITABLE = 1 PyBUF_WRITABLE = 1
@ -81,8 +64,15 @@ STDERR_FILENO = 2
EOF = b"\x1a" EOF = b"\x1a"
MAX_BYTES_WRITTEN = 32767 MAX_BYTES_WRITTEN = 32767
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(ctypes.Structure): class Py_buffer(Structure):
_fields_ = [ _fields_ = [
("buf", c_void_p), ("buf", c_void_p),
("obj", py_object), ("obj", py_object),
@ -97,20 +87,14 @@ class Py_buffer(ctypes.Structure):
("internal", c_void_p), ("internal", c_void_p),
] ]
if PY2: PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
_fields_.insert(-1, ("smalltable", c_ssize_t * 2)) PyBuffer_Release = pythonapi.PyBuffer_Release
# On PyPy we cannot get buffers so our ability to operate here is
# serverly limited.
if pythonapi is None:
get_buffer = None
else:
def get_buffer(obj, writable=False): def get_buffer(obj, writable=False):
buf = Py_buffer() buf = Py_buffer()
flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
PyObject_GetBuffer(py_object(obj), byref(buf), flags) PyObject_GetBuffer(py_object(obj), byref(buf), flags)
try: try:
buffer_type = c_char * buf.len buffer_type = c_char * buf.len
return buffer_type.from_address(buf.buf) return buffer_type.from_address(buf.buf)
@ -123,7 +107,7 @@ class _WindowsConsoleRawIOBase(io.RawIOBase):
self.handle = handle self.handle = handle
def isatty(self): def isatty(self):
io.RawIOBase.isatty(self) super().isatty()
return True return True
@ -155,7 +139,7 @@ class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
# wait for KeyboardInterrupt # wait for KeyboardInterrupt
time.sleep(0.1) time.sleep(0.1)
if not rv: if not rv:
raise OSError("Windows error: {}".format(GetLastError())) raise OSError(f"Windows error: {GetLastError()}")
if buffer[0] == EOF: if buffer[0] == EOF:
return 0 return 0
@ -172,7 +156,7 @@ class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
return "ERROR_SUCCESS" return "ERROR_SUCCESS"
elif errno == ERROR_NOT_ENOUGH_MEMORY: elif errno == ERROR_NOT_ENOUGH_MEMORY:
return "ERROR_NOT_ENOUGH_MEMORY" return "ERROR_NOT_ENOUGH_MEMORY"
return "Windows error {}".format(errno) return f"Windows error {errno}"
def write(self, b): def write(self, b):
bytes_to_be_written = len(b) bytes_to_be_written = len(b)
@ -194,17 +178,17 @@ class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
return bytes_written return bytes_written
class ConsoleStream(object): class ConsoleStream:
def __init__(self, text_stream, byte_stream): def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None:
self._text_stream = text_stream self._text_stream = text_stream
self.buffer = byte_stream self.buffer = byte_stream
@property @property
def name(self): def name(self) -> str:
return self.buffer.name return self.buffer.name
def write(self, x): def write(self, x: t.AnyStr) -> int:
if isinstance(x, text_type): if isinstance(x, str):
return self._text_stream.write(x) return self._text_stream.write(x)
try: try:
self.flush() self.flush()
@ -212,159 +196,84 @@ class ConsoleStream(object):
pass pass
return self.buffer.write(x) return self.buffer.write(x)
def writelines(self, lines): def writelines(self, lines: t.Iterable[t.AnyStr]) -> None:
for line in lines: for line in lines:
self.write(line) self.write(line)
def __getattr__(self, name): def __getattr__(self, name: str) -> t.Any:
return getattr(self._text_stream, name) return getattr(self._text_stream, name)
def isatty(self): def isatty(self) -> bool:
return self.buffer.isatty() return self.buffer.isatty()
def __repr__(self): def __repr__(self):
return "<ConsoleStream name={!r} encoding={!r}>".format( return f"<ConsoleStream name={self.name!r} encoding={self.encoding!r}>"
self.name, self.encoding
)
class WindowsChunkedWriter(object): def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO:
"""
Wraps a stream (such as stdout), acting as a transparent proxy for all
attribute access apart from method 'write()' which we wrap to write in
limited chunks due to a Windows limitation on binary console streams.
"""
def __init__(self, wrapped):
# double-underscore everything to prevent clashes with names of
# attributes on the wrapped stream object.
self.__wrapped = wrapped
def __getattr__(self, name):
return getattr(self.__wrapped, name)
def write(self, text):
total_to_write = len(text)
written = 0
while written < total_to_write:
to_write = min(total_to_write - written, MAX_BYTES_WRITTEN)
self.__wrapped.write(text[written : written + to_write])
written += to_write
_wrapped_std_streams = set()
def _wrap_std_stream(name):
# Python 2 & Windows 7 and below
if (
PY2
and sys.getwindowsversion()[:2] <= (6, 1)
and name not in _wrapped_std_streams
):
setattr(sys, name, WindowsChunkedWriter(getattr(sys, name)))
_wrapped_std_streams.add(name)
def _get_text_stdin(buffer_stream):
text_stream = _NonClosingTextIOWrapper( text_stream = _NonClosingTextIOWrapper(
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
"utf-16-le", "utf-16-le",
"strict", "strict",
line_buffering=True, line_buffering=True,
) )
return ConsoleStream(text_stream, buffer_stream) return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
def _get_text_stdout(buffer_stream): def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO:
text_stream = _NonClosingTextIOWrapper( text_stream = _NonClosingTextIOWrapper(
io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
"utf-16-le", "utf-16-le",
"strict", "strict",
line_buffering=True, line_buffering=True,
) )
return ConsoleStream(text_stream, buffer_stream) return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
def _get_text_stderr(buffer_stream): def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO:
text_stream = _NonClosingTextIOWrapper( text_stream = _NonClosingTextIOWrapper(
io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
"utf-16-le", "utf-16-le",
"strict", "strict",
line_buffering=True, line_buffering=True,
) )
return ConsoleStream(text_stream, buffer_stream) return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
if PY2: _stream_factories: t.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = {
def _hash_py_argv():
return zlib.crc32("\x00".join(sys.argv[1:]))
_initial_argv_hash = _hash_py_argv()
def _get_windows_argv():
argc = c_int(0)
argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc))
if not argv_unicode:
raise WinError()
try:
argv = [argv_unicode[i] for i in range(0, argc.value)]
finally:
LocalFree(argv_unicode)
del argv_unicode
if not hasattr(sys, "frozen"):
argv = argv[1:]
while len(argv) > 0:
arg = argv[0]
if not arg.startswith("-") or arg == "-":
break
argv = argv[1:]
if arg.startswith(("-c", "-m")):
break
return argv[1:]
_stream_factories = {
0: _get_text_stdin, 0: _get_text_stdin,
1: _get_text_stdout, 1: _get_text_stdout,
2: _get_text_stderr, 2: _get_text_stderr,
} }
def _is_console(f): def _is_console(f: t.TextIO) -> bool:
if not hasattr(f, "fileno"): if not hasattr(f, "fileno"):
return False return False
try: try:
fileno = f.fileno() fileno = f.fileno()
except OSError: except (OSError, io.UnsupportedOperation):
return False return False
handle = msvcrt.get_osfhandle(fileno) handle = msvcrt.get_osfhandle(fileno)
return bool(GetConsoleMode(handle, byref(DWORD()))) return bool(GetConsoleMode(handle, byref(DWORD())))
def _get_windows_console_stream(f, encoding, errors): def _get_windows_console_stream(
f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str]
) -> t.Optional[t.TextIO]:
if ( if (
get_buffer is not None get_buffer is not None
and encoding in ("utf-16-le", None) and encoding in {"utf-16-le", None}
and errors in ("strict", None) and errors in {"strict", None}
and _is_console(f) and _is_console(f)
): ):
func = _stream_factories.get(f.fileno()) func = _stream_factories.get(f.fileno())
if func is not None: if func is not None:
if not PY2: b = getattr(f, "buffer", None)
f = getattr(f, "buffer", None)
if f is None: if b is None:
return None return None
else:
# If we are on Python 2 we need to set the stream that we return func(b)
# deal with to binary mode as otherwise the exercise if a
# bit moot. The same problems apply as for
# get_binary_stdin and friends from _compat.
msvcrt.setmode(f.fileno(), os.O_BINARY)
return func(f)

View file

@ -1,17 +1,20 @@
import enum
import errno import errno
import inspect
import os import os
import sys import sys
import typing
import typing as t
from collections import abc
from contextlib import contextmanager from contextlib import contextmanager
from contextlib import ExitStack
from functools import partial
from functools import update_wrapper from functools import update_wrapper
from gettext import gettext as _
from gettext import ngettext
from itertools import repeat from itertools import repeat
from ._compat import isidentifier from . import types
from ._compat import iteritems from ._unicodefun import _verify_python_env
from ._compat import PY2
from ._compat import string_types
from ._unicodefun import _check_for_unicode_literals
from ._unicodefun import _verify_python3_env
from .exceptions import Abort from .exceptions import Abort
from .exceptions import BadParameter from .exceptions import BadParameter
from .exceptions import ClickException from .exceptions import ClickException
@ -22,58 +25,49 @@ from .formatting import HelpFormatter
from .formatting import join_options from .formatting import join_options
from .globals import pop_context from .globals import pop_context
from .globals import push_context from .globals import push_context
from .parser import _flag_needs_value
from .parser import OptionParser from .parser import OptionParser
from .parser import split_opt from .parser import split_opt
from .termui import confirm from .termui import confirm
from .termui import prompt from .termui import prompt
from .termui import style from .termui import style
from .types import BOOL from .utils import _detect_program_name
from .types import convert_type from .utils import _expand_args
from .types import IntRange
from .utils import echo from .utils import echo
from .utils import get_os_args
from .utils import make_default_short_help from .utils import make_default_short_help
from .utils import make_str from .utils import make_str
from .utils import PacifyFlushWrapper from .utils import PacifyFlushWrapper
_missing = object() if t.TYPE_CHECKING:
import typing_extensions as te
from .shell_completion import CompletionItem
SUBCOMMAND_METAVAR = "COMMAND [ARGS]..." F = t.TypeVar("F", bound=t.Callable[..., t.Any])
SUBCOMMANDS_METAVAR = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." V = t.TypeVar("V")
DEPRECATED_HELP_NOTICE = " (DEPRECATED)"
DEPRECATED_INVOKE_NOTICE = "DeprecationWarning: The command %(name)s is deprecated."
def _maybe_show_deprecated_notice(cmd): def _complete_visible_commands(
if cmd.deprecated: ctx: "Context", incomplete: str
echo(style(DEPRECATED_INVOKE_NOTICE % {"name": cmd.name}, fg="red"), err=True) ) -> t.Iterator[t.Tuple[str, "Command"]]:
"""List all the subcommands of a group that start with the
incomplete value and aren't hidden.
:param ctx: Invocation context for the group.
def fast_exit(code): :param incomplete: Value being completed. May be empty.
"""Exit without garbage collection, this speeds up exit by about 10ms for
things like bash completion.
""" """
sys.stdout.flush() multi = t.cast(MultiCommand, ctx.command)
sys.stderr.flush()
os._exit(code) for name in multi.list_commands(ctx):
if name.startswith(incomplete):
command = multi.get_command(ctx, name)
if command is not None and not command.hidden:
yield name, command
def _bashcomplete(cmd, prog_name, complete_var=None): def _check_multicommand(
"""Internal handler for the bash completion support.""" base_command: "MultiCommand", cmd_name: str, cmd: "Command", register: bool = False
if complete_var is None: ) -> None:
complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper())
complete_instr = os.environ.get(complete_var)
if not complete_instr:
return
from ._bashcomplete import bashcomplete
if bashcomplete(cmd, prog_name, complete_var, complete_instr):
fast_exit(1)
def _check_multicommand(base_command, cmd_name, cmd, register=False):
if not base_command.chain or not isinstance(cmd, MultiCommand): if not base_command.chain or not isinstance(cmd, MultiCommand):
return return
if register: if register:
@ -87,44 +81,22 @@ def _check_multicommand(base_command, cmd_name, cmd, register=False):
" that is in chain mode. This is not supported." " that is in chain mode. This is not supported."
) )
raise RuntimeError( raise RuntimeError(
"{}. Command '{}' is set to chain and '{}' was added as" f"{hint}. Command {base_command.name!r} is set to chain and"
" subcommand but it in itself is a multi command. ('{}' is a {}" f" {cmd_name!r} was added as a subcommand but it in itself is a"
" within a chained {} named '{}').".format( f" multi command. ({cmd_name!r} is a {type(cmd).__name__}"
hint, f" within a chained {type(base_command).__name__} named"
base_command.name, f" {base_command.name!r})."
cmd_name,
cmd_name,
cmd.__class__.__name__,
base_command.__class__.__name__,
base_command.name,
)
) )
def batch(iterable, batch_size): def batch(iterable: t.Iterable[V], batch_size: int) -> t.List[t.Tuple[V, ...]]:
return list(zip(*repeat(iter(iterable), batch_size))) return list(zip(*repeat(iter(iterable), batch_size)))
def invoke_param_callback(callback, ctx, param, value):
code = getattr(callback, "__code__", None)
args = getattr(code, "co_argcount", 3)
if args < 3:
from warnings import warn
warn(
"Parameter callbacks take 3 args, (ctx, param, value). The"
" 2-arg style is deprecated and will be removed in 8.0.".format(callback),
DeprecationWarning,
stacklevel=3,
)
return callback(ctx, value)
return callback(ctx, param, value)
@contextmanager @contextmanager
def augment_usage_errors(ctx, param=None): def augment_usage_errors(
ctx: "Context", param: t.Optional["Parameter"] = None
) -> t.Iterator[None]:
"""Context manager that attaches extra information to exceptions.""" """Context manager that attaches extra information to exceptions."""
try: try:
yield yield
@ -140,23 +112,53 @@ def augment_usage_errors(ctx, param=None):
raise raise
def iter_params_for_processing(invocation_order, declaration_order): def iter_params_for_processing(
invocation_order: t.Sequence["Parameter"],
declaration_order: t.Sequence["Parameter"],
) -> t.List["Parameter"]:
"""Given a sequence of parameters in the order as should be considered """Given a sequence of parameters in the order as should be considered
for processing and an iterable of parameters that exist, this returns for processing and an iterable of parameters that exist, this returns
a list in the correct order as they should be processed. a list in the correct order as they should be processed.
""" """
def sort_key(item): def sort_key(item: "Parameter") -> t.Tuple[bool, float]:
try: try:
idx = invocation_order.index(item) idx: float = invocation_order.index(item)
except ValueError: except ValueError:
idx = float("inf") idx = float("inf")
return (not item.is_eager, idx)
return not item.is_eager, idx
return sorted(declaration_order, key=sort_key) return sorted(declaration_order, key=sort_key)
class Context(object): class ParameterSource(enum.Enum):
"""This is an :class:`~enum.Enum` that indicates the source of a
parameter's value.
Use :meth:`click.Context.get_parameter_source` to get the
source for a parameter by name.
.. versionchanged:: 8.0
Use :class:`~enum.Enum` and drop the ``validate`` method.
.. versionchanged:: 8.0
Added the ``PROMPT`` value.
"""
COMMANDLINE = enum.auto()
"""The value was provided by the command line args."""
ENVIRONMENT = enum.auto()
"""The value was provided with an environment variable."""
DEFAULT = enum.auto()
"""Used the default specified by the parameter."""
DEFAULT_MAP = enum.auto()
"""Used a default provided by :attr:`Context.default_map`."""
PROMPT = enum.auto()
"""Used a prompt to confirm a default or provide a value."""
class Context:
"""The context is a special internal object that holds state relevant """The context is a special internal object that holds state relevant
for the script execution at every single level. It's normally invisible for the script execution at every single level. It's normally invisible
to commands unless they opt-in to getting access to it. to commands unless they opt-in to getting access to it.
@ -168,21 +170,6 @@ class Context(object):
A context can be used as context manager in which case it will call A context can be used as context manager in which case it will call
:meth:`close` on teardown. :meth:`close` on teardown.
.. versionadded:: 2.0
Added the `resilient_parsing`, `help_option_names`,
`token_normalize_func` parameters.
.. versionadded:: 3.0
Added the `allow_extra_args` and `allow_interspersed_args`
parameters.
.. versionadded:: 4.0
Added the `color`, `ignore_unknown_options`, and
`max_content_width` parameters.
.. versionadded:: 7.1
Added the `show_default` parameter.
:param command: the command class for this context. :param command: the command class for this context.
:param parent: the parent context. :param parent: the parent context.
:param info_name: the info name for this invocation. Generally this :param info_name: the info name for this invocation. Generally this
@ -237,60 +224,88 @@ class Context(object):
codes are used in texts that Click prints which is by codes are used in texts that Click prints which is by
default not the case. This for instance would affect default not the case. This for instance would affect
help output. help output.
:param show_default: if True, shows defaults for all options. :param show_default: Show defaults for all options. If not set,
Even if an option is later created with show_default=False, defaults to the value from a parent context. Overrides an
this command-level setting overrides it. option's ``show_default`` argument.
.. versionchanged:: 8.0
The ``show_default`` parameter defaults to the value from the
parent context.
.. versionchanged:: 7.1
Added the ``show_default`` parameter.
.. versionchanged:: 4.0
Added the ``color``, ``ignore_unknown_options``, and
``max_content_width`` parameters.
.. versionchanged:: 3.0
Added the ``allow_extra_args`` and ``allow_interspersed_args``
parameters.
.. versionchanged:: 2.0
Added the ``resilient_parsing``, ``help_option_names``, and
``token_normalize_func`` parameters.
""" """
#: The formatter class to create with :meth:`make_formatter`.
#:
#: .. versionadded:: 8.0
formatter_class: t.Type["HelpFormatter"] = HelpFormatter
def __init__( def __init__(
self, self,
command, command: "Command",
parent=None, parent: t.Optional["Context"] = None,
info_name=None, info_name: t.Optional[str] = None,
obj=None, obj: t.Optional[t.Any] = None,
auto_envvar_prefix=None, auto_envvar_prefix: t.Optional[str] = None,
default_map=None, default_map: t.Optional[t.Dict[str, t.Any]] = None,
terminal_width=None, terminal_width: t.Optional[int] = None,
max_content_width=None, max_content_width: t.Optional[int] = None,
resilient_parsing=False, resilient_parsing: bool = False,
allow_extra_args=None, allow_extra_args: t.Optional[bool] = None,
allow_interspersed_args=None, allow_interspersed_args: t.Optional[bool] = None,
ignore_unknown_options=None, ignore_unknown_options: t.Optional[bool] = None,
help_option_names=None, help_option_names: t.Optional[t.List[str]] = None,
token_normalize_func=None, token_normalize_func: t.Optional[t.Callable[[str], str]] = None,
color=None, color: t.Optional[bool] = None,
show_default=None, show_default: t.Optional[bool] = None,
): ) -> None:
#: the parent context or `None` if none exists. #: the parent context or `None` if none exists.
self.parent = parent self.parent = parent
#: the :class:`Command` for this context. #: the :class:`Command` for this context.
self.command = command self.command = command
#: the descriptive information name #: the descriptive information name
self.info_name = info_name self.info_name = info_name
#: the parsed parameters except if the value is hidden in which #: Map of parameter names to their parsed values. Parameters
#: case it's not remembered. #: with ``expose_value=False`` are not stored.
self.params = {} self.params: t.Dict[str, t.Any] = {}
#: the leftover arguments. #: the leftover arguments.
self.args = [] self.args: t.List[str] = []
#: protected arguments. These are arguments that are prepended #: protected arguments. These are arguments that are prepended
#: to `args` when certain parsing scenarios are encountered but #: to `args` when certain parsing scenarios are encountered but
#: must be never propagated to another arguments. This is used #: must be never propagated to another arguments. This is used
#: to implement nested parsing. #: to implement nested parsing.
self.protected_args = [] self.protected_args: t.List[str] = []
if obj is None and parent is not None: if obj is None and parent is not None:
obj = parent.obj obj = parent.obj
#: the user object stored. #: the user object stored.
self.obj = obj self.obj: t.Any = obj
self._meta = getattr(parent, "meta", {}) self._meta: t.Dict[str, t.Any] = getattr(parent, "meta", {})
#: A dictionary (-like object) with defaults for parameters. #: A dictionary (-like object) with defaults for parameters.
if ( if (
default_map is None default_map is None
and info_name is not None
and parent is not None and parent is not None
and parent.default_map is not None and parent.default_map is not None
): ):
default_map = parent.default_map.get(info_name) default_map = parent.default_map.get(info_name)
self.default_map = default_map
self.default_map: t.Optional[t.Dict[str, t.Any]] = default_map
#: This flag indicates if a subcommand is going to be executed. A #: This flag indicates if a subcommand is going to be executed. A
#: group callback can use this information to figure out if it's #: group callback can use this information to figure out if it's
@ -301,22 +316,25 @@ class Context(object):
#: If chaining is enabled this will be set to ``'*'`` in case #: If chaining is enabled this will be set to ``'*'`` in case
#: any commands are executed. It is however not possible to #: any commands are executed. It is however not possible to
#: figure out which ones. If you require this knowledge you #: figure out which ones. If you require this knowledge you
#: should use a :func:`resultcallback`. #: should use a :func:`result_callback`.
self.invoked_subcommand = None self.invoked_subcommand: t.Optional[str] = None
if terminal_width is None and parent is not None: if terminal_width is None and parent is not None:
terminal_width = parent.terminal_width terminal_width = parent.terminal_width
#: The width of the terminal (None is autodetection). #: The width of the terminal (None is autodetection).
self.terminal_width = terminal_width self.terminal_width: t.Optional[int] = terminal_width
if max_content_width is None and parent is not None: if max_content_width is None and parent is not None:
max_content_width = parent.max_content_width max_content_width = parent.max_content_width
#: The maximum width of formatted content (None implies a sensible #: The maximum width of formatted content (None implies a sensible
#: default which is 80 for most things). #: default which is 80 for most things).
self.max_content_width = max_content_width self.max_content_width: t.Optional[int] = max_content_width
if allow_extra_args is None: if allow_extra_args is None:
allow_extra_args = command.allow_extra_args allow_extra_args = command.allow_extra_args
#: Indicates if the context allows extra args or if it should #: Indicates if the context allows extra args or if it should
#: fail on parsing. #: fail on parsing.
#: #:
@ -325,14 +343,16 @@ class Context(object):
if allow_interspersed_args is None: if allow_interspersed_args is None:
allow_interspersed_args = command.allow_interspersed_args allow_interspersed_args = command.allow_interspersed_args
#: Indicates if the context allows mixing of arguments and #: Indicates if the context allows mixing of arguments and
#: options or not. #: options or not.
#: #:
#: .. versionadded:: 3.0 #: .. versionadded:: 3.0
self.allow_interspersed_args = allow_interspersed_args self.allow_interspersed_args: bool = allow_interspersed_args
if ignore_unknown_options is None: if ignore_unknown_options is None:
ignore_unknown_options = command.ignore_unknown_options ignore_unknown_options = command.ignore_unknown_options
#: Instructs click to ignore options that a command does not #: Instructs click to ignore options that a command does not
#: understand and will store it on the context for later #: understand and will store it on the context for later
#: processing. This is primarily useful for situations where you #: processing. This is primarily useful for situations where you
@ -341,7 +361,7 @@ class Context(object):
#: forward all arguments. #: forward all arguments.
#: #:
#: .. versionadded:: 4.0 #: .. versionadded:: 4.0
self.ignore_unknown_options = ignore_unknown_options self.ignore_unknown_options: bool = ignore_unknown_options
if help_option_names is None: if help_option_names is None:
if parent is not None: if parent is not None:
@ -350,19 +370,21 @@ class Context(object):
help_option_names = ["--help"] help_option_names = ["--help"]
#: The names for the help options. #: The names for the help options.
self.help_option_names = help_option_names self.help_option_names: t.List[str] = help_option_names
if token_normalize_func is None and parent is not None: if token_normalize_func is None and parent is not None:
token_normalize_func = parent.token_normalize_func token_normalize_func = parent.token_normalize_func
#: An optional normalization function for tokens. This is #: An optional normalization function for tokens. This is
#: options, choices, commands etc. #: options, choices, commands etc.
self.token_normalize_func = token_normalize_func self.token_normalize_func: t.Optional[
t.Callable[[str], str]
] = token_normalize_func
#: Indicates if resilient parsing is enabled. In that case Click #: Indicates if resilient parsing is enabled. In that case Click
#: will do its best to not cause any failures and default values #: will do its best to not cause any failures and default values
#: will be ignored. Useful for completion. #: will be ignored. Useful for completion.
self.resilient_parsing = resilient_parsing self.resilient_parsing: bool = resilient_parsing
# If there is no envvar prefix yet, but the parent has one and # If there is no envvar prefix yet, but the parent has one and
# the command on this level has a name, we can expand the envvar # the command on this level has a name, we can expand the envvar
@ -373,39 +395,68 @@ class Context(object):
and parent.auto_envvar_prefix is not None and parent.auto_envvar_prefix is not None
and self.info_name is not None and self.info_name is not None
): ):
auto_envvar_prefix = "{}_{}".format( auto_envvar_prefix = (
parent.auto_envvar_prefix, self.info_name.upper() f"{parent.auto_envvar_prefix}_{self.info_name.upper()}"
) )
else: else:
auto_envvar_prefix = auto_envvar_prefix.upper() auto_envvar_prefix = auto_envvar_prefix.upper()
if auto_envvar_prefix is not None: if auto_envvar_prefix is not None:
auto_envvar_prefix = auto_envvar_prefix.replace("-", "_") auto_envvar_prefix = auto_envvar_prefix.replace("-", "_")
self.auto_envvar_prefix = auto_envvar_prefix
self.auto_envvar_prefix: t.Optional[str] = auto_envvar_prefix
if color is None and parent is not None: if color is None and parent is not None:
color = parent.color color = parent.color
#: Controls if styling output is wanted or not. #: Controls if styling output is wanted or not.
self.color = color self.color: t.Optional[bool] = color
self.show_default = show_default if show_default is None and parent is not None:
show_default = parent.show_default
self._close_callbacks = [] #: Show option default values when formatting help text.
self.show_default: t.Optional[bool] = show_default
self._close_callbacks: t.List[t.Callable[[], t.Any]] = []
self._depth = 0 self._depth = 0
self._parameter_source: t.Dict[str, ParameterSource] = {}
self._exit_stack = ExitStack()
def __enter__(self): def to_info_dict(self) -> t.Dict[str, t.Any]:
"""Gather information that could be useful for a tool generating
user-facing documentation. This traverses the entire CLI
structure.
.. code-block:: python
with Context(cli) as ctx:
info = ctx.to_info_dict()
.. versionadded:: 8.0
"""
return {
"command": self.command.to_info_dict(self),
"info_name": self.info_name,
"allow_extra_args": self.allow_extra_args,
"allow_interspersed_args": self.allow_interspersed_args,
"ignore_unknown_options": self.ignore_unknown_options,
"auto_envvar_prefix": self.auto_envvar_prefix,
}
def __enter__(self) -> "Context":
self._depth += 1 self._depth += 1
push_context(self) push_context(self)
return self return self
def __exit__(self, exc_type, exc_value, tb): def __exit__(self, exc_type, exc_value, tb): # type: ignore
self._depth -= 1 self._depth -= 1
if self._depth == 0: if self._depth == 0:
self.close() self.close()
pop_context() pop_context()
@contextmanager @contextmanager
def scope(self, cleanup=True): def scope(self, cleanup: bool = True) -> t.Iterator["Context"]:
"""This helper method can be used with the context object to promote """This helper method can be used with the context object to promote
it to the current thread local (see :func:`get_current_context`). it to the current thread local (see :func:`get_current_context`).
The default behavior of this is to invoke the cleanup functions which The default behavior of this is to invoke the cleanup functions which
@ -443,7 +494,7 @@ class Context(object):
self._depth -= 1 self._depth -= 1
@property @property
def meta(self): def meta(self) -> t.Dict[str, t.Any]:
"""This is a dictionary which is shared with all the contexts """This is a dictionary which is shared with all the contexts
that are nested. It exists so that click utilities can store some that are nested. It exists so that click utilities can store some
state here if they need to. It is however the responsibility of state here if they need to. It is however the responsibility of
@ -470,32 +521,72 @@ class Context(object):
""" """
return self._meta return self._meta
def make_formatter(self): def make_formatter(self) -> HelpFormatter:
"""Creates the formatter for the help and usage output.""" """Creates the :class:`~click.HelpFormatter` for the help and
return HelpFormatter( usage output.
To quickly customize the formatter class used without overriding
this method, set the :attr:`formatter_class` attribute.
.. versionchanged:: 8.0
Added the :attr:`formatter_class` attribute.
"""
return self.formatter_class(
width=self.terminal_width, max_width=self.max_content_width width=self.terminal_width, max_width=self.max_content_width
) )
def call_on_close(self, f): def with_resource(self, context_manager: t.ContextManager[V]) -> V:
"""This decorator remembers a function as callback that should be """Register a resource as if it were used in a ``with``
executed when the context tears down. This is most useful to bind statement. The resource will be cleaned up when the context is
resource handling to the script execution. For instance, file objects popped.
opened by the :class:`File` type will register their close callbacks
here.
:param f: the function to execute on teardown. Uses :meth:`contextlib.ExitStack.enter_context`. It calls the
resource's ``__enter__()`` method and returns the result. When
the context is popped, it closes the stack, which calls the
resource's ``__exit__()`` method.
To register a cleanup function for something that isn't a
context manager, use :meth:`call_on_close`. Or use something
from :mod:`contextlib` to turn it into a context manager first.
.. code-block:: python
@click.group()
@click.option("--name")
@click.pass_context
def cli(ctx):
ctx.obj = ctx.with_resource(connect_db(name))
:param context_manager: The context manager to enter.
:return: Whatever ``context_manager.__enter__()`` returns.
.. versionadded:: 8.0
""" """
self._close_callbacks.append(f) return self._exit_stack.enter_context(context_manager)
return f
def close(self): def call_on_close(self, f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
"""Invokes all close callbacks.""" """Register a function to be called when the context tears down.
for cb in self._close_callbacks:
cb() This can be used to close resources opened during the script
self._close_callbacks = [] execution. Resources that support Python's context manager
protocol which would be used in a ``with`` statement should be
registered with :meth:`with_resource` instead.
:param f: The function to execute on teardown.
"""
return self._exit_stack.callback(f)
def close(self) -> None:
"""Invoke all close callbacks registered with
:meth:`call_on_close`, and exit all context managers entered
with :meth:`with_resource`.
"""
self._exit_stack.close()
# In case the context is reused, create a new exit stack.
self._exit_stack = ExitStack()
@property @property
def command_path(self): def command_path(self) -> str:
"""The computed command path. This is used for the ``usage`` """The computed command path. This is used for the ``usage``
information on the help page. It's automatically created by information on the help page. It's automatically created by
combining the info names of the chain of contexts to the root. combining the info names of the chain of contexts to the root.
@ -504,25 +595,35 @@ class Context(object):
if self.info_name is not None: if self.info_name is not None:
rv = self.info_name rv = self.info_name
if self.parent is not None: if self.parent is not None:
rv = "{} {}".format(self.parent.command_path, rv) parent_command_path = [self.parent.command_path]
if isinstance(self.parent.command, Command):
for param in self.parent.command.get_params(self):
parent_command_path.extend(param.get_usage_pieces(self))
rv = f"{' '.join(parent_command_path)} {rv}"
return rv.lstrip() return rv.lstrip()
def find_root(self): def find_root(self) -> "Context":
"""Finds the outermost context.""" """Finds the outermost context."""
node = self node = self
while node.parent is not None: while node.parent is not None:
node = node.parent node = node.parent
return node return node
def find_object(self, object_type): def find_object(self, object_type: t.Type[V]) -> t.Optional[V]:
"""Finds the closest object of a given type.""" """Finds the closest object of a given type."""
node = self node: t.Optional["Context"] = self
while node is not None: while node is not None:
if isinstance(node.obj, object_type): if isinstance(node.obj, object_type):
return node.obj return node.obj
node = node.parent node = node.parent
def ensure_object(self, object_type): return None
def ensure_object(self, object_type: t.Type[V]) -> V:
"""Like :meth:`find_object` but sets the innermost object to a """Like :meth:`find_object` but sets the innermost object to a
new instance of `object_type` if it does not exist. new instance of `object_type` if it does not exist.
""" """
@ -531,17 +632,39 @@ class Context(object):
self.obj = rv = object_type() self.obj = rv = object_type()
return rv return rv
def lookup_default(self, name): @typing.overload
"""Looks up the default for a parameter name. This by default def lookup_default(
looks into the :attr:`default_map` if available. self, name: str, call: "te.Literal[True]" = True
) -> t.Optional[t.Any]:
...
@typing.overload
def lookup_default(
self, name: str, call: "te.Literal[False]" = ...
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
...
def lookup_default(self, name: str, call: bool = True) -> t.Optional[t.Any]:
"""Get the default for a parameter from :attr:`default_map`.
:param name: Name of the parameter.
:param call: If the default is a callable, call it. Disable to
return the callable instead.
.. versionchanged:: 8.0
Added the ``call`` parameter.
""" """
if self.default_map is not None: if self.default_map is not None:
rv = self.default_map.get(name) value = self.default_map.get(name)
if callable(rv):
rv = rv()
return rv
def fail(self, message): if call and callable(value):
return value()
return value
return None
def fail(self, message: str) -> "te.NoReturn":
"""Aborts the execution of the program with a specific error """Aborts the execution of the program with a specific error
message. message.
@ -549,27 +672,40 @@ class Context(object):
""" """
raise UsageError(message, self) raise UsageError(message, self)
def abort(self): def abort(self) -> "te.NoReturn":
"""Aborts the script.""" """Aborts the script."""
raise Abort() raise Abort()
def exit(self, code=0): def exit(self, code: int = 0) -> "te.NoReturn":
"""Exits the application with a given exit code.""" """Exits the application with a given exit code."""
raise Exit(code) raise Exit(code)
def get_usage(self): def get_usage(self) -> str:
"""Helper method to get formatted usage string for the current """Helper method to get formatted usage string for the current
context and command. context and command.
""" """
return self.command.get_usage(self) return self.command.get_usage(self)
def get_help(self): def get_help(self) -> str:
"""Helper method to get formatted help page for the current """Helper method to get formatted help page for the current
context and command. context and command.
""" """
return self.command.get_help(self) return self.command.get_help(self)
def invoke(*args, **kwargs): # noqa: B902 def _make_sub_context(self, command: "Command") -> "Context":
"""Create a new context of the same type as this context, but
for a new command.
:meta private:
"""
return type(self)(command, info_name=command.name, parent=self)
def invoke(
__self, # noqa: B902
__callback: t.Union["Command", t.Callable[..., t.Any]],
*args: t.Any,
**kwargs: t.Any,
) -> t.Any:
"""Invokes a command callback in exactly the way it expects. There """Invokes a command callback in exactly the way it expects. There
are two ways to invoke this method: are two ways to invoke this method:
@ -584,51 +720,87 @@ class Context(object):
in against the intention of this code and no context was created. For in against the intention of this code and no context was created. For
more information about this change and why it was done in a bugfix more information about this change and why it was done in a bugfix
release see :ref:`upgrade-to-3.2`. release see :ref:`upgrade-to-3.2`.
"""
self, callback = args[:2]
ctx = self
# It's also possible to invoke another command which might or .. versionchanged:: 8.0
# might not have a callback. In that case we also fill All ``kwargs`` are tracked in :attr:`params` so they will be
# in defaults and make a new context for this command. passed if :meth:`forward` is called at multiple levels.
if isinstance(callback, Command): """
other_cmd = callback if isinstance(__callback, Command):
callback = other_cmd.callback other_cmd = __callback
ctx = Context(other_cmd, info_name=other_cmd.name, parent=self)
if callback is None: if other_cmd.callback is None:
raise TypeError( raise TypeError(
"The given command does not have a callback that can be invoked." "The given command does not have a callback that can be invoked."
) )
else:
__callback = other_cmd.callback
ctx = __self._make_sub_context(other_cmd)
for param in other_cmd.params: for param in other_cmd.params:
if param.name not in kwargs and param.expose_value: if param.name not in kwargs and param.expose_value:
kwargs[param.name] = param.get_default(ctx) kwargs[param.name] = param.get_default(ctx) # type: ignore
args = args[2:] # Track all kwargs as params, so that forward() will pass
with augment_usage_errors(self): # them on in subsequent calls.
ctx.params.update(kwargs)
else:
ctx = __self
with augment_usage_errors(__self):
with ctx: with ctx:
return callback(*args, **kwargs) return __callback(*args, **kwargs)
def forward(*args, **kwargs): # noqa: B902 def forward(
__self, __cmd: "Command", *args: t.Any, **kwargs: t.Any # noqa: B902
) -> t.Any:
"""Similar to :meth:`invoke` but fills in default keyword """Similar to :meth:`invoke` but fills in default keyword
arguments from the current context if the other command expects arguments from the current context if the other command expects
it. This cannot invoke callbacks directly, only other commands. it. This cannot invoke callbacks directly, only other commands.
"""
self, cmd = args[:2]
# It's also possible to invoke another command which might or .. versionchanged:: 8.0
# might not have a callback. All ``kwargs`` are tracked in :attr:`params` so they will be
if not isinstance(cmd, Command): passed if ``forward`` is called at multiple levels.
"""
# Can only forward to other commands, not direct callbacks.
if not isinstance(__cmd, Command):
raise TypeError("Callback is not a command.") raise TypeError("Callback is not a command.")
for param in self.params: for param in __self.params:
if param not in kwargs: if param not in kwargs:
kwargs[param] = self.params[param] kwargs[param] = __self.params[param]
return self.invoke(cmd, **kwargs) return __self.invoke(__cmd, *args, **kwargs)
def set_parameter_source(self, name: str, source: ParameterSource) -> None:
"""Set the source of a parameter. This indicates the location
from which the value of the parameter was obtained.
:param name: The name of the parameter.
:param source: A member of :class:`~click.core.ParameterSource`.
"""
self._parameter_source[name] = source
def get_parameter_source(self, name: str) -> t.Optional[ParameterSource]:
"""Get the source of a parameter. This indicates the location
from which the value of the parameter was obtained.
This can be useful for determining when a user specified a value
on the command line that is the same as the default value. It
will be :attr:`~click.core.ParameterSource.DEFAULT` only if the
value was actually taken from the default.
:param name: The name of the parameter.
:rtype: ParameterSource
.. versionchanged:: 8.0
Returns ``None`` if the parameter was not provided from any
source.
"""
return self._parameter_source.get(name)
class BaseCommand(object): class BaseCommand:
"""The base command implements the minimal API contract of commands. """The base command implements the minimal API contract of commands.
Most code will never use this as it does not implement a lot of useful Most code will never use this as it does not implement a lot of useful
functionality but it can act as the direct subclass of alternative functionality but it can act as the direct subclass of alternative
@ -650,6 +822,10 @@ class BaseCommand(object):
passed to the context object. passed to the context object.
""" """
#: The context class to create with :meth:`make_context`.
#:
#: .. versionadded:: 8.0
context_class: t.Type[Context] = Context
#: the default for the :attr:`Context.allow_extra_args` flag. #: the default for the :attr:`Context.allow_extra_args` flag.
allow_extra_args = False allow_extra_args = False
#: the default for the :attr:`Context.allow_interspersed_args` flag. #: the default for the :attr:`Context.allow_interspersed_args` flag.
@ -657,70 +833,158 @@ class BaseCommand(object):
#: the default for the :attr:`Context.ignore_unknown_options` flag. #: the default for the :attr:`Context.ignore_unknown_options` flag.
ignore_unknown_options = False ignore_unknown_options = False
def __init__(self, name, context_settings=None): def __init__(
self,
name: t.Optional[str],
context_settings: t.Optional[t.Dict[str, t.Any]] = None,
) -> None:
#: the name the command thinks it has. Upon registering a command #: the name the command thinks it has. Upon registering a command
#: on a :class:`Group` the group will default the command name #: on a :class:`Group` the group will default the command name
#: with this information. You should instead use the #: with this information. You should instead use the
#: :class:`Context`\'s :attr:`~Context.info_name` attribute. #: :class:`Context`\'s :attr:`~Context.info_name` attribute.
self.name = name self.name = name
if context_settings is None: if context_settings is None:
context_settings = {} context_settings = {}
#: an optional dictionary with defaults passed to the context. #: an optional dictionary with defaults passed to the context.
self.context_settings = context_settings self.context_settings: t.Dict[str, t.Any] = context_settings
def __repr__(self): def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]:
return "<{} {}>".format(self.__class__.__name__, self.name) """Gather information that could be useful for a tool generating
user-facing documentation. This traverses the entire structure
below this command.
def get_usage(self, ctx): Use :meth:`click.Context.to_info_dict` to traverse the entire
CLI structure.
:param ctx: A :class:`Context` representing this command.
.. versionadded:: 8.0
"""
return {"name": self.name}
def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.name}>"
def get_usage(self, ctx: Context) -> str:
raise NotImplementedError("Base commands cannot get usage") raise NotImplementedError("Base commands cannot get usage")
def get_help(self, ctx): def get_help(self, ctx: Context) -> str:
raise NotImplementedError("Base commands cannot get help") raise NotImplementedError("Base commands cannot get help")
def make_context(self, info_name, args, parent=None, **extra): def make_context(
self,
info_name: t.Optional[str],
args: t.List[str],
parent: t.Optional[Context] = None,
**extra: t.Any,
) -> Context:
"""This function when given an info name and arguments will kick """This function when given an info name and arguments will kick
off the parsing and create a new :class:`Context`. It does not off the parsing and create a new :class:`Context`. It does not
invoke the actual command callback though. invoke the actual command callback though.
:param info_name: the info name for this invokation. Generally this To quickly customize the context class used without overriding
this method, set the :attr:`context_class` attribute.
:param info_name: the info name for this invocation. Generally this
is the most descriptive name for the script or is the most descriptive name for the script or
command. For the toplevel script it's usually command. For the toplevel script it's usually
the name of the script, for commands below it it's the name of the script, for commands below it it's
the name of the script. the name of the command.
:param args: the arguments to parse as list of strings. :param args: the arguments to parse as list of strings.
:param parent: the parent context if available. :param parent: the parent context if available.
:param extra: extra keyword arguments forwarded to the context :param extra: extra keyword arguments forwarded to the context
constructor. constructor.
.. versionchanged:: 8.0
Added the :attr:`context_class` attribute.
""" """
for key, value in iteritems(self.context_settings): for key, value in self.context_settings.items():
if key not in extra: if key not in extra:
extra[key] = value extra[key] = value
ctx = Context(self, info_name=info_name, parent=parent, **extra)
ctx = self.context_class(
self, info_name=info_name, parent=parent, **extra # type: ignore
)
with ctx.scope(cleanup=False): with ctx.scope(cleanup=False):
self.parse_args(ctx, args) self.parse_args(ctx, args)
return ctx return ctx
def parse_args(self, ctx, args): def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]:
"""Given a context and a list of arguments this creates the parser """Given a context and a list of arguments this creates the parser
and parses the arguments, then modifies the context as necessary. and parses the arguments, then modifies the context as necessary.
This is automatically invoked by :meth:`make_context`. This is automatically invoked by :meth:`make_context`.
""" """
raise NotImplementedError("Base commands do not know how to parse arguments.") raise NotImplementedError("Base commands do not know how to parse arguments.")
def invoke(self, ctx): def invoke(self, ctx: Context) -> t.Any:
"""Given a context, this invokes the command. The default """Given a context, this invokes the command. The default
implementation is raising a not implemented error. implementation is raising a not implemented error.
""" """
raise NotImplementedError("Base commands are not invokable by default") raise NotImplementedError("Base commands are not invokable by default")
def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]:
"""Return a list of completions for the incomplete value. Looks
at the names of chained multi-commands.
Any command could be part of a chained multi-command, so sibling
commands are valid at any point during command completion. Other
command classes will return more completions.
:param ctx: Invocation context for this command.
:param incomplete: Value being completed. May be empty.
.. versionadded:: 8.0
"""
from click.shell_completion import CompletionItem
results: t.List["CompletionItem"] = []
while ctx.parent is not None:
ctx = ctx.parent
if isinstance(ctx.command, MultiCommand) and ctx.command.chain:
results.extend(
CompletionItem(name, help=command.get_short_help_str())
for name, command in _complete_visible_commands(ctx, incomplete)
if name not in ctx.protected_args
)
return results
@typing.overload
def main( def main(
self, self,
args=None, args: t.Optional[t.Sequence[str]] = None,
prog_name=None, prog_name: t.Optional[str] = None,
complete_var=None, complete_var: t.Optional[str] = None,
standalone_mode=True, standalone_mode: "te.Literal[True]" = True,
**extra **extra: t.Any,
): ) -> "te.NoReturn":
...
@typing.overload
def main(
self,
args: t.Optional[t.Sequence[str]] = None,
prog_name: t.Optional[str] = None,
complete_var: t.Optional[str] = None,
standalone_mode: bool = ...,
**extra: t.Any,
) -> t.Any:
...
def main(
self,
args: t.Optional[t.Sequence[str]] = None,
prog_name: t.Optional[str] = None,
complete_var: t.Optional[str] = None,
standalone_mode: bool = True,
windows_expand_args: bool = True,
**extra: t.Any,
) -> t.Any:
"""This is the way to invoke a script with all the bells and """This is the way to invoke a script with all the bells and
whistles as a command line application. This will always terminate whistles as a command line application. This will always terminate
the application after a call. If this is not wanted, ``SystemExit`` the application after a call. If this is not wanted, ``SystemExit``
@ -729,9 +993,6 @@ class BaseCommand(object):
This method is also available by directly calling the instance of This method is also available by directly calling the instance of
a :class:`Command`. a :class:`Command`.
.. versionadded:: 3.0
Added the `standalone_mode` flag to control the standalone mode.
:param args: the arguments that should be used for parsing. If not :param args: the arguments that should be used for parsing. If not
provided, ``sys.argv[1:]`` is used. provided, ``sys.argv[1:]`` is used.
:param prog_name: the program name that should be used. By default :param prog_name: the program name that should be used. By default
@ -750,31 +1011,39 @@ class BaseCommand(object):
propagated to the caller and the return propagated to the caller and the return
value of this function is the return value value of this function is the return value
of :meth:`invoke`. of :meth:`invoke`.
:param windows_expand_args: Expand glob patterns, user dir, and
env vars in command line args on Windows.
:param extra: extra keyword arguments are forwarded to the context :param extra: extra keyword arguments are forwarded to the context
constructor. See :class:`Context` for more information. constructor. See :class:`Context` for more information.
.. versionchanged:: 8.0.1
Added the ``windows_expand_args`` parameter to allow
disabling command line arg expansion on Windows.
.. versionchanged:: 8.0
When taking arguments from ``sys.argv`` on Windows, glob
patterns, user dir, and env vars are expanded.
.. versionchanged:: 3.0
Added the ``standalone_mode`` parameter.
""" """
# If we are in Python 3, we will verify that the environment is # Verify that the environment is configured correctly, or reject
# sane at this point or reject further execution to avoid a # further execution to avoid a broken script.
# broken script. _verify_python_env()
if not PY2:
_verify_python3_env()
else:
_check_for_unicode_literals()
if args is None: if args is None:
args = get_os_args() args = sys.argv[1:]
if os.name == "nt" and windows_expand_args:
args = _expand_args(args)
else: else:
args = list(args) args = list(args)
if prog_name is None: if prog_name is None:
prog_name = make_str( prog_name = _detect_program_name()
os.path.basename(sys.argv[0] if sys.argv else __file__)
)
# Hook for the Bash completion. This only activates if the Bash # Process shell completion requests and exit early.
# completion is actually enabled, otherwise this is quite a fast self._main_shell_completion(extra, prog_name, complete_var)
# noop.
_bashcomplete(self, prog_name, complete_var)
try: try:
try: try:
@ -792,16 +1061,16 @@ class BaseCommand(object):
ctx.exit() ctx.exit()
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
echo(file=sys.stderr) echo(file=sys.stderr)
raise Abort() raise Abort() from None
except ClickException as e: except ClickException as e:
if not standalone_mode: if not standalone_mode:
raise raise
e.show() e.show()
sys.exit(e.exit_code) sys.exit(e.exit_code)
except IOError as e: except OSError as e:
if e.errno == errno.EPIPE: if e.errno == errno.EPIPE:
sys.stdout = PacifyFlushWrapper(sys.stdout) sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout))
sys.stderr = PacifyFlushWrapper(sys.stderr) sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr))
sys.exit(1) sys.exit(1)
else: else:
raise raise
@ -821,10 +1090,38 @@ class BaseCommand(object):
except Abort: except Abort:
if not standalone_mode: if not standalone_mode:
raise raise
echo("Aborted!", file=sys.stderr) echo(_("Aborted!"), file=sys.stderr)
sys.exit(1) sys.exit(1)
def __call__(self, *args, **kwargs): def _main_shell_completion(
self,
ctx_args: t.Dict[str, t.Any],
prog_name: str,
complete_var: t.Optional[str] = None,
) -> None:
"""Check if the shell is asking for tab completion, process
that, then exit early. Called from :meth:`main` before the
program is invoked.
:param prog_name: Name of the executable in the shell.
:param complete_var: Name of the environment variable that holds
the completion instruction. Defaults to
``_{PROG_NAME}_COMPLETE``.
"""
if complete_var is None:
complete_var = f"_{prog_name}_COMPLETE".replace("-", "_").upper()
instruction = os.environ.get(complete_var)
if not instruction:
return
from .shell_completion import shell_complete
rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction)
sys.exit(rv)
def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
"""Alias for :meth:`main`.""" """Alias for :meth:`main`."""
return self.main(*args, **kwargs) return self.main(*args, **kwargs)
@ -836,6 +1133,8 @@ class Command(BaseCommand):
.. versionchanged:: 2.0 .. versionchanged:: 2.0
Added the `context_settings` parameter. Added the `context_settings` parameter.
.. versionchanged:: 8.0
Added repr showing the command name
.. versionchanged:: 7.1 .. versionchanged:: 7.1
Added the `no_args_is_help` parameter. Added the `no_args_is_help` parameter.
@ -864,31 +1163,33 @@ class Command(BaseCommand):
def __init__( def __init__(
self, self,
name, name: t.Optional[str],
context_settings=None, context_settings: t.Optional[t.Dict[str, t.Any]] = None,
callback=None, callback: t.Optional[t.Callable[..., t.Any]] = None,
params=None, params: t.Optional[t.List["Parameter"]] = None,
help=None, help: t.Optional[str] = None,
epilog=None, epilog: t.Optional[str] = None,
short_help=None, short_help: t.Optional[str] = None,
options_metavar="[OPTIONS]", options_metavar: t.Optional[str] = "[OPTIONS]",
add_help_option=True, add_help_option: bool = True,
no_args_is_help=False, no_args_is_help: bool = False,
hidden=False, hidden: bool = False,
deprecated=False, deprecated: bool = False,
): ) -> None:
BaseCommand.__init__(self, name, context_settings) super().__init__(name, context_settings)
#: the callback to execute when the command fires. This might be #: the callback to execute when the command fires. This might be
#: `None` in which case nothing happens. #: `None` in which case nothing happens.
self.callback = callback self.callback = callback
#: the list of parameters for this command in the order they #: the list of parameters for this command in the order they
#: should show up in the help page and execute. Eager parameters #: should show up in the help page and execute. Eager parameters
#: will automatically be handled before non eager ones. #: will automatically be handled before non eager ones.
self.params = params or [] self.params: t.List["Parameter"] = params or []
# if a form feed (page break) is found in the help text, truncate help # if a form feed (page break) is found in the help text, truncate help
# text to the content preceding the first form feed # text to the content preceding the first form feed
if help and "\f" in help: if help and "\f" in help:
help = help.split("\f", 1)[0] help = help.split("\f", 1)[0]
self.help = help self.help = help
self.epilog = epilog self.epilog = epilog
self.options_metavar = options_metavar self.options_metavar = options_metavar
@ -898,7 +1199,19 @@ class Command(BaseCommand):
self.hidden = hidden self.hidden = hidden
self.deprecated = deprecated self.deprecated = deprecated
def get_usage(self, ctx): def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict(ctx)
info_dict.update(
params=[param.to_info_dict() for param in self.get_params(ctx)],
help=self.help,
epilog=self.epilog,
short_help=self.short_help,
hidden=self.hidden,
deprecated=self.deprecated,
)
return info_dict
def get_usage(self, ctx: Context) -> str:
"""Formats the usage line into a string and returns it. """Formats the usage line into a string and returns it.
Calls :meth:`format_usage` internally. Calls :meth:`format_usage` internally.
@ -907,14 +1220,16 @@ class Command(BaseCommand):
self.format_usage(ctx, formatter) self.format_usage(ctx, formatter)
return formatter.getvalue().rstrip("\n") return formatter.getvalue().rstrip("\n")
def get_params(self, ctx): def get_params(self, ctx: Context) -> t.List["Parameter"]:
rv = self.params rv = self.params
help_option = self.get_help_option(ctx) help_option = self.get_help_option(ctx)
if help_option is not None: if help_option is not None:
rv = rv + [help_option] rv = [*rv, help_option]
return rv return rv
def format_usage(self, ctx, formatter): def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None:
"""Writes the usage line into the formatter. """Writes the usage line into the formatter.
This is a low-level method called by :meth:`get_usage`. This is a low-level method called by :meth:`get_usage`.
@ -922,30 +1237,33 @@ class Command(BaseCommand):
pieces = self.collect_usage_pieces(ctx) pieces = self.collect_usage_pieces(ctx)
formatter.write_usage(ctx.command_path, " ".join(pieces)) formatter.write_usage(ctx.command_path, " ".join(pieces))
def collect_usage_pieces(self, ctx): def collect_usage_pieces(self, ctx: Context) -> t.List[str]:
"""Returns all the pieces that go into the usage line and returns """Returns all the pieces that go into the usage line and returns
it as a list of strings. it as a list of strings.
""" """
rv = [self.options_metavar] rv = [self.options_metavar] if self.options_metavar else []
for param in self.get_params(ctx): for param in self.get_params(ctx):
rv.extend(param.get_usage_pieces(ctx)) rv.extend(param.get_usage_pieces(ctx))
return rv return rv
def get_help_option_names(self, ctx): def get_help_option_names(self, ctx: Context) -> t.List[str]:
"""Returns the names for the help option.""" """Returns the names for the help option."""
all_names = set(ctx.help_option_names) all_names = set(ctx.help_option_names)
for param in self.params: for param in self.params:
all_names.difference_update(param.opts) all_names.difference_update(param.opts)
all_names.difference_update(param.secondary_opts) all_names.difference_update(param.secondary_opts)
return all_names return list(all_names)
def get_help_option(self, ctx): def get_help_option(self, ctx: Context) -> t.Optional["Option"]:
"""Returns the help option object.""" """Returns the help option object."""
help_options = self.get_help_option_names(ctx) help_options = self.get_help_option_names(ctx)
if not help_options or not self.add_help_option:
return
def show_help(ctx, param, value): if not help_options or not self.add_help_option:
return None
def show_help(ctx: Context, param: "Parameter", value: str) -> None:
if value and not ctx.resilient_parsing: if value and not ctx.resilient_parsing:
echo(ctx.get_help(), color=ctx.color) echo(ctx.get_help(), color=ctx.color)
ctx.exit() ctx.exit()
@ -956,17 +1274,17 @@ class Command(BaseCommand):
is_eager=True, is_eager=True,
expose_value=False, expose_value=False,
callback=show_help, callback=show_help,
help="Show this message and exit.", help=_("Show this message and exit."),
) )
def make_parser(self, ctx): def make_parser(self, ctx: Context) -> OptionParser:
"""Creates the underlying option parser for this command.""" """Creates the underlying option parser for this command."""
parser = OptionParser(ctx) parser = OptionParser(ctx)
for param in self.get_params(ctx): for param in self.get_params(ctx):
param.add_to_parser(parser, ctx) param.add_to_parser(parser, ctx)
return parser return parser
def get_help(self, ctx): def get_help(self, ctx: Context) -> str:
"""Formats the help into a string and returns it. """Formats the help into a string and returns it.
Calls :meth:`format_help` internally. Calls :meth:`format_help` internally.
@ -975,18 +1293,21 @@ class Command(BaseCommand):
self.format_help(ctx, formatter) self.format_help(ctx, formatter)
return formatter.getvalue().rstrip("\n") return formatter.getvalue().rstrip("\n")
def get_short_help_str(self, limit=45): def get_short_help_str(self, limit: int = 45) -> str:
"""Gets short help for the command or makes it by shortening the """Gets short help for the command or makes it by shortening the
long help string. long help string.
""" """
return ( text = self.short_help or ""
self.short_help
or self.help
and make_default_short_help(self.help, limit)
or ""
)
def format_help(self, ctx, formatter): if not text and self.help:
text = make_default_short_help(self.help, limit)
if self.deprecated:
text = _("(Deprecated) {text}").format(text=text)
return text.strip()
def format_help(self, ctx: Context, formatter: HelpFormatter) -> None:
"""Writes the help into the formatter if it exists. """Writes the help into the formatter if it exists.
This is a low-level method called by :meth:`get_help`. This is a low-level method called by :meth:`get_help`.
@ -1003,21 +1324,20 @@ class Command(BaseCommand):
self.format_options(ctx, formatter) self.format_options(ctx, formatter)
self.format_epilog(ctx, formatter) self.format_epilog(ctx, formatter)
def format_help_text(self, ctx, formatter): def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None:
"""Writes the help text to the formatter if it exists.""" """Writes the help text to the formatter if it exists."""
if self.help: text = self.help or ""
formatter.write_paragraph()
with formatter.indentation():
help_text = self.help
if self.deprecated:
help_text += DEPRECATED_HELP_NOTICE
formatter.write_text(help_text)
elif self.deprecated:
formatter.write_paragraph()
with formatter.indentation():
formatter.write_text(DEPRECATED_HELP_NOTICE)
def format_options(self, ctx, formatter): if self.deprecated:
text = _("(Deprecated) {text}").format(text=text)
if text:
formatter.write_paragraph()
with formatter.indentation():
formatter.write_text(text)
def format_options(self, ctx: Context, formatter: HelpFormatter) -> None:
"""Writes all the options into the formatter if they exist.""" """Writes all the options into the formatter if they exist."""
opts = [] opts = []
for param in self.get_params(ctx): for param in self.get_params(ctx):
@ -1026,17 +1346,17 @@ class Command(BaseCommand):
opts.append(rv) opts.append(rv)
if opts: if opts:
with formatter.section("Options"): with formatter.section(_("Options")):
formatter.write_dl(opts) formatter.write_dl(opts)
def format_epilog(self, ctx, formatter): def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None:
"""Writes the epilog into the formatter if it exists.""" """Writes the epilog into the formatter if it exists."""
if self.epilog: if self.epilog:
formatter.write_paragraph() formatter.write_paragraph()
with formatter.indentation(): with formatter.indentation():
formatter.write_text(self.epilog) formatter.write_text(self.epilog)
def parse_args(self, ctx, args): def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]:
if not args and self.no_args_is_help and not ctx.resilient_parsing: if not args and self.no_args_is_help and not ctx.resilient_parsing:
echo(ctx.get_help(), color=ctx.color) echo(ctx.get_help(), color=ctx.color)
ctx.exit() ctx.exit()
@ -1049,22 +1369,64 @@ class Command(BaseCommand):
if args and not ctx.allow_extra_args and not ctx.resilient_parsing: if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
ctx.fail( ctx.fail(
"Got unexpected extra argument{} ({})".format( ngettext(
"s" if len(args) != 1 else "", " ".join(map(make_str, args)) "Got unexpected extra argument ({args})",
) "Got unexpected extra arguments ({args})",
len(args),
).format(args=" ".join(map(str, args)))
) )
ctx.args = args ctx.args = args
return args return args
def invoke(self, ctx): def invoke(self, ctx: Context) -> t.Any:
"""Given a context, this invokes the attached callback (if it exists) """Given a context, this invokes the attached callback (if it exists)
in the right way. in the right way.
""" """
_maybe_show_deprecated_notice(self) if self.deprecated:
message = _(
"DeprecationWarning: The command {name!r} is deprecated."
).format(name=self.name)
echo(style(message, fg="red"), err=True)
if self.callback is not None: if self.callback is not None:
return ctx.invoke(self.callback, **ctx.params) return ctx.invoke(self.callback, **ctx.params)
def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]:
"""Return a list of completions for the incomplete value. Looks
at the names of options and chained multi-commands.
:param ctx: Invocation context for this command.
:param incomplete: Value being completed. May be empty.
.. versionadded:: 8.0
"""
from click.shell_completion import CompletionItem
results: t.List["CompletionItem"] = []
if incomplete and not incomplete[0].isalnum():
for param in self.get_params(ctx):
if (
not isinstance(param, Option)
or param.hidden
or (
not param.multiple
and ctx.get_parameter_source(param.name) # type: ignore
is ParameterSource.COMMANDLINE
)
):
continue
results.extend(
CompletionItem(name, help=param.help)
for name in [*param.opts, *param.secondary_opts]
if name.startswith(incomplete)
)
results.extend(super().shell_complete(ctx, incomplete))
return results
class MultiCommand(Command): class MultiCommand(Command):
"""A multi command is the basic implementation of a command that """A multi command is the basic implementation of a command that
@ -1086,8 +1448,9 @@ class MultiCommand(Command):
is enabled. This restricts the form of commands in that is enabled. This restricts the form of commands in that
they cannot have optional arguments but it allows they cannot have optional arguments but it allows
multiple commands to be chained together. multiple commands to be chained together.
:param result_callback: the result callback to attach to this multi :param result_callback: The result callback to attach to this multi
command. command. This can be set or changed later with the
:meth:`result_callback` decorator.
""" """
allow_extra_args = True allow_extra_args = True
@ -1095,29 +1458,33 @@ class MultiCommand(Command):
def __init__( def __init__(
self, self,
name=None, name: t.Optional[str] = None,
invoke_without_command=False, invoke_without_command: bool = False,
no_args_is_help=None, no_args_is_help: t.Optional[bool] = None,
subcommand_metavar=None, subcommand_metavar: t.Optional[str] = None,
chain=False, chain: bool = False,
result_callback=None, result_callback: t.Optional[t.Callable[..., t.Any]] = None,
**attrs **attrs: t.Any,
): ) -> None:
Command.__init__(self, name, **attrs) super().__init__(name, **attrs)
if no_args_is_help is None: if no_args_is_help is None:
no_args_is_help = not invoke_without_command no_args_is_help = not invoke_without_command
self.no_args_is_help = no_args_is_help self.no_args_is_help = no_args_is_help
self.invoke_without_command = invoke_without_command self.invoke_without_command = invoke_without_command
if subcommand_metavar is None: if subcommand_metavar is None:
if chain: if chain:
subcommand_metavar = SUBCOMMANDS_METAVAR subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
else: else:
subcommand_metavar = SUBCOMMAND_METAVAR subcommand_metavar = "COMMAND [ARGS]..."
self.subcommand_metavar = subcommand_metavar self.subcommand_metavar = subcommand_metavar
self.chain = chain self.chain = chain
#: The result callback that is stored. This can be set or # The result callback that is stored. This can be set or
#: overridden with the :func:`resultcallback` decorator. # overridden with the :func:`result_callback` decorator.
self.result_callback = result_callback self._result_callback = result_callback
if self.chain: if self.chain:
for param in self.params: for param in self.params:
@ -1127,17 +1494,35 @@ class MultiCommand(Command):
" optional arguments." " optional arguments."
) )
def collect_usage_pieces(self, ctx): def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]:
rv = Command.collect_usage_pieces(self, ctx) info_dict = super().to_info_dict(ctx)
commands = {}
for name in self.list_commands(ctx):
command = self.get_command(ctx, name)
if command is None:
continue
sub_ctx = ctx._make_sub_context(command)
with sub_ctx.scope(cleanup=False):
commands[name] = command.to_info_dict(sub_ctx)
info_dict.update(commands=commands, chain=self.chain)
return info_dict
def collect_usage_pieces(self, ctx: Context) -> t.List[str]:
rv = super().collect_usage_pieces(ctx)
rv.append(self.subcommand_metavar) rv.append(self.subcommand_metavar)
return rv return rv
def format_options(self, ctx, formatter): def format_options(self, ctx: Context, formatter: HelpFormatter) -> None:
Command.format_options(self, ctx, formatter) super().format_options(ctx, formatter)
self.format_commands(ctx, formatter) self.format_commands(ctx, formatter)
def resultcallback(self, replace=False): def result_callback(self, replace: bool = False) -> t.Callable[[F], F]:
"""Adds a result callback to the chain command. By default if a """Adds a result callback to the command. By default if a
result callback is already registered this will chain them but result callback is already registered this will chain them but
this can be disabled with the `replace` parameter. The result this can be disabled with the `replace` parameter. The result
callback is invoked with the return value of the subcommand callback is invoked with the return value of the subcommand
@ -1152,31 +1537,47 @@ class MultiCommand(Command):
def cli(input): def cli(input):
return 42 return 42
@cli.resultcallback() @cli.result_callback()
def process_result(result, input): def process_result(result, input):
return result + input return result + input
.. versionadded:: 3.0
:param replace: if set to `True` an already existing result :param replace: if set to `True` an already existing result
callback will be removed. callback will be removed.
.. versionchanged:: 8.0
Renamed from ``resultcallback``.
.. versionadded:: 3.0
""" """
def decorator(f): def decorator(f: F) -> F:
old_callback = self.result_callback old_callback = self._result_callback
if old_callback is None or replace: if old_callback is None or replace:
self.result_callback = f self._result_callback = f
return f return f
def function(__value, *args, **kwargs): def function(__value, *args, **kwargs): # type: ignore
return f(old_callback(__value, *args, **kwargs), *args, **kwargs) inner = old_callback(__value, *args, **kwargs) # type: ignore
return f(inner, *args, **kwargs)
self.result_callback = rv = update_wrapper(function, f) self._result_callback = rv = update_wrapper(t.cast(F, function), f)
return rv return rv
return decorator return decorator
def format_commands(self, ctx, formatter): def resultcallback(self, replace: bool = False) -> t.Callable[[F], F]:
import warnings
warnings.warn(
"'resultcallback' has been renamed to 'result_callback'."
" The old name will be removed in Click 8.1.",
DeprecationWarning,
stacklevel=2,
)
return self.result_callback(replace=replace)
def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None:
"""Extra format methods for multi methods that adds all the commands """Extra format methods for multi methods that adds all the commands
after the options. after the options.
""" """
@ -1201,15 +1602,16 @@ class MultiCommand(Command):
rows.append((subcommand, help)) rows.append((subcommand, help))
if rows: if rows:
with formatter.section("Commands"): with formatter.section(_("Commands")):
formatter.write_dl(rows) formatter.write_dl(rows)
def parse_args(self, ctx, args): def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]:
if not args and self.no_args_is_help and not ctx.resilient_parsing: if not args and self.no_args_is_help and not ctx.resilient_parsing:
echo(ctx.get_help(), color=ctx.color) echo(ctx.get_help(), color=ctx.color)
ctx.exit() ctx.exit()
rest = Command.parse_args(self, ctx, args) rest = super().parse_args(ctx, args)
if self.chain: if self.chain:
ctx.protected_args = rest ctx.protected_args = rest
ctx.args = [] ctx.args = []
@ -1218,29 +1620,24 @@ class MultiCommand(Command):
return ctx.args return ctx.args
def invoke(self, ctx): def invoke(self, ctx: Context) -> t.Any:
def _process_result(value): def _process_result(value: t.Any) -> t.Any:
if self.result_callback is not None: if self._result_callback is not None:
value = ctx.invoke(self.result_callback, value, **ctx.params) value = ctx.invoke(self._result_callback, value, **ctx.params)
return value return value
if not ctx.protected_args: if not ctx.protected_args:
# If we are invoked without command the chain flag controls
# how this happens. If we are not in chain mode, the return
# value here is the return value of the command.
# If however we are in chain mode, the return value is the
# return value of the result processor invoked with an empty
# list (which means that no subcommand actually was executed).
if self.invoke_without_command: if self.invoke_without_command:
if not self.chain: # No subcommand was invoked, so the result callback is
return Command.invoke(self, ctx) # invoked with None for regular groups, or an empty list
# for chained groups.
with ctx: with ctx:
Command.invoke(self, ctx) super().invoke(ctx)
return _process_result([]) return _process_result([] if self.chain else None)
ctx.fail("Missing command.") ctx.fail(_("Missing command."))
# Fetch args back out # Fetch args back out
args = ctx.protected_args + ctx.args args = [*ctx.protected_args, *ctx.args]
ctx.args = [] ctx.args = []
ctx.protected_args = [] ctx.protected_args = []
@ -1252,8 +1649,9 @@ class MultiCommand(Command):
# resources until the result processor has worked. # resources until the result processor has worked.
with ctx: with ctx:
cmd_name, cmd, args = self.resolve_command(ctx, args) cmd_name, cmd, args = self.resolve_command(ctx, args)
assert cmd is not None
ctx.invoked_subcommand = cmd_name ctx.invoked_subcommand = cmd_name
Command.invoke(self, ctx) super().invoke(ctx)
sub_ctx = cmd.make_context(cmd_name, args, parent=ctx) sub_ctx = cmd.make_context(cmd_name, args, parent=ctx)
with sub_ctx: with sub_ctx:
return _process_result(sub_ctx.command.invoke(sub_ctx)) return _process_result(sub_ctx.command.invoke(sub_ctx))
@ -1265,7 +1663,7 @@ class MultiCommand(Command):
# but nothing else. # but nothing else.
with ctx: with ctx:
ctx.invoked_subcommand = "*" if args else None ctx.invoked_subcommand = "*" if args else None
Command.invoke(self, ctx) super().invoke(ctx)
# Otherwise we make every single context and invoke them in a # Otherwise we make every single context and invoke them in a
# chain. In that case the return value to the result processor # chain. In that case the return value to the result processor
@ -1273,6 +1671,7 @@ class MultiCommand(Command):
contexts = [] contexts = []
while args: while args:
cmd_name, cmd, args = self.resolve_command(ctx, args) cmd_name, cmd, args = self.resolve_command(ctx, args)
assert cmd is not None
sub_ctx = cmd.make_context( sub_ctx = cmd.make_context(
cmd_name, cmd_name,
args, args,
@ -1289,7 +1688,9 @@ class MultiCommand(Command):
rv.append(sub_ctx.command.invoke(sub_ctx)) rv.append(sub_ctx.command.invoke(sub_ctx))
return _process_result(rv) return _process_result(rv)
def resolve_command(self, ctx, args): def resolve_command(
self, ctx: Context, args: t.List[str]
) -> t.Tuple[t.Optional[str], t.Optional[Command], t.List[str]]:
cmd_name = make_str(args[0]) cmd_name = make_str(args[0])
original_cmd_name = cmd_name original_cmd_name = cmd_name
@ -1311,36 +1712,94 @@ class MultiCommand(Command):
if cmd is None and not ctx.resilient_parsing: if cmd is None and not ctx.resilient_parsing:
if split_opt(cmd_name)[0]: if split_opt(cmd_name)[0]:
self.parse_args(ctx, ctx.args) self.parse_args(ctx, ctx.args)
ctx.fail("No such command '{}'.".format(original_cmd_name)) ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name))
return cmd_name if cmd else None, cmd, args[1:]
return cmd_name, cmd, args[1:] def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]:
def get_command(self, ctx, cmd_name):
"""Given a context and a command name, this returns a """Given a context and a command name, this returns a
:class:`Command` object if it exists or returns `None`. :class:`Command` object if it exists or returns `None`.
""" """
raise NotImplementedError() raise NotImplementedError
def list_commands(self, ctx): def list_commands(self, ctx: Context) -> t.List[str]:
"""Returns a list of subcommand names in the order they should """Returns a list of subcommand names in the order they should
appear. appear.
""" """
return [] return []
def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]:
"""Return a list of completions for the incomplete value. Looks
at the names of options, subcommands, and chained
multi-commands.
:param ctx: Invocation context for this command.
:param incomplete: Value being completed. May be empty.
.. versionadded:: 8.0
"""
from click.shell_completion import CompletionItem
results = [
CompletionItem(name, help=command.get_short_help_str())
for name, command in _complete_visible_commands(ctx, incomplete)
]
results.extend(super().shell_complete(ctx, incomplete))
return results
class Group(MultiCommand): class Group(MultiCommand):
"""A group allows a command to have subcommands attached. This is the """A group allows a command to have subcommands attached. This is
most common way to implement nesting in Click. the most common way to implement nesting in Click.
:param commands: a dictionary of commands. :param name: The name of the group command.
:param commands: A dict mapping names to :class:`Command` objects.
Can also be a list of :class:`Command`, which will use
:attr:`Command.name` to create the dict.
:param attrs: Other command arguments described in
:class:`MultiCommand`, :class:`Command`, and
:class:`BaseCommand`.
.. versionchanged:: 8.0
The ``commmands`` argument can be a list of command objects.
""" """
def __init__(self, name=None, commands=None, **attrs): #: If set, this is used by the group's :meth:`command` decorator
MultiCommand.__init__(self, name, **attrs) #: as the default :class:`Command` class. This is useful to make all
#: the registered subcommands by their exported names. #: subcommands use a custom command class.
self.commands = commands or {} #:
#: .. versionadded:: 8.0
command_class: t.Optional[t.Type[Command]] = None
def add_command(self, cmd, name=None): #: If set, this is used by the group's :meth:`group` decorator
#: as the default :class:`Group` class. This is useful to make all
#: subgroups use a custom group class.
#:
#: If set to the special value :class:`type` (literally
#: ``group_class = type``), this group's class will be used as the
#: default class. This makes a custom group class continue to make
#: custom groups.
#:
#: .. versionadded:: 8.0
group_class: t.Optional[t.Union[t.Type["Group"], t.Type[type]]] = None
# Literal[type] isn't valid, so use Type[type]
def __init__(
self,
name: t.Optional[str] = None,
commands: t.Optional[t.Union[t.Dict[str, Command], t.Sequence[Command]]] = None,
**attrs: t.Any,
) -> None:
super().__init__(name, **attrs)
if commands is None:
commands = {}
elif isinstance(commands, abc.Sequence):
commands = {c.name: c for c in commands if c.name is not None}
#: The registered subcommands by their exported names.
self.commands: t.Dict[str, Command] = commands
def add_command(self, cmd: Command, name: t.Optional[str] = None) -> None:
"""Registers another :class:`Command` with this group. If the name """Registers another :class:`Command` with this group. If the name
is not provided, the name of the command is used. is not provided, the name of the command is used.
""" """
@ -1350,40 +1809,65 @@ class Group(MultiCommand):
_check_multicommand(self, name, cmd, register=True) _check_multicommand(self, name, cmd, register=True)
self.commands[name] = cmd self.commands[name] = cmd
def command(self, *args, **kwargs): def command(
self, *args: t.Any, **kwargs: t.Any
) -> t.Callable[[t.Callable[..., t.Any]], Command]:
"""A shortcut decorator for declaring and attaching a command to """A shortcut decorator for declaring and attaching a command to
the group. This takes the same arguments as :func:`command` but the group. This takes the same arguments as :func:`command` and
immediately registers the created command with this instance by immediately registers the created command with this group by
calling into :meth:`add_command`. calling :meth:`add_command`.
To customize the command class used, set the
:attr:`command_class` attribute.
.. versionchanged:: 8.0
Added the :attr:`command_class` attribute.
""" """
from .decorators import command from .decorators import command
def decorator(f): if self.command_class is not None and "cls" not in kwargs:
kwargs["cls"] = self.command_class
def decorator(f: t.Callable[..., t.Any]) -> Command:
cmd = command(*args, **kwargs)(f) cmd = command(*args, **kwargs)(f)
self.add_command(cmd) self.add_command(cmd)
return cmd return cmd
return decorator return decorator
def group(self, *args, **kwargs): def group(
self, *args: t.Any, **kwargs: t.Any
) -> t.Callable[[t.Callable[..., t.Any]], "Group"]:
"""A shortcut decorator for declaring and attaching a group to """A shortcut decorator for declaring and attaching a group to
the group. This takes the same arguments as :func:`group` but the group. This takes the same arguments as :func:`group` and
immediately registers the created command with this instance by immediately registers the created group with this group by
calling into :meth:`add_command`. calling :meth:`add_command`.
To customize the group class used, set the :attr:`group_class`
attribute.
.. versionchanged:: 8.0
Added the :attr:`group_class` attribute.
""" """
from .decorators import group from .decorators import group
def decorator(f): if self.group_class is not None and "cls" not in kwargs:
if self.group_class is type:
kwargs["cls"] = type(self)
else:
kwargs["cls"] = self.group_class
def decorator(f: t.Callable[..., t.Any]) -> "Group":
cmd = group(*args, **kwargs)(f) cmd = group(*args, **kwargs)(f)
self.add_command(cmd) self.add_command(cmd)
return cmd return cmd
return decorator return decorator
def get_command(self, ctx, cmd_name): def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]:
return self.commands.get(cmd_name) return self.commands.get(cmd_name)
def list_commands(self, ctx): def list_commands(self, ctx: Context) -> t.List[str]:
return sorted(self.commands) return sorted(self.commands)
@ -1394,31 +1878,52 @@ class CommandCollection(MultiCommand):
provides all the commands for each of them. provides all the commands for each of them.
""" """
def __init__(self, name=None, sources=None, **attrs): def __init__(
MultiCommand.__init__(self, name, **attrs) self,
name: t.Optional[str] = None,
sources: t.Optional[t.List[MultiCommand]] = None,
**attrs: t.Any,
) -> None:
super().__init__(name, **attrs)
#: The list of registered multi commands. #: The list of registered multi commands.
self.sources = sources or [] self.sources: t.List[MultiCommand] = sources or []
def add_source(self, multi_cmd): def add_source(self, multi_cmd: MultiCommand) -> None:
"""Adds a new multi command to the chain dispatcher.""" """Adds a new multi command to the chain dispatcher."""
self.sources.append(multi_cmd) self.sources.append(multi_cmd)
def get_command(self, ctx, cmd_name): def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]:
for source in self.sources: for source in self.sources:
rv = source.get_command(ctx, cmd_name) rv = source.get_command(ctx, cmd_name)
if rv is not None: if rv is not None:
if self.chain: if self.chain:
_check_multicommand(self, cmd_name, rv) _check_multicommand(self, cmd_name, rv)
return rv return rv
def list_commands(self, ctx): return None
rv = set()
def list_commands(self, ctx: Context) -> t.List[str]:
rv: t.Set[str] = set()
for source in self.sources: for source in self.sources:
rv.update(source.list_commands(ctx)) rv.update(source.list_commands(ctx))
return sorted(rv) return sorted(rv)
class Parameter(object): def _check_iter(value: t.Any) -> t.Iterator[t.Any]:
"""Check if the value is iterable but not a string. Raises a type
error, or return an iterator over the value.
"""
if isinstance(value, str):
raise TypeError
return iter(value)
class Parameter:
r"""A parameter to a command comes in two versions: they are either r"""A parameter to a command comes in two versions: they are either
:class:`Option`\s or :class:`Argument`\s. Other subclasses are currently :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently
not supported by design as some of the internals for parsing are not supported by design as some of the internals for parsing are
@ -1436,13 +1941,15 @@ class Parameter(object):
:param default: the default value if omitted. This can also be a callable, :param default: the default value if omitted. This can also be a callable,
in which case it's invoked when the default is needed in which case it's invoked when the default is needed
without any arguments. without any arguments.
:param callback: a callback that should be executed after the parameter :param callback: A function to further process or validate the value
was matched. This is called as ``fn(ctx, param, after type conversion. It is called as ``f(ctx, param, value)``
value)`` and needs to return the value. and must return the value. It is called for all sources,
including prompts.
:param nargs: the number of arguments to match. If not ``1`` the return :param nargs: the number of arguments to match. If not ``1`` the return
value is a tuple instead of single value. The default for value is a tuple instead of single value. The default for
nargs is ``1`` (except if the type is a tuple, then it's nargs is ``1`` (except if the type is a tuple, then it's
the arity of the tuple). the arity of the tuple). If ``nargs=-1``, all remaining
parameters are collected.
:param metavar: how the value is represented in the help page. :param metavar: how the value is represented in the help page.
:param expose_value: if this is `True` then the value is passed onwards :param expose_value: if this is `True` then the value is passed onwards
to the command callback and stored on the context, to the command callback and stored on the context,
@ -1452,6 +1959,32 @@ class Parameter(object):
order of processing. order of processing.
:param envvar: a string or list of strings that are environment variables :param envvar: a string or list of strings that are environment variables
that should be checked. that should be checked.
:param shell_complete: A function that returns custom shell
completions. Used instead of the param's type completion if
given. Takes ``ctx, param, incomplete`` and must return a list
of :class:`~click.shell_completion.CompletionItem` or a list of
strings.
.. versionchanged:: 8.0
``process_value`` validates required parameters and bounded
``nargs``, and invokes the parameter callback before returning
the value. This allows the callback to validate prompts.
``full_process_value`` is removed.
.. versionchanged:: 8.0
``autocompletion`` is renamed to ``shell_complete`` and has new
semantics described above. The old name is deprecated and will
be removed in 8.1, until then it will be wrapped to match the
new requirements.
.. versionchanged:: 8.0
For ``multiple=True, nargs>1``, the default must be a list of
tuples.
.. versionchanged:: 8.0
Setting a default is no longer required for ``nargs>1``, it will
default to ``None``. ``multiple=True`` or ``nargs=-1`` will
default to ``()``.
.. versionchanged:: 7.1 .. versionchanged:: 7.1
Empty environment variables are ignored rather than taking the Empty environment variables are ignored rather than taking the
@ -1463,27 +1996,38 @@ class Parameter(object):
parameter. The old callback format will still work, but it will parameter. The old callback format will still work, but it will
raise a warning to give you a chance to migrate the code easier. raise a warning to give you a chance to migrate the code easier.
""" """
param_type_name = "parameter" param_type_name = "parameter"
def __init__( def __init__(
self, self,
param_decls=None, param_decls: t.Optional[t.Sequence[str]] = None,
type=None, type: t.Optional[t.Union[types.ParamType, t.Any]] = None,
required=False, required: bool = False,
default=None, default: t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]] = None,
callback=None, callback: t.Optional[t.Callable[[Context, "Parameter", t.Any], t.Any]] = None,
nargs=None, nargs: t.Optional[int] = None,
metavar=None, multiple: bool = False,
expose_value=True, metavar: t.Optional[str] = None,
is_eager=False, expose_value: bool = True,
envvar=None, is_eager: bool = False,
autocompletion=None, envvar: t.Optional[t.Union[str, t.Sequence[str]]] = None,
): shell_complete: t.Optional[
t.Callable[
[Context, "Parameter", str],
t.Union[t.List["CompletionItem"], t.List[str]],
]
] = None,
autocompletion: t.Optional[
t.Callable[
[Context, t.List[str], str], t.List[t.Union[t.Tuple[str, str], str]]
]
] = None,
) -> None:
self.name, self.opts, self.secondary_opts = self._parse_decls( self.name, self.opts, self.secondary_opts = self._parse_decls(
param_decls or (), expose_value param_decls or (), expose_value
) )
self.type = types.convert_type(type, default)
self.type = convert_type(type, default)
# Default nargs to what the type tells us if we have that # Default nargs to what the type tells us if we have that
# information available. # information available.
@ -1496,158 +2040,355 @@ class Parameter(object):
self.required = required self.required = required
self.callback = callback self.callback = callback
self.nargs = nargs self.nargs = nargs
self.multiple = False self.multiple = multiple
self.expose_value = expose_value self.expose_value = expose_value
self.default = default self.default = default
self.is_eager = is_eager self.is_eager = is_eager
self.metavar = metavar self.metavar = metavar
self.envvar = envvar self.envvar = envvar
self.autocompletion = autocompletion
def __repr__(self): if autocompletion is not None:
return "<{} {}>".format(self.__class__.__name__, self.name) import warnings
warnings.warn(
"'autocompletion' is renamed to 'shell_complete'. The old name is"
" deprecated and will be removed in Click 8.1. See the docs about"
" 'Parameter' for information about new behavior.",
DeprecationWarning,
stacklevel=2,
)
def shell_complete(
ctx: Context, param: "Parameter", incomplete: str
) -> t.List["CompletionItem"]:
from click.shell_completion import CompletionItem
out = []
for c in autocompletion(ctx, [], incomplete): # type: ignore
if isinstance(c, tuple):
c = CompletionItem(c[0], help=c[1])
elif isinstance(c, str):
c = CompletionItem(c)
if c.value.startswith(incomplete):
out.append(c)
return out
self._custom_shell_complete = shell_complete
if __debug__:
if self.type.is_composite and nargs != self.type.arity:
raise ValueError(
f"'nargs' must be {self.type.arity} (or None) for"
f" type {self.type!r}, but it was {nargs}."
)
# Skip no default or callable default.
check_default = default if not callable(default) else None
if check_default is not None:
if multiple:
try:
# Only check the first value against nargs.
check_default = next(_check_iter(check_default), None)
except TypeError:
raise ValueError(
"'default' must be a list when 'multiple' is true."
) from None
# Can be None for multiple with empty default.
if nargs != 1 and check_default is not None:
try:
_check_iter(check_default)
except TypeError:
if multiple:
message = (
"'default' must be a list of lists when 'multiple' is"
" true and 'nargs' != 1."
)
else:
message = "'default' must be a list when 'nargs' != 1."
raise ValueError(message) from None
if nargs > 1 and len(check_default) != nargs:
subject = "item length" if multiple else "length"
raise ValueError(
f"'default' {subject} must match nargs={nargs}."
)
def to_info_dict(self) -> t.Dict[str, t.Any]:
"""Gather information that could be useful for a tool generating
user-facing documentation.
Use :meth:`click.Context.to_info_dict` to traverse the entire
CLI structure.
.. versionadded:: 8.0
"""
return {
"name": self.name,
"param_type_name": self.param_type_name,
"opts": self.opts,
"secondary_opts": self.secondary_opts,
"type": self.type.to_info_dict(),
"required": self.required,
"nargs": self.nargs,
"multiple": self.multiple,
"default": self.default,
"envvar": self.envvar,
}
def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.name}>"
def _parse_decls(
self, decls: t.Sequence[str], expose_value: bool
) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]:
raise NotImplementedError()
@property @property
def human_readable_name(self): def human_readable_name(self) -> str:
"""Returns the human readable name of this parameter. This is the """Returns the human readable name of this parameter. This is the
same as the name for options, but the metavar for arguments. same as the name for options, but the metavar for arguments.
""" """
return self.name return self.name # type: ignore
def make_metavar(self): def make_metavar(self) -> str:
if self.metavar is not None: if self.metavar is not None:
return self.metavar return self.metavar
metavar = self.type.get_metavar(self) metavar = self.type.get_metavar(self)
if metavar is None: if metavar is None:
metavar = self.type.name.upper() metavar = self.type.name.upper()
if self.nargs != 1: if self.nargs != 1:
metavar += "..." metavar += "..."
return metavar return metavar
def get_default(self, ctx): @typing.overload
"""Given a context variable this calculates the default value.""" def get_default(
# Otherwise go with the regular default. self, ctx: Context, call: "te.Literal[True]" = True
if callable(self.default): ) -> t.Optional[t.Any]:
rv = self.default() ...
else:
rv = self.default
return self.type_cast_value(ctx, rv)
def add_to_parser(self, parser, ctx): @typing.overload
pass def get_default(
self, ctx: Context, call: bool = ...
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
...
def get_default(
self, ctx: Context, call: bool = True
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
"""Get the default for the parameter. Tries
:meth:`Context.lookup_default` first, then the local default.
:param ctx: Current context.
:param call: If the default is a callable, call it. Disable to
return the callable instead.
.. versionchanged:: 8.0.2
Type casting is no longer performed when getting a default.
.. versionchanged:: 8.0.1
Type casting can fail in resilient parsing mode. Invalid
defaults will not prevent showing help text.
.. versionchanged:: 8.0
Looks at ``ctx.default_map`` first.
.. versionchanged:: 8.0
Added the ``call`` parameter.
"""
value = ctx.lookup_default(self.name, call=False) # type: ignore
def consume_value(self, ctx, opts):
value = opts.get(self.name)
if value is None: if value is None:
value = self.value_from_envvar(ctx) value = self.default
if value is None:
value = ctx.lookup_default(self.name) if call and callable(value):
value = value()
return value return value
def type_cast_value(self, ctx, value): def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
"""Given a value this runs it properly through the type system. raise NotImplementedError()
This automatically handles things like `nargs` and `multiple` as
well as composite types. def consume_value(
self, ctx: Context, opts: t.Mapping[str, t.Any]
) -> t.Tuple[t.Any, ParameterSource]:
value = opts.get(self.name) # type: ignore
source = ParameterSource.COMMANDLINE
if value is None:
value = self.value_from_envvar(ctx)
source = ParameterSource.ENVIRONMENT
if value is None:
value = ctx.lookup_default(self.name) # type: ignore
source = ParameterSource.DEFAULT_MAP
if value is None:
value = self.get_default(ctx)
source = ParameterSource.DEFAULT
return value, source
def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any:
"""Convert and validate a value against the option's
:attr:`type`, :attr:`multiple`, and :attr:`nargs`.
""" """
if self.type.is_composite: if value is None:
if self.nargs <= 1: return () if self.multiple or self.nargs == -1 else None
raise TypeError(
"Attempted to invoke composite type but nargs has" def check_iter(value: t.Any) -> t.Iterator:
" been set to {}. This is not supported; nargs" try:
" needs to be set to a fixed value > 1.".format(self.nargs) 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
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 convert(value: t.Any) -> t.Tuple:
return tuple(self.type(x, self, ctx) for x in check_iter(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: if self.multiple:
return tuple(self.type(x or (), self, ctx) for x in value or ()) return tuple(convert(x) for x in check_iter(value))
return self.type(value or (), self, ctx)
def _convert(value, level): return convert(value)
if level == 0:
return self.type(value, self, ctx)
return tuple(_convert(x, level - 1) for x in value or ())
return _convert(value, (self.nargs != 1) + bool(self.multiple)) def value_is_missing(self, value: t.Any) -> bool:
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 value_is_missing(self, value):
if value is None: if value is None:
return True return True
if (self.nargs != 1 or self.multiple) and value == (): if (self.nargs != 1 or self.multiple) and value == ():
return True return True
return False return False
def full_process_value(self, ctx, value): def process_value(self, ctx: Context, value: t.Any) -> t.Any:
value = self.process_value(ctx, value) value = self.type_cast_value(ctx, value)
if value is None and not ctx.resilient_parsing:
value = self.get_default(ctx)
if self.required and self.value_is_missing(value): if self.required and self.value_is_missing(value):
raise MissingParameter(ctx=ctx, param=self) raise MissingParameter(ctx=ctx, param=self)
if self.callback is not None:
value = self.callback(ctx, self, value)
return value return value
def resolve_envvar_value(self, ctx): def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]:
if self.envvar is None: if self.envvar is None:
return return None
if isinstance(self.envvar, (tuple, list)):
for envvar in self.envvar: if isinstance(self.envvar, str):
rv = os.environ.get(envvar)
if rv is not None:
return rv
else:
rv = os.environ.get(self.envvar) rv = os.environ.get(self.envvar)
if rv != "": if rv:
return rv
else:
for envvar in self.envvar:
rv = os.environ.get(envvar)
if rv:
return rv return rv
def value_from_envvar(self, ctx): return None
rv = self.resolve_envvar_value(ctx)
def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]:
rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx)
if rv is not None and self.nargs != 1: if rv is not None and self.nargs != 1:
rv = self.type.split_envvar_value(rv) rv = self.type.split_envvar_value(rv)
return rv return rv
def handle_parse_result(self, ctx, opts, args): def handle_parse_result(
self, ctx: Context, opts: t.Mapping[str, t.Any], args: t.List[str]
) -> t.Tuple[t.Any, t.List[str]]:
with augment_usage_errors(ctx, param=self): with augment_usage_errors(ctx, param=self):
value = self.consume_value(ctx, opts) value, source = self.consume_value(ctx, opts)
ctx.set_parameter_source(self.name, source) # type: ignore
try: try:
value = self.full_process_value(ctx, value) value = self.process_value(ctx, value)
except Exception: except Exception:
if not ctx.resilient_parsing: if not ctx.resilient_parsing:
raise raise
value = None value = None
if self.callback is not None:
try:
value = invoke_param_callback(self.callback, ctx, self, value)
except Exception:
if not ctx.resilient_parsing:
raise
if self.expose_value: if self.expose_value:
ctx.params[self.name] = value ctx.params[self.name] = value # type: ignore
return value, args return value, args
def get_help_record(self, ctx): def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]:
pass pass
def get_usage_pieces(self, ctx): def get_usage_pieces(self, ctx: Context) -> t.List[str]:
return [] return []
def get_error_hint(self, ctx): def get_error_hint(self, ctx: Context) -> str:
"""Get a stringified version of the param for use in error messages to """Get a stringified version of the param for use in error messages to
indicate which param caused the error. indicate which param caused the error.
""" """
hint_list = self.opts or [self.human_readable_name] hint_list = self.opts or [self.human_readable_name]
return " / ".join(repr(x) for x in hint_list) return " / ".join(f"'{x}'" for x in hint_list)
def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]:
"""Return a list of completions for the incomplete value. If a
``shell_complete`` function was given during init, it is used.
Otherwise, the :attr:`type`
:meth:`~click.types.ParamType.shell_complete` function is used.
:param ctx: Invocation context for this command.
:param incomplete: Value being completed. May be empty.
.. versionadded:: 8.0
"""
if self._custom_shell_complete is not None:
results = self._custom_shell_complete(ctx, self, incomplete)
if results and isinstance(results[0], str):
from click.shell_completion import CompletionItem
results = [CompletionItem(c) for c in results]
return t.cast(t.List["CompletionItem"], results)
return self.type.shell_complete(ctx, self, incomplete)
class Option(Parameter): class Option(Parameter):
@ -1666,8 +2407,12 @@ class Option(Parameter):
:param prompt: if set to `True` or a non empty string then the user will be :param prompt: if set to `True` or a non empty string then the user will be
prompted for input. If set to `True` the prompt will be the prompted for input. If set to `True` the prompt will be the
option name capitalized. option name capitalized.
:param confirmation_prompt: if set then the value will need to be confirmed :param confirmation_prompt: Prompt a second time to confirm the
if it was prompted for. value if it was prompted for. Can be set to a string instead of
``True`` to customize the message.
:param prompt_required: If set to ``False``, the user will be
prompted for input only when the option was specified as a flag
without a value.
:param hide_input: if this is `True` then the input on the prompt will be :param hide_input: if this is `True` then the input on the prompt will be
hidden from the user. This is useful for password hidden from the user. This is useful for password
input. input.
@ -1687,106 +2432,146 @@ class Option(Parameter):
context. context.
:param help: the help string. :param help: the help string.
:param hidden: hide this option from help outputs. :param hidden: hide this option from help outputs.
.. versionchanged:: 8.0.1
``type`` is detected from ``flag_value`` if given.
""" """
param_type_name = "option" param_type_name = "option"
def __init__( def __init__(
self, self,
param_decls=None, param_decls: t.Optional[t.Sequence[str]] = None,
show_default=False, show_default: t.Union[bool, str] = False,
prompt=False, prompt: t.Union[bool, str] = False,
confirmation_prompt=False, confirmation_prompt: t.Union[bool, str] = False,
hide_input=False, prompt_required: bool = True,
is_flag=None, hide_input: bool = False,
flag_value=None, is_flag: t.Optional[bool] = None,
multiple=False, flag_value: t.Optional[t.Any] = None,
count=False, multiple: bool = False,
allow_from_autoenv=True, count: bool = False,
type=None, allow_from_autoenv: bool = True,
help=None, type: t.Optional[t.Union[types.ParamType, t.Any]] = None,
hidden=False, help: t.Optional[str] = None,
show_choices=True, hidden: bool = False,
show_envvar=False, show_choices: bool = True,
**attrs show_envvar: bool = False,
): **attrs: t.Any,
default_is_missing = attrs.get("default", _missing) is _missing ) -> None:
Parameter.__init__(self, param_decls, type=type, **attrs) default_is_missing = "default" not in attrs
super().__init__(param_decls, type=type, multiple=multiple, **attrs)
if prompt is True: if prompt is True:
prompt_text = self.name.replace("_", " ").capitalize() if self.name is None:
raise TypeError("'name' is required with 'prompt=True'.")
prompt_text: t.Optional[str] = self.name.replace("_", " ").capitalize()
elif prompt is False: elif prompt is False:
prompt_text = None prompt_text = None
else: else:
prompt_text = prompt prompt_text = t.cast(str, prompt)
self.prompt = prompt_text self.prompt = prompt_text
self.confirmation_prompt = confirmation_prompt self.confirmation_prompt = confirmation_prompt
self.prompt_required = prompt_required
self.hide_input = hide_input self.hide_input = hide_input
self.hidden = hidden self.hidden = hidden
# Flags # If prompt is enabled but not required, then the option can be
# used as a flag to indicate using prompt or flag_value.
self._flag_needs_value = self.prompt is not None and not self.prompt_required
if is_flag is None: if is_flag is None:
if flag_value is not None: if flag_value is not None:
# Implicitly a flag because flag_value was set.
is_flag = True is_flag = True
elif self._flag_needs_value:
# Not a flag, but when used as a flag it shows a prompt.
is_flag = False
else: else:
# Implicitly a flag because flag options were given.
is_flag = bool(self.secondary_opts) is_flag = bool(self.secondary_opts)
elif is_flag is False and not self._flag_needs_value:
# Not a flag, and prompt is not enabled, can be used as a
# flag if flag_value is set.
self._flag_needs_value = flag_value is not None
if is_flag and default_is_missing: if is_flag and default_is_missing:
self.default = False self.default: t.Union[t.Any, t.Callable[[], t.Any]] = False
if flag_value is None: if flag_value is None:
flag_value = not self.default flag_value = not self.default
self.is_flag = is_flag
self.flag_value = flag_value if is_flag and type is None:
if self.is_flag and isinstance(self.flag_value, bool) and type in [None, bool]: # Re-guess the type from the flag value instead of the
self.type = BOOL # default.
self.is_bool_flag = True self.type = types.convert_type(None, flag_value)
else:
self.is_bool_flag = False self.is_flag: bool = is_flag
self.is_bool_flag = is_flag and isinstance(self.type, types.BoolParamType)
self.flag_value: t.Any = flag_value
# Counting # Counting
self.count = count self.count = count
if count: if count:
if type is None: if type is None:
self.type = IntRange(min=0) self.type = types.IntRange(min=0)
if default_is_missing: if default_is_missing:
self.default = 0 self.default = 0
self.multiple = multiple
self.allow_from_autoenv = allow_from_autoenv self.allow_from_autoenv = allow_from_autoenv
self.help = help self.help = help
self.show_default = show_default self.show_default = show_default
self.show_choices = show_choices self.show_choices = show_choices
self.show_envvar = show_envvar self.show_envvar = show_envvar
# Sanity check for stuff we don't support
if __debug__: if __debug__:
if self.nargs < 0: if self.nargs == -1:
raise TypeError("Options cannot have nargs < 0") raise TypeError("nargs=-1 is not supported for options.")
if self.prompt and self.is_flag and not self.is_bool_flag: if self.prompt and self.is_flag and not self.is_bool_flag:
raise TypeError("Cannot prompt for flags that are not bools.") raise TypeError("'prompt' is not valid for non-boolean flag.")
if not self.is_bool_flag and self.secondary_opts: if not self.is_bool_flag and self.secondary_opts:
raise TypeError("Got secondary option for non boolean flag.") raise TypeError("Secondary flag is not valid for non-boolean flag.")
if self.is_bool_flag and self.hide_input and self.prompt is not None: if self.is_bool_flag and self.hide_input and self.prompt is not None:
raise TypeError("Hidden input does not work with boolean flag prompts.")
if self.count:
if self.multiple:
raise TypeError( raise TypeError(
"Options cannot be multiple and count at the same time." "'prompt' with 'hide_input' is not valid for boolean flag."
)
elif self.is_flag:
raise TypeError(
"Options cannot be count and flags at the same time."
) )
def _parse_decls(self, decls, expose_value): if self.count:
if self.multiple:
raise TypeError("'count' is not valid with 'multiple'.")
if self.is_flag:
raise TypeError("'count' is not valid with 'is_flag'.")
def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
info_dict.update(
help=self.help,
prompt=self.prompt,
is_flag=self.is_flag,
flag_value=self.flag_value,
count=self.count,
hidden=self.hidden,
)
return info_dict
def _parse_decls(
self, decls: t.Sequence[str], expose_value: bool
) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]:
opts = [] opts = []
secondary_opts = [] secondary_opts = []
name = None name = None
possible_names = [] possible_names = []
for decl in decls: for decl in decls:
if isidentifier(decl): if decl.isidentifier():
if name is not None: if name is not None:
raise TypeError("Name defined twice") raise TypeError(f"Name '{name}' defined twice")
name = decl name = decl
else: else:
split_char = ";" if decl[:1] == "/" else "/" split_char = ";" if decl[:1] == "/" else "/"
@ -1799,6 +2584,11 @@ class Option(Parameter):
second = second.lstrip() second = second.lstrip()
if second: if second:
secondary_opts.append(second.lstrip()) secondary_opts.append(second.lstrip())
if first == second:
raise ValueError(
f"Boolean option {decl!r} cannot use the"
" same flag for true/false."
)
else: else:
possible_names.append(split_opt(decl)) possible_names.append(split_opt(decl))
opts.append(decl) opts.append(decl)
@ -1806,7 +2596,7 @@ class Option(Parameter):
if name is None and possible_names: if name is None and possible_names:
possible_names.sort(key=lambda x: -len(x[0])) # group long options first possible_names.sort(key=lambda x: -len(x[0])) # group long options first
name = possible_names[0][1].replace("-", "_").lower() name = possible_names[0][1].replace("-", "_").lower()
if not isidentifier(name): if not name.isidentifier():
name = None name = None
if name is None: if name is None:
@ -1816,19 +2606,14 @@ class Option(Parameter):
if not opts and not secondary_opts: if not opts and not secondary_opts:
raise TypeError( raise TypeError(
"No options defined but a name was passed ({}). Did you" f"No options defined but a name was passed ({name})."
" mean to declare an argument instead of an option?".format(name) " Did you mean to declare an argument instead? Did"
f" you mean to pass '--{name}'?"
) )
return name, opts, secondary_opts return name, opts, secondary_opts
def add_to_parser(self, parser, ctx): def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
kwargs = {
"dest": self.name,
"nargs": self.nargs,
"obj": self,
}
if self.multiple: if self.multiple:
action = "append" action = "append"
elif self.count: elif self.count:
@ -1837,74 +2622,150 @@ class Option(Parameter):
action = "store" action = "store"
if self.is_flag: if self.is_flag:
kwargs.pop("nargs", None) action = f"{action}_const"
action_const = "{}_const".format(action)
if self.is_bool_flag and self.secondary_opts: if self.is_bool_flag and self.secondary_opts:
parser.add_option(self.opts, action=action_const, const=True, **kwargs)
parser.add_option( parser.add_option(
self.secondary_opts, action=action_const, const=False, **kwargs obj=self, opts=self.opts, dest=self.name, action=action, const=True
)
parser.add_option(
obj=self,
opts=self.secondary_opts,
dest=self.name,
action=action,
const=False,
) )
else: else:
parser.add_option( parser.add_option(
self.opts, action=action_const, const=self.flag_value, **kwargs obj=self,
opts=self.opts,
dest=self.name,
action=action,
const=self.flag_value,
) )
else: else:
kwargs["action"] = action parser.add_option(
parser.add_option(self.opts, **kwargs) obj=self,
opts=self.opts,
dest=self.name,
action=action,
nargs=self.nargs,
)
def get_help_record(self, ctx): def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]:
if self.hidden: if self.hidden:
return return None
any_prefix_is_slash = []
any_prefix_is_slash = False
def _write_opts(opts: t.Sequence[str]) -> str:
nonlocal any_prefix_is_slash
def _write_opts(opts):
rv, any_slashes = join_options(opts) rv, any_slashes = join_options(opts)
if any_slashes: if any_slashes:
any_prefix_is_slash[:] = [True] any_prefix_is_slash = True
if not self.is_flag and not self.count: if not self.is_flag and not self.count:
rv += " {}".format(self.make_metavar()) rv += f" {self.make_metavar()}"
return rv return rv
rv = [_write_opts(self.opts)] rv = [_write_opts(self.opts)]
if self.secondary_opts: if self.secondary_opts:
rv.append(_write_opts(self.secondary_opts)) rv.append(_write_opts(self.secondary_opts))
help = self.help or "" help = self.help or ""
extra = [] extra = []
if self.show_envvar: if self.show_envvar:
envvar = self.envvar envvar = self.envvar
if envvar is None: if envvar is None:
if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None: if (
envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper()) self.allow_from_autoenv
and ctx.auto_envvar_prefix is not None
and self.name is not None
):
envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"
if envvar is not None: if envvar is not None:
extra.append( var_str = (
"env var: {}".format( envvar
", ".join(str(d) for d in envvar) if isinstance(envvar, str)
if isinstance(envvar, (list, tuple)) else ", ".join(str(d) for d in envvar)
else envvar
) )
) extra.append(_("env var: {var}").format(var=var_str))
if self.default is not None and (self.show_default or ctx.show_default):
if isinstance(self.show_default, string_types): # Temporarily enable resilient parsing to avoid type casting
default_string = "({})".format(self.show_default) # failing for the default. Might be possible to extend this to
elif isinstance(self.default, (list, tuple)): # help formatting in general.
default_string = ", ".join(str(d) for d in self.default) resilient = ctx.resilient_parsing
elif inspect.isfunction(self.default): ctx.resilient_parsing = True
default_string = "(dynamic)"
try:
default_value = self.get_default(ctx, call=False)
finally:
ctx.resilient_parsing = resilient
show_default_is_str = isinstance(self.show_default, str)
if show_default_is_str or (
default_value is not None and (self.show_default or ctx.show_default)
):
if show_default_is_str:
default_string = f"({self.show_default})"
elif isinstance(default_value, (list, tuple)):
default_string = ", ".join(str(d) for d in default_value)
elif callable(default_value):
default_string = _("(dynamic)")
elif self.is_bool_flag and self.secondary_opts:
# For boolean flags that have distinct True/False opts,
# use the opt without prefix instead of the value.
default_string = split_opt(
(self.opts if self.default else self.secondary_opts)[0]
)[1]
else: else:
default_string = self.default default_string = str(default_value)
extra.append("default: {}".format(default_string))
if default_string:
extra.append(_("default: {default}").format(default=default_string))
if (
isinstance(self.type, types._NumberRangeBase)
# skip count with default range type
and not (self.count and self.type.min == 0 and self.type.max is None)
):
range_str = self.type._describe_range()
if range_str:
extra.append(range_str)
if self.required: if self.required:
extra.append("required") extra.append(_("required"))
if extra: if extra:
help = "{}[{}]".format( extra_str = "; ".join(extra)
"{} ".format(help) if help else "", "; ".join(extra) help = f"{help} [{extra_str}]" if help else f"[{extra_str}]"
)
return ("; " if any_prefix_is_slash else " / ").join(rv), help return ("; " if any_prefix_is_slash else " / ").join(rv), help
def get_default(self, ctx): @typing.overload
def get_default(
self, ctx: Context, call: "te.Literal[True]" = True
) -> t.Optional[t.Any]:
...
@typing.overload
def get_default(
self, ctx: Context, call: bool = ...
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
...
def get_default(
self, ctx: Context, call: bool = True
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
# If we're a non boolean flag our default is more complex because # If we're a non boolean flag our default is more complex because
# we need to look at all flags in the same group to figure out # we need to look at all flags in the same group to figure out
# if we're the the default one in which case we return the flag # if we're the the default one in which case we return the flag
@ -1912,16 +2773,20 @@ class Option(Parameter):
if self.is_flag and not self.is_bool_flag: if self.is_flag and not self.is_bool_flag:
for param in ctx.command.params: for param in ctx.command.params:
if param.name == self.name and param.default: if param.name == self.name and param.default:
return param.flag_value return param.flag_value # type: ignore
return None
return Parameter.get_default(self, ctx)
def prompt_for_value(self, ctx): return None
return super().get_default(ctx, call=call)
def prompt_for_value(self, ctx: Context) -> t.Any:
"""This is an alternative flow that can be activated in the full """This is an alternative flow that can be activated in the full
value processing if a value does not exist. It will prompt the value processing if a value does not exist. It will prompt the
user until a valid value exists and then returns the processed user until a valid value exists and then returns the processed
value as result. value as result.
""" """
assert self.prompt is not None
# Calculate the default before prompting anything to be stable. # Calculate the default before prompting anything to be stable.
default = self.get_default(ctx) default = self.get_default(ctx)
@ -1940,29 +2805,74 @@ class Option(Parameter):
value_proc=lambda x: self.process_value(ctx, x), value_proc=lambda x: self.process_value(ctx, x),
) )
def resolve_envvar_value(self, ctx): def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]:
rv = Parameter.resolve_envvar_value(self, ctx) rv = super().resolve_envvar_value(ctx)
if rv is not None: if rv is not None:
return rv return rv
if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None:
envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper())
return os.environ.get(envvar)
def value_from_envvar(self, ctx): if (
rv = self.resolve_envvar_value(ctx) self.allow_from_autoenv
if rv is None: and ctx.auto_envvar_prefix is not None
return None and self.name is not None
value_depth = (self.nargs != 1) + bool(self.multiple) ):
if value_depth > 0 and rv is not None: envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"
rv = self.type.split_envvar_value(rv) rv = os.environ.get(envvar)
if self.multiple and self.nargs != 1:
rv = batch(rv, self.nargs)
return rv return rv
def full_process_value(self, ctx, value): def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]:
if value is None and self.prompt is not None and not ctx.resilient_parsing: rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx)
return self.prompt_for_value(ctx)
return Parameter.full_process_value(self, ctx, value) if rv is None:
return None
value_depth = (self.nargs != 1) + bool(self.multiple)
if value_depth > 0:
rv = self.type.split_envvar_value(rv)
if self.multiple and self.nargs != 1:
rv = batch(rv, self.nargs)
return rv
def consume_value(
self, ctx: Context, opts: t.Mapping[str, "Parameter"]
) -> t.Tuple[t.Any, ParameterSource]:
value, source = super().consume_value(ctx, opts)
# The parser will emit a sentinel value if the option can be
# given as a flag without a value. This is different from None
# to distinguish from the flag not being given at all.
if value is _flag_needs_value:
if self.prompt is not None and not ctx.resilient_parsing:
value = self.prompt_for_value(ctx)
source = ParameterSource.PROMPT
else:
value = self.flag_value
source = ParameterSource.COMMANDLINE
elif (
self.multiple
and value is not None
and any(v is _flag_needs_value for v in value)
):
value = [self.flag_value if v is _flag_needs_value else v for v in value]
source = ParameterSource.COMMANDLINE
# The value wasn't set, or used the param's default, prompt if
# prompting is enabled.
elif (
source in {None, ParameterSource.DEFAULT}
and self.prompt is not None
and (self.required or self.prompt_required)
and not ctx.resilient_parsing
):
value = self.prompt_for_value(ctx)
source = ParameterSource.PROMPT
return value, source
class Argument(Parameter): class Argument(Parameter):
@ -1975,37 +2885,48 @@ class Argument(Parameter):
param_type_name = "argument" param_type_name = "argument"
def __init__(self, param_decls, required=None, **attrs): def __init__(
self,
param_decls: t.Sequence[str],
required: t.Optional[bool] = None,
**attrs: t.Any,
) -> None:
if required is None: if required is None:
if attrs.get("default") is not None: if attrs.get("default") is not None:
required = False required = False
else: else:
required = attrs.get("nargs", 1) > 0 required = attrs.get("nargs", 1) > 0
Parameter.__init__(self, param_decls, required=required, **attrs)
if self.default is not None and self.nargs < 0: if "multiple" in attrs:
raise TypeError( raise TypeError("__init__() got an unexpected keyword argument 'multiple'.")
"nargs=-1 in combination with a default value is not supported."
) super().__init__(param_decls, required=required, **attrs)
if __debug__:
if self.default is not None and self.nargs == -1:
raise TypeError("'default' is not supported for nargs=-1.")
@property @property
def human_readable_name(self): def human_readable_name(self) -> str:
if self.metavar is not None: if self.metavar is not None:
return self.metavar return self.metavar
return self.name.upper() return self.name.upper() # type: ignore
def make_metavar(self): def make_metavar(self) -> str:
if self.metavar is not None: if self.metavar is not None:
return self.metavar return self.metavar
var = self.type.get_metavar(self) var = self.type.get_metavar(self)
if not var: if not var:
var = self.name.upper() var = self.name.upper() # type: ignore
if not self.required: if not self.required:
var = "[{}]".format(var) var = f"[{var}]"
if self.nargs != 1: if self.nargs != 1:
var += "..." var += "..."
return var return var
def _parse_decls(self, decls, expose_value): def _parse_decls(
self, decls: t.Sequence[str], expose_value: bool
) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]:
if not decls: if not decls:
if not expose_value: if not expose_value:
return None, [], [] return None, [], []
@ -2016,15 +2937,15 @@ class Argument(Parameter):
else: else:
raise TypeError( raise TypeError(
"Arguments take exactly one parameter declaration, got" "Arguments take exactly one parameter declaration, got"
" {}".format(len(decls)) f" {len(decls)}."
) )
return name, [arg], [] return name, [arg], []
def get_usage_pieces(self, ctx): def get_usage_pieces(self, ctx: Context) -> t.List[str]:
return [self.make_metavar()] return [self.make_metavar()]
def get_error_hint(self, ctx): def get_error_hint(self, ctx: Context) -> str:
return repr(self.make_metavar()) return f"'{self.make_metavar()}'"
def add_to_parser(self, parser, ctx): def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)

View file

@ -1,41 +1,48 @@
import inspect import inspect
import sys import types
import typing as t
from functools import update_wrapper from functools import update_wrapper
from gettext import gettext as _
from ._compat import iteritems
from ._unicodefun import _check_for_unicode_literals
from .core import Argument from .core import Argument
from .core import Command from .core import Command
from .core import Context
from .core import Group from .core import Group
from .core import Option from .core import Option
from .core import Parameter
from .globals import get_current_context from .globals import get_current_context
from .utils import echo from .utils import echo
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
FC = t.TypeVar("FC", t.Callable[..., t.Any], Command)
def pass_context(f):
def pass_context(f: F) -> F:
"""Marks a callback as wanting to receive the current context """Marks a callback as wanting to receive the current context
object as first argument. object as first argument.
""" """
def new_func(*args, **kwargs): def new_func(*args, **kwargs): # type: ignore
return f(get_current_context(), *args, **kwargs) return f(get_current_context(), *args, **kwargs)
return update_wrapper(new_func, f) return update_wrapper(t.cast(F, new_func), f)
def pass_obj(f): def pass_obj(f: F) -> F:
"""Similar to :func:`pass_context`, but only pass the object on the """Similar to :func:`pass_context`, but only pass the object on the
context onwards (:attr:`Context.obj`). This is useful if that object context onwards (:attr:`Context.obj`). This is useful if that object
represents the state of a nested system. represents the state of a nested system.
""" """
def new_func(*args, **kwargs): def new_func(*args, **kwargs): # type: ignore
return f(get_current_context().obj, *args, **kwargs) return f(get_current_context().obj, *args, **kwargs)
return update_wrapper(new_func, f) return update_wrapper(t.cast(F, new_func), f)
def make_pass_decorator(object_type, ensure=False): def make_pass_decorator(
object_type: t.Type, ensure: bool = False
) -> "t.Callable[[F], F]":
"""Given an object type this creates a decorator that will work """Given an object type this creates a decorator that will work
similar to :func:`pass_obj` but instead of passing the object of the similar to :func:`pass_obj` but instead of passing the object of the
current context, it will find the innermost context of type current context, it will find the innermost context of type
@ -58,52 +65,99 @@ def make_pass_decorator(object_type, ensure=False):
remembered on the context if it's not there yet. remembered on the context if it's not there yet.
""" """
def decorator(f): def decorator(f: F) -> F:
def new_func(*args, **kwargs): def new_func(*args, **kwargs): # type: ignore
ctx = get_current_context() ctx = get_current_context()
if ensure: if ensure:
obj = ctx.ensure_object(object_type) obj = ctx.ensure_object(object_type)
else: else:
obj = ctx.find_object(object_type) obj = ctx.find_object(object_type)
if obj is None: if obj is None:
raise RuntimeError( raise RuntimeError(
"Managed to invoke callback without a context" "Managed to invoke callback without a context"
" object of type '{}' existing".format(object_type.__name__) f" object of type {object_type.__name__!r}"
" existing."
) )
return ctx.invoke(f, obj, *args, **kwargs) return ctx.invoke(f, obj, *args, **kwargs)
return update_wrapper(new_func, f) return update_wrapper(t.cast(F, new_func), f)
return decorator return decorator
def _make_command(f, name, attrs, cls): def pass_meta_key(
key: str, *, doc_description: t.Optional[str] = None
) -> "t.Callable[[F], F]":
"""Create a decorator that passes a key from
:attr:`click.Context.meta` as the first argument to the decorated
function.
:param key: Key in ``Context.meta`` to pass.
:param doc_description: Description of the object being passed,
inserted into the decorator's docstring. Defaults to "the 'key'
key from Context.meta".
.. versionadded:: 8.0
"""
def decorator(f: F) -> F:
def new_func(*args, **kwargs): # type: ignore
ctx = get_current_context()
obj = ctx.meta[key]
return ctx.invoke(f, obj, *args, **kwargs)
return update_wrapper(t.cast(F, new_func), f)
if doc_description is None:
doc_description = f"the {key!r} key from :attr:`click.Context.meta`"
decorator.__doc__ = (
f"Decorator that passes {doc_description} as the first argument"
" to the decorated function."
)
return decorator
def _make_command(
f: F,
name: t.Optional[str],
attrs: t.MutableMapping[str, t.Any],
cls: t.Type[Command],
) -> Command:
if isinstance(f, Command): if isinstance(f, Command):
raise TypeError("Attempted to convert a callback into a command twice.") raise TypeError("Attempted to convert a callback into a command twice.")
try: try:
params = f.__click_params__ params = f.__click_params__ # type: ignore
params.reverse() params.reverse()
del f.__click_params__ del f.__click_params__ # type: ignore
except AttributeError: except AttributeError:
params = [] params = []
help = attrs.get("help") help = attrs.get("help")
if help is None: if help is None:
help = inspect.getdoc(f) help = inspect.getdoc(f)
if isinstance(help, bytes):
help = help.decode("utf-8")
else: else:
help = inspect.cleandoc(help) help = inspect.cleandoc(help)
attrs["help"] = help attrs["help"] = help
_check_for_unicode_literals()
return cls( return cls(
name=name or f.__name__.lower().replace("_", "-"), name=name or f.__name__.lower().replace("_", "-"),
callback=f, callback=f,
params=params, params=params,
**attrs **attrs,
) )
def command(name=None, cls=None, **attrs): def command(
name: t.Optional[str] = None,
cls: t.Optional[t.Type[Command]] = None,
**attrs: t.Any,
) -> t.Callable[[F], Command]:
r"""Creates a new :class:`Command` and uses the decorated function as r"""Creates a new :class:`Command` and uses the decorated function as
callback. This will also automatically attach all decorated callback. This will also automatically attach all decorated
:func:`option`\s and :func:`argument`\s as parameters to the command. :func:`option`\s and :func:`argument`\s as parameters to the command.
@ -126,33 +180,34 @@ def command(name=None, cls=None, **attrs):
if cls is None: if cls is None:
cls = Command cls = Command
def decorator(f): def decorator(f: t.Callable[..., t.Any]) -> Command:
cmd = _make_command(f, name, attrs, cls) cmd = _make_command(f, name, attrs, cls) # type: ignore
cmd.__doc__ = f.__doc__ cmd.__doc__ = f.__doc__
return cmd return cmd
return decorator return decorator
def group(name=None, **attrs): def group(name: t.Optional[str] = None, **attrs: t.Any) -> t.Callable[[F], Group]:
"""Creates a new :class:`Group` with a function as callback. This """Creates a new :class:`Group` with a function as callback. This
works otherwise the same as :func:`command` just that the `cls` works otherwise the same as :func:`command` just that the `cls`
parameter is set to :class:`Group`. parameter is set to :class:`Group`.
""" """
attrs.setdefault("cls", Group) attrs.setdefault("cls", Group)
return command(name, **attrs) return t.cast(Group, command(name, **attrs))
def _param_memo(f, param): def _param_memo(f: FC, param: Parameter) -> None:
if isinstance(f, Command): if isinstance(f, Command):
f.params.append(param) f.params.append(param)
else: else:
if not hasattr(f, "__click_params__"): if not hasattr(f, "__click_params__"):
f.__click_params__ = [] f.__click_params__ = [] # type: ignore
f.__click_params__.append(param)
f.__click_params__.append(param) # type: ignore
def argument(*param_decls, **attrs): def argument(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
"""Attaches an argument to the command. All positional arguments are """Attaches an argument to the command. All positional arguments are
passed as parameter declarations to :class:`Argument`; all keyword passed as parameter declarations to :class:`Argument`; all keyword
arguments are forwarded unchanged (except ``cls``). arguments are forwarded unchanged (except ``cls``).
@ -163,7 +218,7 @@ def argument(*param_decls, **attrs):
:class:`Argument`. :class:`Argument`.
""" """
def decorator(f): def decorator(f: FC) -> FC:
ArgumentClass = attrs.pop("cls", Argument) ArgumentClass = attrs.pop("cls", Argument)
_param_memo(f, ArgumentClass(param_decls, **attrs)) _param_memo(f, ArgumentClass(param_decls, **attrs))
return f return f
@ -171,7 +226,7 @@ def argument(*param_decls, **attrs):
return decorator return decorator
def option(*param_decls, **attrs): def option(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
"""Attaches an option to the command. All positional arguments are """Attaches an option to the command. All positional arguments are
passed as parameter declarations to :class:`Option`; all keyword passed as parameter declarations to :class:`Option`; all keyword
arguments are forwarded unchanged (except ``cls``). arguments are forwarded unchanged (except ``cls``).
@ -182,7 +237,7 @@ def option(*param_decls, **attrs):
:class:`Option`. :class:`Option`.
""" """
def decorator(f): def decorator(f: FC) -> FC:
# Issue 926, copy attrs, so pre-defined options can re-use the same cls= # Issue 926, copy attrs, so pre-defined options can re-use the same cls=
option_attrs = attrs.copy() option_attrs = attrs.copy()
@ -195,139 +250,187 @@ def option(*param_decls, **attrs):
return decorator return decorator
def confirmation_option(*param_decls, **attrs): def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
"""Shortcut for confirmation prompts that can be ignored by passing """Add a ``--yes`` option which shows a prompt before continuing if
``--yes`` as parameter. not passed. If the prompt is declined, the program will exit.
This is equivalent to decorating a function with :func:`option` with :param param_decls: One or more option names. Defaults to the single
the following parameters:: value ``"--yes"``.
:param kwargs: Extra arguments are passed to :func:`option`.
"""
def callback(ctx, param, value): def callback(ctx: Context, param: Parameter, value: bool) -> None:
if not value: if not value:
ctx.abort() ctx.abort()
@click.command() if not param_decls:
@click.option('--yes', is_flag=True, callback=callback, param_decls = ("--yes",)
expose_value=False, prompt='Do you want to continue?')
def dropdb(): kwargs.setdefault("is_flag", True)
pass 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: 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.
: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",)
def decorator(f): kwargs.setdefault("prompt", True)
def callback(ctx, param, value): kwargs.setdefault("confirmation_prompt", True)
if not value: kwargs.setdefault("hide_input", True)
ctx.abort() return option(*param_decls, **kwargs)
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)
return decorator
def password_option(*param_decls, **attrs): def version_option(
"""Shortcut for password prompts. 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.
This is equivalent to decorating a function with :func:`option` with If ``version`` is not provided, Click will try to detect it using
the following parameters:: :func:`importlib.metadata.version` to get the version for the
``package_name``. On Python < 3.8, the ``importlib_metadata``
backport must be installed.
@click.command() If ``package_name`` is not provided, Click will try to detect it by
@click.option('--password', prompt=True, confirmation_prompt=True, inspecting the stack frames. This will be used to detect the
hide_input=True) version, so it must match the name of the installed package.
def changeadmin(password):
pass :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")
def decorator(f): if version is None and package_name is None:
attrs.setdefault("prompt", True) frame = inspect.currentframe()
attrs.setdefault("confirmation_prompt", True) f_back = frame.f_back if frame is not None else None
attrs.setdefault("hide_input", True) f_globals = f_back.f_globals if f_back is not None else None
return option(*(param_decls or ("--password",)), **attrs)(f) # break reference cycle
# https://docs.python.org/3/library/inspect.html#the-interpreter-stack
del frame
return decorator if f_globals is not None:
package_name = f_globals.get("__name__")
if package_name == "__main__":
package_name = f_globals.get("__package__")
def version_option(version=None, *param_decls, **attrs): if package_name:
"""Adds a ``--version`` option which immediately ends the program package_name = package_name.partition(".")[0]
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 def callback(ctx: Context, param: Parameter, value: bool) -> None:
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: if not value or ctx.resilient_parsing:
return return
prog = prog_name
if prog is None: nonlocal prog_name
prog = ctx.find_root().info_name nonlocal version
ver = version
if ver is None: 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: try:
import pkg_resources from importlib import metadata # type: ignore
except ImportError: except ImportError:
pass # Python < 3.8
else: import importlib_metadata as metadata # type: ignore
for dist in pkg_resources.working_set:
scripts = dist.get_entry_map().get("console_scripts") or {} try:
for _, entry_point in iteritems(scripts): version = metadata.version(package_name) # type: ignore
if entry_point.module_name == module: except metadata.PackageNotFoundError: # type: ignore
ver = dist.version raise RuntimeError(
break f"{package_name!r} is not installed. Try passing"
if ver is None: " 'package_name' instead."
raise RuntimeError("Could not determine version") ) from None
echo(message % {"prog": prog, "version": ver}, color=ctx.color)
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() ctx.exit()
attrs.setdefault("is_flag", True) if not param_decls:
attrs.setdefault("expose_value", False) param_decls = ("--version",)
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 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, **attrs): def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
"""Adds a ``--help`` option which immediately ends the program """Add a ``--help`` option which immediately prints the help page
printing out the help page. This is usually unnecessary to add as and exits the program.
this is added by default to all commands unless suppressed.
Like :func:`version_option`, this is implemented as eager option that This is usually unnecessary, as the ``--help`` option is added to
prints in the callback and exits. each command automatically unless ``add_help_option=False`` is
passed.
All arguments are forwarded to :func:`option`. :param param_decls: One or more option names. Defaults to the single
value ``"--help"``.
:param kwargs: Extra arguments are passed to :func:`option`.
""" """
def decorator(f): def callback(ctx: Context, param: Parameter, value: bool) -> None:
def callback(ctx, param, value): if not value or ctx.resilient_parsing:
if value and not ctx.resilient_parsing: return
echo(ctx.get_help(), color=ctx.color) echo(ctx.get_help(), color=ctx.color)
ctx.exit() ctx.exit()
attrs.setdefault("is_flag", True) if not param_decls:
attrs.setdefault("expose_value", False) param_decls = ("--help",)
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 get_text_stderr
from ._compat import PY2
from .utils import echo from .utils import echo
if t.TYPE_CHECKING:
from .core import Context
from .core import Parameter
def _join_param_hints(param_hint):
if isinstance(param_hint, (tuple, list)): def _join_param_hints(
param_hint: t.Optional[t.Union[t.Sequence[str], str]]
) -> t.Optional[str]:
if param_hint is not None and not isinstance(param_hint, str):
return " / ".join(repr(x) for x in param_hint) return " / ".join(repr(x) for x in param_hint)
return param_hint return param_hint
class ClickException(Exception): class ClickException(Exception):
"""An exception that Click can handle and show to the user.""" """An exception that Click can handle and show to the user."""
#: The exit code for this exception #: The exit code for this exception.
exit_code = 1 exit_code = 1
def __init__(self, message): def __init__(self, message: str) -> None:
ctor_msg = message super().__init__(message)
if PY2:
if ctor_msg is not None:
ctor_msg = ctor_msg.encode("utf-8")
Exception.__init__(self, ctor_msg)
self.message = message self.message = message
def format_message(self): def format_message(self) -> str:
return self.message return self.message
def __str__(self): def __str__(self) -> str:
return self.message return self.message
if PY2: def show(self, file: t.Optional[t.IO] = None) -> None:
__unicode__ = __str__
def __str__(self):
return self.message.encode("utf-8")
def show(self, file=None):
if file is None: if file is None:
file = get_text_stderr() file = get_text_stderr()
echo("Error: {}".format(self.format_message()), file=file)
echo(_("Error: {message}").format(message=self.format_message()), file=file)
class UsageError(ClickException): class UsageError(ClickException):
@ -53,24 +54,32 @@ class UsageError(ClickException):
exit_code = 2 exit_code = 2
def __init__(self, message, ctx=None): def __init__(self, message: str, ctx: t.Optional["Context"] = None) -> None:
ClickException.__init__(self, message) super().__init__(message)
self.ctx = ctx self.ctx = ctx
self.cmd = self.ctx.command if self.ctx else None self.cmd = self.ctx.command if self.ctx else None
def show(self, file=None): def show(self, file: t.Optional[t.IO] = None) -> None:
if file is None: if file is None:
file = get_text_stderr() file = get_text_stderr()
color = None color = None
hint = "" hint = ""
if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None: if (
hint = "Try '{} {}' for help.\n".format( self.ctx is not None
self.ctx.command_path, self.ctx.help_option_names[0] and self.ctx.command.get_help_option(self.ctx) is not None
):
hint = _("Try '{command} {option}' for help.").format(
command=self.ctx.command_path, option=self.ctx.help_option_names[0]
) )
hint = f"{hint}\n"
if self.ctx is not None: if self.ctx is not None:
color = self.ctx.color color = self.ctx.color
echo("{}\n{}".format(self.ctx.get_usage(), hint), file=file, color=color) echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color)
echo("Error: {}".format(self.format_message()), file=file, color=color) echo(
_("Error: {message}").format(message=self.format_message()),
file=file,
color=color,
)
class BadParameter(UsageError): class BadParameter(UsageError):
@ -91,21 +100,28 @@ class BadParameter(UsageError):
each item is quoted and separated. each item is quoted and separated.
""" """
def __init__(self, message, ctx=None, param=None, param_hint=None): def __init__(
UsageError.__init__(self, message, ctx) self,
message: str,
ctx: t.Optional["Context"] = None,
param: t.Optional["Parameter"] = None,
param_hint: t.Optional[str] = None,
) -> None:
super().__init__(message, ctx)
self.param = param self.param = param
self.param_hint = param_hint self.param_hint = param_hint
def format_message(self): def format_message(self) -> str:
if self.param_hint is not None: if self.param_hint is not None:
param_hint = self.param_hint param_hint = self.param_hint
elif self.param is not None: elif self.param is not None:
param_hint = self.param.get_error_hint(self.ctx) param_hint = self.param.get_error_hint(self.ctx) # type: ignore
else: else:
return "Invalid value: {}".format(self.message) return _("Invalid value: {message}").format(message=self.message)
param_hint = _join_param_hints(param_hint)
return "Invalid value for {}: {}".format(param_hint, self.message) return _("Invalid value for {param_hint}: {message}").format(
param_hint=_join_param_hints(param_hint), message=self.message
)
class MissingParameter(BadParameter): class MissingParameter(BadParameter):
@ -121,19 +137,26 @@ class MissingParameter(BadParameter):
""" """
def __init__( def __init__(
self, message=None, ctx=None, param=None, param_hint=None, param_type=None self,
): message: t.Optional[str] = None,
BadParameter.__init__(self, message, ctx, param, param_hint) ctx: t.Optional["Context"] = None,
param: t.Optional["Parameter"] = None,
param_hint: t.Optional[str] = None,
param_type: t.Optional[str] = None,
) -> None:
super().__init__(message or "", ctx, param, param_hint)
self.param_type = param_type self.param_type = param_type
def format_message(self): def format_message(self) -> str:
if self.param_hint is not None: if self.param_hint is not None:
param_hint = self.param_hint param_hint: t.Optional[str] = self.param_hint
elif self.param is not None: elif self.param is not None:
param_hint = self.param.get_error_hint(self.ctx) param_hint = self.param.get_error_hint(self.ctx) # type: ignore
else: else:
param_hint = None param_hint = None
param_hint = _join_param_hints(param_hint) param_hint = _join_param_hints(param_hint)
param_hint = f" {param_hint}" if param_hint else ""
param_type = self.param_type param_type = self.param_type
if param_type is None and self.param is not None: if param_type is None and self.param is not None:
@ -144,30 +167,31 @@ class MissingParameter(BadParameter):
msg_extra = self.param.type.get_missing_message(self.param) msg_extra = self.param.type.get_missing_message(self.param)
if msg_extra: if msg_extra:
if msg: if msg:
msg += ". {}".format(msg_extra) msg += f". {msg_extra}"
else: else:
msg = msg_extra msg = msg_extra
return "Missing {}{}{}{}".format( msg = f" {msg}" if msg else ""
param_type,
" {}".format(param_hint) if param_hint else "",
". " if msg else ".",
msg or "",
)
def __str__(self): # Translate param_type for known types.
if self.message is None: if param_type == "argument":
missing = _("Missing argument")
elif param_type == "option":
missing = _("Missing option")
elif param_type == "parameter":
missing = _("Missing parameter")
else:
missing = _("Missing {param_type}").format(param_type=param_type)
return f"{missing}{param_hint}.{msg}"
def __str__(self) -> str:
if not self.message:
param_name = self.param.name if self.param else None param_name = self.param.name if self.param else None
return "missing parameter: {}".format(param_name) return _("Missing parameter: {param_name}").format(param_name=param_name)
else: else:
return self.message return self.message
if PY2:
__unicode__ = __str__
def __str__(self):
return self.__unicode__().encode("utf-8")
class NoSuchOption(UsageError): class NoSuchOption(UsageError):
"""Raised if click attempted to handle an option that does not """Raised if click attempted to handle an option that does not
@ -176,22 +200,31 @@ class NoSuchOption(UsageError):
.. versionadded:: 4.0 .. versionadded:: 4.0
""" """
def __init__(self, option_name, message=None, possibilities=None, ctx=None): def __init__(
self,
option_name: str,
message: t.Optional[str] = None,
possibilities: t.Optional[t.Sequence[str]] = None,
ctx: t.Optional["Context"] = None,
) -> None:
if message is None: if message is None:
message = "no such option: {}".format(option_name) message = _("No such option: {name}").format(name=option_name)
UsageError.__init__(self, message, ctx)
super().__init__(message, ctx)
self.option_name = option_name self.option_name = option_name
self.possibilities = possibilities self.possibilities = possibilities
def format_message(self): def format_message(self) -> str:
bits = [self.message] if not self.possibilities:
if self.possibilities: return self.message
if len(self.possibilities) == 1:
bits.append("Did you mean {}?".format(self.possibilities[0])) possibility_str = ", ".join(sorted(self.possibilities))
else: suggest = ngettext(
possibilities = sorted(self.possibilities) "Did you mean {possibility}?",
bits.append("(Possible options: {})".format(", ".join(possibilities))) "(Possible options: {possibilities})",
return " ".join(bits) len(self.possibilities),
).format(possibility=possibility_str, possibilities=possibility_str)
return f"{self.message} {suggest}"
class BadOptionUsage(UsageError): class BadOptionUsage(UsageError):
@ -204,8 +237,10 @@ class BadOptionUsage(UsageError):
:param option_name: the name of the option being used incorrectly. :param option_name: the name of the option being used incorrectly.
""" """
def __init__(self, option_name, message, ctx=None): def __init__(
UsageError.__init__(self, message, ctx) self, option_name: str, message: str, ctx: t.Optional["Context"] = None
) -> None:
super().__init__(message, ctx)
self.option_name = option_name self.option_name = option_name
@ -217,23 +252,22 @@ class BadArgumentUsage(UsageError):
.. versionadded:: 6.0 .. versionadded:: 6.0
""" """
def __init__(self, message, ctx=None):
UsageError.__init__(self, message, ctx)
class FileError(ClickException): class FileError(ClickException):
"""Raised if a file cannot be opened.""" """Raised if a file cannot be opened."""
def __init__(self, filename, hint=None): def __init__(self, filename: str, hint: t.Optional[str] = None) -> None:
ui_filename = filename_to_ui(filename)
if hint is None: if hint is None:
hint = "unknown error" hint = _("unknown error")
ClickException.__init__(self, hint)
self.ui_filename = ui_filename super().__init__(hint)
self.ui_filename = os.fsdecode(filename)
self.filename = filename self.filename = filename
def format_message(self): def format_message(self) -> str:
return "Could not open file {}: {}".format(self.ui_filename, self.message) return _("Could not open file {filename!r}: {message}").format(
filename=self.ui_filename, message=self.message
)
class Abort(RuntimeError): class Abort(RuntimeError):
@ -249,5 +283,5 @@ class Exit(RuntimeError):
__slots__ = ("exit_code",) __slots__ = ("exit_code",)
def __init__(self, code=0): def __init__(self, code: int = 0) -> None:
self.exit_code = code self.exit_code = code

View file

@ -1,30 +1,38 @@
import typing as t
from contextlib import contextmanager from contextlib import contextmanager
from gettext import gettext as _
from ._compat import term_len from ._compat import term_len
from .parser import split_opt from .parser import split_opt
from .termui import get_terminal_size
# Can force a width. This is used by the test system # Can force a width. This is used by the test system
FORCED_WIDTH = None FORCED_WIDTH: t.Optional[int] = None
def measure_table(rows): def measure_table(rows: t.Iterable[t.Tuple[str, str]]) -> t.Tuple[int, ...]:
widths = {} widths: t.Dict[int, int] = {}
for row in rows: for row in rows:
for idx, col in enumerate(row): for idx, col in enumerate(row):
widths[idx] = max(widths.get(idx, 0), term_len(col)) widths[idx] = max(widths.get(idx, 0), term_len(col))
return tuple(y for x, y in sorted(widths.items())) return tuple(y for x, y in sorted(widths.items()))
def iter_rows(rows, col_count): def iter_rows(
rows: t.Iterable[t.Tuple[str, str]], col_count: int
) -> t.Iterator[t.Tuple[str, ...]]:
for row in rows: for row in rows:
row = tuple(row)
yield row + ("",) * (col_count - len(row)) yield row + ("",) * (col_count - len(row))
def wrap_text( def wrap_text(
text, width=78, initial_indent="", subsequent_indent="", preserve_paragraphs=False text: str,
): width: int = 78,
initial_indent: str = "",
subsequent_indent: str = "",
preserve_paragraphs: bool = False,
) -> str:
"""A helper function that intelligently wraps text. By default, it """A helper function that intelligently wraps text. By default, it
assumes that it operates on a single paragraph of text but if the assumes that it operates on a single paragraph of text but if the
`preserve_paragraphs` parameter is provided it will intelligently `preserve_paragraphs` parameter is provided it will intelligently
@ -55,11 +63,11 @@ def wrap_text(
if not preserve_paragraphs: if not preserve_paragraphs:
return wrapper.fill(text) return wrapper.fill(text)
p = [] p: t.List[t.Tuple[int, bool, str]] = []
buf = [] buf: t.List[str] = []
indent = None indent = None
def _flush_par(): def _flush_par() -> None:
if not buf: if not buf:
return return
if buf[0].strip() == "\b": if buf[0].strip() == "\b":
@ -91,7 +99,7 @@ def wrap_text(
return "\n\n".join(rv) return "\n\n".join(rv)
class HelpFormatter(object): class HelpFormatter:
"""This class helps with formatting text-based help pages. It's """This class helps with formatting text-based help pages. It's
usually just needed for very special internal cases, but it's also usually just needed for very special internal cases, but it's also
exposed so that developers can write their own fancy outputs. exposed so that developers can write their own fancy outputs.
@ -103,38 +111,51 @@ class HelpFormatter(object):
width clamped to a maximum of 78. width clamped to a maximum of 78.
""" """
def __init__(self, indent_increment=2, width=None, max_width=None): def __init__(
self,
indent_increment: int = 2,
width: t.Optional[int] = None,
max_width: t.Optional[int] = None,
) -> None:
import shutil
self.indent_increment = indent_increment self.indent_increment = indent_increment
if max_width is None: if max_width is None:
max_width = 80 max_width = 80
if width is None: if width is None:
width = FORCED_WIDTH width = FORCED_WIDTH
if width is None: if width is None:
width = max(min(get_terminal_size()[0], max_width) - 2, 50) width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50)
self.width = width self.width = width
self.current_indent = 0 self.current_indent = 0
self.buffer = [] self.buffer: t.List[str] = []
def write(self, string): def write(self, string: str) -> None:
"""Writes a unicode string into the internal buffer.""" """Writes a unicode string into the internal buffer."""
self.buffer.append(string) self.buffer.append(string)
def indent(self): def indent(self) -> None:
"""Increases the indentation.""" """Increases the indentation."""
self.current_indent += self.indent_increment self.current_indent += self.indent_increment
def dedent(self): def dedent(self) -> None:
"""Decreases the indentation.""" """Decreases the indentation."""
self.current_indent -= self.indent_increment self.current_indent -= self.indent_increment
def write_usage(self, prog, args="", prefix="Usage: "): def write_usage(
self, prog: str, args: str = "", prefix: t.Optional[str] = None
) -> None:
"""Writes a usage line into the buffer. """Writes a usage line into the buffer.
:param prog: the program name. :param prog: the program name.
:param args: whitespace separated list of arguments. :param args: whitespace separated list of arguments.
:param prefix: the prefix for the first line. :param prefix: The prefix for the first line. Defaults to
``"Usage: "``.
""" """
usage_prefix = "{:>{w}}{} ".format(prefix, prog, w=self.current_indent) if prefix is None:
prefix = f"{_('Usage:')} "
usage_prefix = f"{prefix:>{self.current_indent}}{prog} "
text_width = self.width - self.current_indent text_width = self.width - self.current_indent
if text_width >= (term_len(usage_prefix) + 20): if text_width >= (term_len(usage_prefix) + 20):
@ -161,25 +182,24 @@ class HelpFormatter(object):
self.write("\n") self.write("\n")
def write_heading(self, heading): def write_heading(self, heading: str) -> None:
"""Writes a heading into the buffer.""" """Writes a heading into the buffer."""
self.write("{:>{w}}{}:\n".format("", heading, w=self.current_indent)) self.write(f"{'':>{self.current_indent}}{heading}:\n")
def write_paragraph(self): def write_paragraph(self) -> None:
"""Writes a paragraph into the buffer.""" """Writes a paragraph into the buffer."""
if self.buffer: if self.buffer:
self.write("\n") self.write("\n")
def write_text(self, text): def write_text(self, text: str) -> None:
"""Writes re-indented text into the buffer. This rewraps and """Writes re-indented text into the buffer. This rewraps and
preserves paragraphs. preserves paragraphs.
""" """
text_width = max(self.width - self.current_indent, 11)
indent = " " * self.current_indent indent = " " * self.current_indent
self.write( self.write(
wrap_text( wrap_text(
text, text,
text_width, self.width,
initial_indent=indent, initial_indent=indent,
subsequent_indent=indent, subsequent_indent=indent,
preserve_paragraphs=True, preserve_paragraphs=True,
@ -187,7 +207,12 @@ class HelpFormatter(object):
) )
self.write("\n") self.write("\n")
def write_dl(self, rows, col_max=30, col_spacing=2): def write_dl(
self,
rows: t.Sequence[t.Tuple[str, str]],
col_max: int = 30,
col_spacing: int = 2,
) -> None:
"""Writes a definition list into the buffer. This is how options """Writes a definition list into the buffer. This is how options
and commands are usually formatted. and commands are usually formatted.
@ -204,7 +229,7 @@ class HelpFormatter(object):
first_col = min(widths[0], col_max) + col_spacing first_col = min(widths[0], col_max) + col_spacing
for first, second in iter_rows(rows, len(widths)): for first, second in iter_rows(rows, len(widths)):
self.write("{:>{w}}{}".format("", first, w=self.current_indent)) self.write(f"{'':>{self.current_indent}}{first}")
if not second: if not second:
self.write("\n") self.write("\n")
continue continue
@ -219,23 +244,15 @@ class HelpFormatter(object):
lines = wrapped_text.splitlines() lines = wrapped_text.splitlines()
if lines: if lines:
self.write("{}\n".format(lines[0])) self.write(f"{lines[0]}\n")
for line in lines[1:]: for line in lines[1:]:
self.write( self.write(f"{'':>{first_col + self.current_indent}}{line}\n")
"{:>{w}}{}\n".format(
"", line, w=first_col + self.current_indent
)
)
if len(lines) > 1:
# separate long help from next option
self.write("\n")
else: else:
self.write("\n") self.write("\n")
@contextmanager @contextmanager
def section(self, name): def section(self, name: str) -> t.Iterator[None]:
"""Helpful context manager that writes a paragraph, a heading, """Helpful context manager that writes a paragraph, a heading,
and the indents. and the indents.
@ -250,7 +267,7 @@ class HelpFormatter(object):
self.dedent() self.dedent()
@contextmanager @contextmanager
def indentation(self): def indentation(self) -> t.Iterator[None]:
"""A context manager that increases the indentation.""" """A context manager that increases the indentation."""
self.indent() self.indent()
try: try:
@ -258,12 +275,12 @@ class HelpFormatter(object):
finally: finally:
self.dedent() self.dedent()
def getvalue(self): def getvalue(self) -> str:
"""Returns the buffer contents.""" """Returns the buffer contents."""
return "".join(self.buffer) return "".join(self.buffer)
def join_options(options): def join_options(options: t.Sequence[str]) -> t.Tuple[str, bool]:
"""Given a list of option strings this joins them in the most appropriate """Given a list of option strings this joins them in the most appropriate
way and returns them in the form ``(formatted_string, way and returns them in the form ``(formatted_string,
any_prefix_is_slash)`` where the second item in the tuple is a flag that any_prefix_is_slash)`` where the second item in the tuple is a flag that
@ -271,13 +288,14 @@ def join_options(options):
""" """
rv = [] rv = []
any_prefix_is_slash = False any_prefix_is_slash = False
for opt in options: for opt in options:
prefix = split_opt(opt)[0] prefix = split_opt(opt)[0]
if prefix == "/": if prefix == "/":
any_prefix_is_slash = True any_prefix_is_slash = True
rv.append((len(prefix), opt)) rv.append((len(prefix), opt))
rv.sort(key=lambda x: x[0]) rv.sort(key=lambda x: x[0])
return ", ".join(x[1] for x in rv), any_prefix_is_slash
rv = ", ".join(x[1] for x in rv)
return rv, any_prefix_is_slash

View file

@ -1,9 +1,25 @@
import typing
import typing as t
from threading import local from threading import local
if t.TYPE_CHECKING:
import typing_extensions as te
from .core import Context
_local = local() _local = local()
def get_current_context(silent=False): @typing.overload
def get_current_context(silent: "te.Literal[False]" = False) -> "Context":
...
@typing.overload
def get_current_context(silent: bool = ...) -> t.Optional["Context"]:
...
def get_current_context(silent: bool = False) -> t.Optional["Context"]:
"""Returns the current click context. This can be used as a way to """Returns the current click context. This can be used as a way to
access the current context object from anywhere. This is a more implicit access the current context object from anywhere. This is a more implicit
alternative to the :func:`pass_context` decorator. This function is alternative to the :func:`pass_context` decorator. This function is
@ -19,29 +35,35 @@ def get_current_context(silent=False):
:exc:`RuntimeError`. :exc:`RuntimeError`.
""" """
try: try:
return _local.stack[-1] return t.cast("Context", _local.stack[-1])
except (AttributeError, IndexError): except (AttributeError, IndexError) as e:
if not silent: if not silent:
raise RuntimeError("There is no active click context.") raise RuntimeError("There is no active click context.") from e
return None
def push_context(ctx): def push_context(ctx: "Context") -> None:
"""Pushes a new context to the current stack.""" """Pushes a new context to the current stack."""
_local.__dict__.setdefault("stack", []).append(ctx) _local.__dict__.setdefault("stack", []).append(ctx)
def pop_context(): def pop_context() -> None:
"""Removes the top level from the stack.""" """Removes the top level from the stack."""
_local.stack.pop() _local.stack.pop()
def resolve_color_default(color=None): def resolve_color_default(color: t.Optional[bool] = None) -> t.Optional[bool]:
""""Internal helper to get the default value of the color flag. If a """Internal helper to get the default value of the color flag. If a
value is passed it's returned unchanged, otherwise it's looked up from value is passed it's returned unchanged, otherwise it's looked up from
the current context. the current context.
""" """
if color is not None: if color is not None:
return color return color
ctx = get_current_context(silent=True) ctx = get_current_context(silent=True)
if ctx is not None: if ctx is not None:
return ctx.color return ctx.color
return None

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
""" """
This module started out as largely a copy paste from the stdlib's This module started out as largely a copy paste from the stdlib's
optparse module with the features removed that we do not need from optparse module with the features removed that we do not need from
@ -18,16 +17,38 @@ by the Python Software Foundation. This is limited to code in parser.py.
Copyright 2001-2006 Gregory P. Ward. All rights reserved. Copyright 2001-2006 Gregory P. Ward. All rights reserved.
Copyright 2002-2006 Python Software Foundation. All rights reserved. Copyright 2002-2006 Python Software Foundation. All rights reserved.
""" """
import re # This code uses parts of optparse written by Gregory P. Ward and
# maintained by the Python Software Foundation.
# Copyright 2001-2006 Gregory P. Ward
# Copyright 2002-2006 Python Software Foundation
import typing as t
from collections import deque from collections import deque
from gettext import gettext as _
from gettext import ngettext
from .exceptions import BadArgumentUsage from .exceptions import BadArgumentUsage
from .exceptions import BadOptionUsage from .exceptions import BadOptionUsage
from .exceptions import NoSuchOption from .exceptions import NoSuchOption
from .exceptions import UsageError from .exceptions import UsageError
if t.TYPE_CHECKING:
import typing_extensions as te
from .core import Argument as CoreArgument
from .core import Context
from .core import Option as CoreOption
from .core import Parameter as CoreParameter
def _unpack_args(args, nargs_spec): V = t.TypeVar("V")
# Sentinel value that indicates an option was passed as a flag without a
# value but is not a flag option. Option.consume_value uses this to
# prompt or use the flag_value.
_flag_needs_value = object()
def _unpack_args(
args: t.Sequence[str], nargs_spec: t.Sequence[int]
) -> t.Tuple[t.Sequence[t.Union[str, t.Sequence[t.Optional[str]], None]], t.List[str]]:
"""Given an iterable of arguments and an iterable of nargs specifications, """Given an iterable of arguments and an iterable of nargs specifications,
it returns a tuple with all the unpacked arguments at the first index it returns a tuple with all the unpacked arguments at the first index
and all remaining arguments as the second. and all remaining arguments as the second.
@ -39,10 +60,10 @@ def _unpack_args(args, nargs_spec):
""" """
args = deque(args) args = deque(args)
nargs_spec = deque(nargs_spec) nargs_spec = deque(nargs_spec)
rv = [] rv: t.List[t.Union[str, t.Tuple[t.Optional[str], ...], None]] = []
spos = None spos: t.Optional[int] = None
def _fetch(c): def _fetch(c: "te.Deque[V]") -> t.Optional[V]:
try: try:
if spos is None: if spos is None:
return c.popleft() return c.popleft()
@ -53,18 +74,25 @@ def _unpack_args(args, nargs_spec):
while nargs_spec: while nargs_spec:
nargs = _fetch(nargs_spec) nargs = _fetch(nargs_spec)
if nargs is None:
continue
if nargs == 1: if nargs == 1:
rv.append(_fetch(args)) rv.append(_fetch(args))
elif nargs > 1: elif nargs > 1:
x = [_fetch(args) for _ in range(nargs)] x = [_fetch(args) for _ in range(nargs)]
# If we're reversed, we're pulling in the arguments in reverse, # If we're reversed, we're pulling in the arguments in reverse,
# so we need to turn them around. # so we need to turn them around.
if spos is not None: if spos is not None:
x.reverse() x.reverse()
rv.append(tuple(x)) rv.append(tuple(x))
elif nargs < 0: elif nargs < 0:
if spos is not None: if spos is not None:
raise TypeError("Cannot have two nargs < 0") raise TypeError("Cannot have two nargs < 0")
spos = len(rv) spos = len(rv)
rv.append(None) rv.append(None)
@ -78,13 +106,7 @@ def _unpack_args(args, nargs_spec):
return tuple(rv), list(args) return tuple(rv), list(args)
def _error_opt_args(nargs, opt): def split_opt(opt: str) -> t.Tuple[str, str]:
if nargs == 1:
raise BadOptionUsage(opt, "{} option requires an argument".format(opt))
raise BadOptionUsage(opt, "{} option requires {} arguments".format(opt, nargs))
def split_opt(opt):
first = opt[:1] first = opt[:1]
if first.isalnum(): if first.isalnum():
return "", opt return "", opt
@ -93,34 +115,57 @@ def split_opt(opt):
return first, opt[1:] return first, opt[1:]
def normalize_opt(opt, ctx): def normalize_opt(opt: str, ctx: t.Optional["Context"]) -> str:
if ctx is None or ctx.token_normalize_func is None: if ctx is None or ctx.token_normalize_func is None:
return opt return opt
prefix, opt = split_opt(opt) prefix, opt = split_opt(opt)
return prefix + ctx.token_normalize_func(opt) return f"{prefix}{ctx.token_normalize_func(opt)}"
def split_arg_string(string): def split_arg_string(string: str) -> t.List[str]:
"""Given an argument string this attempts to split it into small parts.""" """Split an argument string as with :func:`shlex.split`, but don't
rv = [] fail if the string is incomplete. Ignores a missing closing quote or
for match in re.finditer( incomplete escape sequence and uses the partial token as-is.
r"('([^'\\]*(?:\\.[^'\\]*)*)'|\"([^\"\\]*(?:\\.[^\"\\]*)*)\"|\S+)\s*",
string, .. code-block:: python
re.S,
): split_arg_string("example 'my file")
arg = match.group().strip() ["example", "my file"]
if arg[:1] == arg[-1:] and arg[:1] in "\"'":
arg = arg[1:-1].encode("ascii", "backslashreplace").decode("unicode-escape") 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: try:
arg = type(string)(arg) for token in lex:
except UnicodeError: out.append(token)
pass except ValueError:
rv.append(arg) # Raised when end-of-string is reached in an invalid state. Use
return rv # 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(object): class Option:
def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None): 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,
):
self._short_opts = [] self._short_opts = []
self._long_opts = [] self._long_opts = []
self.prefixes = set() self.prefixes = set()
@ -128,7 +173,7 @@ class Option(object):
for opt in opts: for opt in opts:
prefix, value = split_opt(opt) prefix, value = split_opt(opt)
if not prefix: if not prefix:
raise ValueError("Invalid start character for option ({})".format(opt)) raise ValueError(f"Invalid start character for option ({opt})")
self.prefixes.add(prefix[0]) self.prefixes.add(prefix[0])
if len(prefix) == 1 and len(value) == 1: if len(prefix) == 1 and len(value) == 1:
self._short_opts.append(opt) self._short_opts.append(opt)
@ -146,53 +191,66 @@ class Option(object):
self.obj = obj self.obj = obj
@property @property
def takes_value(self): def takes_value(self) -> bool:
return self.action in ("store", "append") return self.action in ("store", "append")
def process(self, value, state): def process(self, value: str, state: "ParsingState") -> None:
if self.action == "store": if self.action == "store":
state.opts[self.dest] = value state.opts[self.dest] = value # type: ignore
elif self.action == "store_const": elif self.action == "store_const":
state.opts[self.dest] = self.const state.opts[self.dest] = self.const # type: ignore
elif self.action == "append": elif self.action == "append":
state.opts.setdefault(self.dest, []).append(value) state.opts.setdefault(self.dest, []).append(value) # type: ignore
elif self.action == "append_const": elif self.action == "append_const":
state.opts.setdefault(self.dest, []).append(self.const) state.opts.setdefault(self.dest, []).append(self.const) # type: ignore
elif self.action == "count": elif self.action == "count":
state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore
else: else:
raise ValueError("unknown action '{}'".format(self.action)) raise ValueError(f"unknown action '{self.action}'")
state.order.append(self.obj) state.order.append(self.obj)
class Argument(object): class Argument:
def __init__(self, dest, nargs=1, obj=None): def __init__(self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1):
self.dest = dest self.dest = dest
self.nargs = nargs self.nargs = nargs
self.obj = obj self.obj = obj
def process(self, value, state): def process(
self,
value: t.Union[t.Optional[str], t.Sequence[t.Optional[str]]],
state: "ParsingState",
) -> None:
if self.nargs > 1: if self.nargs > 1:
assert value is not None
holes = sum(1 for x in value if x is None) holes = sum(1 for x in value if x is None)
if holes == len(value): if holes == len(value):
value = None value = None
elif holes != 0: elif holes != 0:
raise BadArgumentUsage( raise BadArgumentUsage(
"argument {} takes {} values".format(self.dest, self.nargs) _("Argument {name!r} takes {nargs} values.").format(
name=self.dest, nargs=self.nargs
) )
state.opts[self.dest] = value )
if self.nargs == -1 and self.obj.envvar is not None and value == ():
# Replace empty tuple with None so that a value from the
# environment may be tried.
value = None
state.opts[self.dest] = value # type: ignore
state.order.append(self.obj) state.order.append(self.obj)
class ParsingState(object): class ParsingState:
def __init__(self, rargs): def __init__(self, rargs: t.List[str]) -> None:
self.opts = {} self.opts: t.Dict[str, t.Any] = {}
self.largs = [] self.largs: t.List[str] = []
self.rargs = rargs self.rargs = rargs
self.order = [] self.order: t.List["CoreParameter"] = []
class OptionParser(object): class OptionParser:
"""The option parser is an internal class that is ultimately used to """The option parser is an internal class that is ultimately used to
parse options and arguments. It's modelled after optparse and brings parse options and arguments. It's modelled after optparse and brings
a similar but vastly simplified API. It should generally not be used a similar but vastly simplified API. It should generally not be used
@ -206,7 +264,7 @@ class OptionParser(object):
should go with. should go with.
""" """
def __init__(self, ctx=None): def __init__(self, ctx: t.Optional["Context"] = None) -> None:
#: The :class:`~click.Context` for this parser. This might be #: The :class:`~click.Context` for this parser. This might be
#: `None` for some advanced use cases. #: `None` for some advanced use cases.
self.ctx = ctx self.ctx = ctx
@ -220,44 +278,54 @@ class OptionParser(object):
#: second mode where it will ignore it and continue processing #: second mode where it will ignore it and continue processing
#: after shifting all the unknown options into the resulting args. #: after shifting all the unknown options into the resulting args.
self.ignore_unknown_options = False self.ignore_unknown_options = False
if ctx is not None: if ctx is not None:
self.allow_interspersed_args = ctx.allow_interspersed_args self.allow_interspersed_args = ctx.allow_interspersed_args
self.ignore_unknown_options = ctx.ignore_unknown_options self.ignore_unknown_options = ctx.ignore_unknown_options
self._short_opt = {}
self._long_opt = {}
self._opt_prefixes = {"-", "--"}
self._args = []
def add_option(self, opts, dest, action=None, nargs=1, const=None, obj=None): self._short_opt: t.Dict[str, Option] = {}
self._long_opt: t.Dict[str, Option] = {}
self._opt_prefixes = {"-", "--"}
self._args: t.List[Argument] = []
def add_option(
self,
obj: "CoreOption",
opts: t.Sequence[str],
dest: t.Optional[str],
action: t.Optional[str] = None,
nargs: int = 1,
const: t.Optional[t.Any] = None,
) -> None:
"""Adds a new option named `dest` to the parser. The destination """Adds a new option named `dest` to the parser. The destination
is not inferred (unlike with optparse) and needs to be explicitly is not inferred (unlike with optparse) and needs to be explicitly
provided. Action can be any of ``store``, ``store_const``, provided. Action can be any of ``store``, ``store_const``,
``append``, ``appnd_const`` or ``count``. ``append``, ``append_const`` or ``count``.
The `obj` can be used to identify the option in the order list The `obj` can be used to identify the option in the order list
that is returned from the parser. that is returned from the parser.
""" """
if obj is None:
obj = dest
opts = [normalize_opt(opt, self.ctx) for opt in opts] opts = [normalize_opt(opt, self.ctx) for opt in opts]
option = Option(opts, dest, action=action, nargs=nargs, const=const, obj=obj) option = Option(obj, opts, dest, action=action, nargs=nargs, const=const)
self._opt_prefixes.update(option.prefixes) self._opt_prefixes.update(option.prefixes)
for opt in option._short_opts: for opt in option._short_opts:
self._short_opt[opt] = option self._short_opt[opt] = option
for opt in option._long_opts: for opt in option._long_opts:
self._long_opt[opt] = option self._long_opt[opt] = option
def add_argument(self, dest, nargs=1, obj=None): def add_argument(
self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1
) -> None:
"""Adds a positional argument named `dest` to the parser. """Adds a positional argument named `dest` to the parser.
The `obj` can be used to identify the option in the order list The `obj` can be used to identify the option in the order list
that is returned from the parser. that is returned from the parser.
""" """
if obj is None: self._args.append(Argument(obj, dest=dest, nargs=nargs))
obj = dest
self._args.append(Argument(dest=dest, nargs=nargs, obj=obj))
def parse_args(self, args): def parse_args(
self, args: t.List[str]
) -> t.Tuple[t.Dict[str, t.Any], t.List[str], t.List["CoreParameter"]]:
"""Parses positional arguments and returns ``(values, args, order)`` """Parses positional arguments and returns ``(values, args, order)``
for the parsed options and arguments as well as the leftover for the parsed options and arguments as well as the leftover
arguments if there are any. The order is a list of objects as they arguments if there are any. The order is a list of objects as they
@ -273,7 +341,7 @@ class OptionParser(object):
raise raise
return state.opts, state.largs, state.order return state.opts, state.largs, state.order
def _process_args_for_args(self, state): def _process_args_for_args(self, state: ParsingState) -> None:
pargs, args = _unpack_args( pargs, args = _unpack_args(
state.largs + state.rargs, [x.nargs for x in self._args] state.largs + state.rargs, [x.nargs for x in self._args]
) )
@ -284,7 +352,7 @@ class OptionParser(object):
state.largs = args state.largs = args
state.rargs = [] state.rargs = []
def _process_args_for_options(self, state): def _process_args_for_options(self, state: ParsingState) -> None:
while state.rargs: while state.rargs:
arg = state.rargs.pop(0) arg = state.rargs.pop(0)
arglen = len(arg) arglen = len(arg)
@ -320,9 +388,13 @@ class OptionParser(object):
# *empty* -- still a subset of [arg0, ..., arg(i-1)], but # *empty* -- still a subset of [arg0, ..., arg(i-1)], but
# not a very interesting subset! # not a very interesting subset!
def _match_long_opt(self, opt, explicit_value, state): def _match_long_opt(
self, opt: str, explicit_value: t.Optional[str], state: ParsingState
) -> None:
if opt not in self._long_opt: if opt not in self._long_opt:
possibilities = [word for word in self._long_opt if word.startswith(opt)] from difflib import get_close_matches
possibilities = get_close_matches(opt, self._long_opt)
raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
option = self._long_opt[opt] option = self._long_opt[opt]
@ -334,31 +406,26 @@ class OptionParser(object):
if explicit_value is not None: if explicit_value is not None:
state.rargs.insert(0, explicit_value) state.rargs.insert(0, explicit_value)
nargs = option.nargs value = self._get_value_from_state(opt, option, state)
if len(state.rargs) < nargs:
_error_opt_args(nargs, opt)
elif nargs == 1:
value = state.rargs.pop(0)
else:
value = tuple(state.rargs[:nargs])
del state.rargs[:nargs]
elif explicit_value is not None: elif explicit_value is not None:
raise BadOptionUsage(opt, "{} option does not take a value".format(opt)) raise BadOptionUsage(
opt, _("Option {name!r} does not take a value.").format(name=opt)
)
else: else:
value = None value = None
option.process(value, state) option.process(value, state)
def _match_short_opt(self, arg, state): def _match_short_opt(self, arg: str, state: ParsingState) -> None:
stop = False stop = False
i = 1 i = 1
prefix = arg[0] prefix = arg[0]
unknown_options = [] unknown_options = []
for ch in arg[1:]: for ch in arg[1:]:
opt = normalize_opt(prefix + ch, self.ctx) opt = normalize_opt(f"{prefix}{ch}", self.ctx)
option = self._short_opt.get(opt) option = self._short_opt.get(opt)
i += 1 i += 1
@ -374,14 +441,7 @@ class OptionParser(object):
state.rargs.insert(0, arg[i:]) state.rargs.insert(0, arg[i:])
stop = True stop = True
nargs = option.nargs value = self._get_value_from_state(opt, option, state)
if len(state.rargs) < nargs:
_error_opt_args(nargs, opt)
elif nargs == 1:
value = state.rargs.pop(0)
else:
value = tuple(state.rargs[:nargs])
del state.rargs[:nargs]
else: else:
value = None value = None
@ -396,9 +456,47 @@ class OptionParser(object):
# to the state as new larg. This way there is basic combinatorics # to the state as new larg. This way there is basic combinatorics
# that can be achieved while still ignoring unknown arguments. # that can be achieved while still ignoring unknown arguments.
if self.ignore_unknown_options and unknown_options: if self.ignore_unknown_options and unknown_options:
state.largs.append("{}{}".format(prefix, "".join(unknown_options))) state.largs.append(f"{prefix}{''.join(unknown_options)}")
def _process_opts(self, arg, state): def _get_value_from_state(
self, option_name: str, option: Option, state: ParsingState
) -> t.Any:
nargs = option.nargs
if len(state.rargs) < nargs:
if option.obj._flag_needs_value:
# Option allows omitting the value.
value = _flag_needs_value
else:
raise BadOptionUsage(
option_name,
ngettext(
"Option {name!r} requires an argument.",
"Option {name!r} requires {nargs} arguments.",
nargs,
).format(name=option_name, nargs=nargs),
)
elif nargs == 1:
next_rarg = state.rargs[0]
if (
option.obj._flag_needs_value
and isinstance(next_rarg, str)
and next_rarg[:1] in self._opt_prefixes
and len(next_rarg) > 1
):
# The next arg looks like the start of an option, don't
# use it as the value if omitting the value is allowed.
value = _flag_needs_value
else:
value = state.rargs.pop(0)
else:
value = tuple(state.rargs[:nargs])
del state.rargs[:nargs]
return value
def _process_opts(self, arg: str, state: ParsingState) -> None:
explicit_value = None explicit_value = None
# Long option handling happens in two parts. The first part is # Long option handling happens in two parts. The first part is
# supporting explicitly attached values. In any case, we will try # supporting explicitly attached values. In any case, we will try
@ -422,7 +520,10 @@ class OptionParser(object):
# short option code and will instead raise the no option # short option code and will instead raise the no option
# error. # error.
if arg[:2] not in self._opt_prefixes: if arg[:2] not in self._opt_prefixes:
return self._match_short_opt(arg, state) self._match_short_opt(arg, state)
return
if not self.ignore_unknown_options: if not self.ignore_unknown_options:
raise raise
state.largs.append(arg) state.largs.append(arg)

0
src/click/py.typed Normal file
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 io
import itertools import itertools
import os import os
import struct
import sys import sys
import typing
import typing as t
from gettext import gettext as _
from ._compat import DEFAULT_COLUMNS
from ._compat import get_winterm_size
from ._compat import isatty from ._compat import isatty
from ._compat import raw_input
from ._compat import string_types
from ._compat import strip_ansi from ._compat import strip_ansi
from ._compat import text_type
from ._compat import WIN from ._compat import WIN
from .exceptions import Abort from .exceptions import Abort
from .exceptions import UsageError from .exceptions import UsageError
from .globals import resolve_color_default from .globals import resolve_color_default
from .types import Choice from .types import Choice
from .types import convert_type from .types import convert_type
from .types import Path from .types import ParamType
from .utils import echo from .utils import echo
from .utils import LazyFile from .utils import LazyFile
if t.TYPE_CHECKING:
from ._termui_impl import ProgressBar
V = t.TypeVar("V")
# The prompt functions to use. The doc tools currently override these # The prompt functions to use. The doc tools currently override these
# functions to customize how they work. # functions to customize how they work.
visible_prompt_func = raw_input visible_prompt_func: t.Callable[[str], str] = input
_ansi_colors = { _ansi_colors = {
"black": 30, "black": 30,
@ -48,63 +50,61 @@ _ansi_colors = {
_ansi_reset_all = "\033[0m" _ansi_reset_all = "\033[0m"
def hidden_prompt_func(prompt): def hidden_prompt_func(prompt: str) -> str:
import getpass import getpass
return getpass.getpass(prompt) return getpass.getpass(prompt)
def _build_prompt( def _build_prompt(
text, suffix, show_default=False, default=None, show_choices=True, type=None text: str,
): suffix: str,
show_default: bool = False,
default: t.Optional[t.Any] = None,
show_choices: bool = True,
type: t.Optional[ParamType] = None,
) -> str:
prompt = text prompt = text
if type is not None and show_choices and isinstance(type, Choice): if type is not None and show_choices and isinstance(type, Choice):
prompt += " ({})".format(", ".join(map(str, type.choices))) prompt += f" ({', '.join(map(str, type.choices))})"
if default is not None and show_default: if default is not None and show_default:
prompt = "{} [{}]".format(prompt, _format_default(default)) prompt = f"{prompt} [{_format_default(default)}]"
return prompt + suffix return f"{prompt}{suffix}"
def _format_default(default): def _format_default(default: t.Any) -> t.Any:
if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"): if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"):
return default.name return default.name # type: ignore
return default return default
def prompt( def prompt(
text, text: str,
default=None, default: t.Optional[t.Any] = None,
hide_input=False, hide_input: bool = False,
confirmation_prompt=False, confirmation_prompt: t.Union[bool, str] = False,
type=None, type: t.Optional[t.Union[ParamType, t.Any]] = None,
value_proc=None, value_proc: t.Optional[t.Callable[[str], t.Any]] = None,
prompt_suffix=": ", prompt_suffix: str = ": ",
show_default=True, show_default: bool = True,
err=False, err: bool = False,
show_choices=True, show_choices: bool = True,
): ) -> t.Any:
"""Prompts a user for input. This is a convenience function that can """Prompts a user for input. This is a convenience function that can
be used to prompt a user for input later. be used to prompt a user for input later.
If the user aborts the input by sending a interrupt signal, this If the user aborts the input by sending a interrupt signal, this
function will catch it and raise a :exc:`Abort` exception. function will catch it and raise a :exc:`Abort` exception.
.. versionadded:: 7.0
Added the show_choices parameter.
.. versionadded:: 6.0
Added unicode support for cmd.exe on Windows.
.. versionadded:: 4.0
Added the `err` parameter.
:param text: the text to show for the prompt. :param text: the text to show for the prompt.
:param default: the default value to use if no input happens. If this :param default: the default value to use if no input happens. If this
is not given it will prompt until it's aborted. is not given it will prompt until it's aborted.
:param hide_input: if this is set to true then the input value will :param hide_input: if this is set to true then the input value will
be hidden. be hidden.
:param confirmation_prompt: asks for confirmation for the value. :param confirmation_prompt: Prompt a second time to confirm the
value. Can be set to a string instead of ``True`` to customize
the message.
:param type: the type to use to check the value against. :param type: the type to use to check the value against.
:param value_proc: if this parameter is provided it's a function that :param value_proc: if this parameter is provided it's a function that
is invoked instead of the type conversion to is invoked instead of the type conversion to
@ -117,23 +117,37 @@ def prompt(
For example if type is a Choice of either day or week, For example if type is a Choice of either day or week,
show_choices is true and text is "Group by" then the show_choices is true and text is "Group by" then the
prompt will be "Group by (day, week): ". prompt will be "Group by (day, week): ".
"""
result = None
def prompt_func(text): .. versionadded:: 8.0
``confirmation_prompt`` can be a custom string.
.. versionadded:: 7.0
Added the ``show_choices`` parameter.
.. versionadded:: 6.0
Added unicode support for cmd.exe on Windows.
.. versionadded:: 4.0
Added the `err` parameter.
"""
def prompt_func(text: str) -> str:
f = hidden_prompt_func if hide_input else visible_prompt_func f = hidden_prompt_func if hide_input else visible_prompt_func
try: try:
# Write the prompt separately so that we get nice # Write the prompt separately so that we get nice
# coloring through colorama on Windows # coloring through colorama on Windows
echo(text, nl=False, err=err) echo(text.rstrip(" "), nl=False, err=err)
return f("") # Echo a space to stdout to work around an issue where
# readline causes backspace to clear the whole line.
return f(" ")
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
# getpass doesn't print a newline if the user aborts input with ^C. # getpass doesn't print a newline if the user aborts input with ^C.
# Allegedly this behavior is inherited from getpass(3). # Allegedly this behavior is inherited from getpass(3).
# A doc bug has been filed at https://bugs.python.org/issue24711 # A doc bug has been filed at https://bugs.python.org/issue24711
if hide_input: if hide_input:
echo(None, err=err) echo(None, err=err)
raise Abort() raise Abort() from None
if value_proc is None: if value_proc is None:
value_proc = convert_type(type, default) value_proc = convert_type(type, default)
@ -142,72 +156,93 @@ def prompt(
text, prompt_suffix, show_default, default, show_choices, type text, prompt_suffix, show_default, default, show_choices, type
) )
while 1: if confirmation_prompt:
while 1: if confirmation_prompt is True:
confirmation_prompt = _("Repeat for confirmation")
confirmation_prompt = t.cast(str, confirmation_prompt)
confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix)
while True:
while True:
value = prompt_func(prompt) value = prompt_func(prompt)
if value: if value:
break break
elif default is not None: elif default is not None:
if isinstance(value_proc, Path):
# validate Path default value(exists, dir_okay etc.)
value = default value = default
break break
return default
try: try:
result = value_proc(value) result = value_proc(value)
except UsageError as e: except UsageError as e:
echo("Error: {}".format(e.message), err=err) # noqa: B306 if hide_input:
echo(_("Error: The value you entered was invalid."), err=err)
else:
echo(_("Error: {e.message}").format(e=e), err=err) # noqa: B306
continue continue
if not confirmation_prompt: if not confirmation_prompt:
return result return result
while 1: while True:
value2 = prompt_func("Repeat for confirmation: ") confirmation_prompt = t.cast(str, confirmation_prompt)
value2 = prompt_func(confirmation_prompt)
if value2: if value2:
break break
if value == value2: if value == value2:
return result return result
echo("Error: the two entered values do not match", err=err) echo(_("Error: The two entered values do not match."), err=err)
def confirm( def confirm(
text, default=False, abort=False, prompt_suffix=": ", show_default=True, err=False text: str,
): default: t.Optional[bool] = False,
abort: bool = False,
prompt_suffix: str = ": ",
show_default: bool = True,
err: bool = False,
) -> bool:
"""Prompts for confirmation (yes/no question). """Prompts for confirmation (yes/no question).
If the user aborts the input by sending a interrupt signal this If the user aborts the input by sending a interrupt signal this
function will catch it and raise a :exc:`Abort` exception. function will catch it and raise a :exc:`Abort` exception.
.. versionadded:: 4.0
Added the `err` parameter.
:param text: the question to ask. :param text: the question to ask.
:param default: the default for the prompt. :param default: The default value to use when no input is given. If
``None``, repeat until input is given.
:param abort: if this is set to `True` a negative answer aborts the :param abort: if this is set to `True` a negative answer aborts the
exception by raising :exc:`Abort`. exception by raising :exc:`Abort`.
:param prompt_suffix: a suffix that should be added to the prompt. :param prompt_suffix: a suffix that should be added to the prompt.
:param show_default: shows or hides the default value in the prompt. :param show_default: shows or hides the default value in the prompt.
:param err: if set to true the file defaults to ``stderr`` instead of :param err: if set to true the file defaults to ``stderr`` instead of
``stdout``, the same as with echo. ``stdout``, the same as with echo.
.. versionchanged:: 8.0
Repeat until input is given if ``default`` is ``None``.
.. versionadded:: 4.0
Added the ``err`` parameter.
""" """
prompt = _build_prompt( prompt = _build_prompt(
text, prompt_suffix, show_default, "Y/n" if default else "y/N" text,
prompt_suffix,
show_default,
"y/n" if default is None else ("Y/n" if default else "y/N"),
) )
while 1:
while True:
try: try:
# Write the prompt separately so that we get nice # Write the prompt separately so that we get nice
# coloring through colorama on Windows # coloring through colorama on Windows
echo(prompt, nl=False, err=err) echo(prompt, nl=False, err=err)
value = visible_prompt_func("").lower().strip() value = visible_prompt_func("").lower().strip()
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
raise Abort() raise Abort() from None
if value in ("y", "yes"): if value in ("y", "yes"):
rv = True rv = True
elif value in ("n", "no"): elif value in ("n", "no"):
rv = False rv = False
elif value == "": elif default is not None and value == "":
rv = default rv = default
else: else:
echo("Error: invalid input", err=err) echo(_("Error: invalid input"), err=err)
continue continue
break break
if abort and not rv: if abort and not rv:
@ -215,54 +250,30 @@ def confirm(
return rv return rv
def get_terminal_size(): def get_terminal_size() -> os.terminal_size:
"""Returns the current size of the terminal as tuple in the form """Returns the current size of the terminal as tuple in the form
``(width, height)`` in columns and rows. ``(width, height)`` in columns and rows.
.. deprecated:: 8.0
Will be removed in Click 8.1. Use
:func:`shutil.get_terminal_size` instead.
""" """
# If shutil has get_terminal_size() (Python 3.3 and later) use that
if sys.version_info >= (3, 3):
import shutil import shutil
import warnings
shutil_get_terminal_size = getattr(shutil, "get_terminal_size", None) warnings.warn(
if shutil_get_terminal_size: "'click.get_terminal_size()' is deprecated and will be removed"
sz = shutil_get_terminal_size() " in Click 8.1. Use 'shutil.get_terminal_size()' instead.",
return sz.columns, sz.lines DeprecationWarning,
stacklevel=2,
# We provide a sensible default for get_winterm_size() when being invoked )
# inside a subprocess. Without this, it would not provide a useful input. return shutil.get_terminal_size()
if get_winterm_size is not None:
size = get_winterm_size()
if size == (0, 0):
return (79, 24)
else:
return size
def ioctl_gwinsz(fd):
try:
import fcntl
import termios
cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
except Exception:
return
return cr
cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2)
if not cr:
try:
fd = os.open(os.ctermid(), os.O_RDONLY)
try:
cr = ioctl_gwinsz(fd)
finally:
os.close(fd)
except Exception:
pass
if not cr or not cr[0] or not cr[1]:
cr = (os.environ.get("LINES", 25), os.environ.get("COLUMNS", DEFAULT_COLUMNS))
return int(cr[1]), int(cr[0])
def echo_via_pager(text_or_generator, color=None): def echo_via_pager(
text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str],
color: t.Optional[bool] = None,
) -> None:
"""This function takes a text and shows it via an environment specific """This function takes a text and shows it via an environment specific
pager on stdout. pager on stdout.
@ -277,14 +288,14 @@ def echo_via_pager(text_or_generator, color=None):
color = resolve_color_default(color) color = resolve_color_default(color)
if inspect.isgeneratorfunction(text_or_generator): if inspect.isgeneratorfunction(text_or_generator):
i = text_or_generator() i = t.cast(t.Callable[[], t.Iterable[str]], text_or_generator)()
elif isinstance(text_or_generator, string_types): elif isinstance(text_or_generator, str):
i = [text_or_generator] i = [text_or_generator]
else: else:
i = iter(text_or_generator) i = iter(t.cast(t.Iterable[str], text_or_generator))
# convert every element of i to a text type if necessary # convert every element of i to a text type if necessary
text_generator = (el if isinstance(el, string_types) else text_type(el) for el in i) text_generator = (el if isinstance(el, str) else str(el) for el in i)
from ._termui_impl import pager from ._termui_impl import pager
@ -292,21 +303,22 @@ def echo_via_pager(text_or_generator, color=None):
def progressbar( def progressbar(
iterable=None, iterable: t.Optional[t.Iterable[V]] = None,
length=None, length: t.Optional[int] = None,
label=None, label: t.Optional[str] = None,
show_eta=True, show_eta: bool = True,
show_percent=None, show_percent: t.Optional[bool] = None,
show_pos=False, show_pos: bool = False,
item_show_func=None, item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None,
fill_char="#", fill_char: str = "#",
empty_char="-", empty_char: str = "-",
bar_template="%(label)s [%(bar)s] %(info)s", bar_template: str = "%(label)s [%(bar)s] %(info)s",
info_sep=" ", info_sep: str = " ",
width=36, width: int = 36,
file=None, file: t.Optional[t.TextIO] = None,
color=None, color: t.Optional[bool] = None,
): update_min_steps: int = 1,
) -> "ProgressBar[V]":
"""This function creates an iterable context manager that can be used """This function creates an iterable context manager that can be used
to iterate over something while showing a progress bar. It will to iterate over something while showing a progress bar. It will
either iterate over the `iterable` or `length` items (that are counted either iterate over the `iterable` or `length` items (that are counted
@ -346,11 +358,19 @@ def progressbar(
process_chunk(chunk) process_chunk(chunk)
bar.update(chunks.bytes) bar.update(chunks.bytes)
.. versionadded:: 2.0 The ``update()`` method also takes an optional value specifying the
``current_item`` at the new position. This is useful when used
together with ``item_show_func`` to customize the output for each
manual step::
.. versionadded:: 4.0 with click.progressbar(
Added the `color` parameter. Added a `update` method to the length=total_size,
progressbar object. label='Unzipping archive',
item_show_func=lambda a: a.filename
) as bar:
for archive in zip_file:
archive.extract()
bar.update(archive.size, archive)
:param iterable: an iterable to iterate over. If not provided the length :param iterable: an iterable to iterate over. If not provided the length
is required. is required.
@ -369,10 +389,10 @@ def progressbar(
`False` if not. `False` if not.
:param show_pos: enables or disables the absolute position display. The :param show_pos: enables or disables the absolute position display. The
default is `False`. default is `False`.
:param item_show_func: a function called with the current item which :param item_show_func: A function called with the current item which
can return a string to show the current item can return a string to show next to the progress bar. If the
next to the progress bar. Note that the current function returns ``None`` nothing is shown. The current item can
item can be `None`! be ``None``, such as when entering and exiting the bar.
:param fill_char: the character to use to show the filled part of the :param fill_char: the character to use to show the filled part of the
progress bar. progress bar.
:param empty_char: the character to use to show the non-filled part of :param empty_char: the character to use to show the non-filled part of
@ -384,12 +404,33 @@ def progressbar(
:param info_sep: the separator between multiple info items (eta etc.) :param info_sep: the separator between multiple info items (eta etc.)
:param width: the width of the progress bar in characters, 0 means full :param width: the width of the progress bar in characters, 0 means full
terminal width terminal width
:param file: the file to write to. If this is not a terminal then :param file: The file to write to. If this is not a terminal then
only the label is printed. only the label is printed.
:param color: controls if the terminal supports ANSI colors or not. The :param color: controls if the terminal supports ANSI colors or not. The
default is autodetection. This is only needed if ANSI default is autodetection. This is only needed if ANSI
codes are included anywhere in the progress bar output codes are included anywhere in the progress bar output
which is not the case by default. which is not the case by default.
:param update_min_steps: Render only when this many updates have
completed. This allows tuning for very fast iterators.
.. versionchanged:: 8.0
Output is shown even if execution time is less than 0.5 seconds.
.. versionchanged:: 8.0
``item_show_func`` shows the current item, not the previous one.
.. versionchanged:: 8.0
Labels are echoed if the output is not a TTY. Reverts a change
in 7.0 that removed all output.
.. versionadded:: 8.0
Added the ``update_min_steps`` parameter.
.. versionchanged:: 4.0
Added the ``color`` parameter. Added the ``update`` method to
the object.
.. versionadded:: 2.0
""" """
from ._termui_impl import ProgressBar from ._termui_impl import ProgressBar
@ -409,10 +450,11 @@ def progressbar(
label=label, label=label,
width=width, width=width,
color=color, color=color,
update_min_steps=update_min_steps,
) )
def clear(): def clear() -> None:
"""Clears the terminal screen. This will have the effect of clearing """Clears the terminal screen. This will have the effect of clearing
the whole visible space of the terminal and moving the cursor to the the whole visible space of the terminal and moving the cursor to the
top left. This does not do anything if not connected to a terminal. top left. This does not do anything if not connected to a terminal.
@ -421,26 +463,39 @@ def clear():
""" """
if not isatty(sys.stdout): if not isatty(sys.stdout):
return return
# If we're on Windows and we don't have colorama available, then we
# clear the screen by shelling out. Otherwise we can use an escape
# sequence.
if WIN: if WIN:
os.system("cls") os.system("cls")
else: else:
sys.stdout.write("\033[2J\033[1;1H") sys.stdout.write("\033[2J\033[1;1H")
def _interpret_color(
color: t.Union[int, t.Tuple[int, int, int], str], offset: int = 0
) -> str:
if isinstance(color, int):
return f"{38 + offset};5;{color:d}"
if isinstance(color, (tuple, list)):
r, g, b = color
return f"{38 + offset};2;{r:d};{g:d};{b:d}"
return str(_ansi_colors[color] + offset)
def style( def style(
text, text: t.Any,
fg=None, fg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None,
bg=None, bg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None,
bold=None, bold: t.Optional[bool] = None,
dim=None, dim: t.Optional[bool] = None,
underline=None, underline: t.Optional[bool] = None,
blink=None, overline: t.Optional[bool] = None,
reverse=None, italic: t.Optional[bool] = None,
reset=True, blink: t.Optional[bool] = None,
): reverse: t.Optional[bool] = None,
strikethrough: t.Optional[bool] = None,
reset: bool = True,
) -> str:
"""Styles a text with ANSI styles and returns the new string. By """Styles a text with ANSI styles and returns the new string. By
default the styling is self contained which means that at the end default the styling is self contained which means that at the end
of the string a reset code is issued. This can be prevented by of the string a reset code is issued. This can be prevented by
@ -451,6 +506,7 @@ def style(
click.echo(click.style('Hello World!', fg='green')) click.echo(click.style('Hello World!', fg='green'))
click.echo(click.style('ATTENTION!', blink=True)) click.echo(click.style('ATTENTION!', blink=True))
click.echo(click.style('Some things', reverse=True, fg='cyan')) click.echo(click.style('Some things', reverse=True, fg='cyan'))
click.echo(click.style('More colors', fg=(255, 12, 128), bg=117))
Supported color names: Supported color names:
@ -472,10 +528,15 @@ def style(
* ``bright_white`` * ``bright_white``
* ``reset`` (reset the color code only) * ``reset`` (reset the color code only)
.. versionadded:: 2.0 If the terminal supports it, color may also be specified as:
.. versionadded:: 7.0 - An integer in the interval [0, 255]. The terminal must support
Added support for bright colors. 8-bit/256-color mode.
- An RGB tuple of three integers in [0, 255]. The terminal must
support 24-bit/true-color mode.
See https://en.wikipedia.org/wiki/ANSI_color and
https://gist.github.com/XVilka/8346728 for more information.
:param text: the string to style with ansi codes. :param text: the string to style with ansi codes.
:param fg: if provided this will become the foreground color. :param fg: if provided this will become the foreground color.
@ -484,42 +545,73 @@ def style(
:param dim: if provided this will enable or disable dim mode. This is :param dim: if provided this will enable or disable dim mode. This is
badly supported. badly supported.
:param underline: if provided this will enable or disable underline. :param underline: if provided this will enable or disable underline.
:param overline: if provided this will enable or disable overline.
:param italic: if provided this will enable or disable italic.
:param blink: if provided this will enable or disable blinking. :param blink: if provided this will enable or disable blinking.
:param reverse: if provided this will enable or disable inverse :param reverse: if provided this will enable or disable inverse
rendering (foreground becomes background and the rendering (foreground becomes background and the
other way round). other way round).
:param strikethrough: if provided this will enable or disable
striking through text.
:param reset: by default a reset-all code is added at the end of the :param reset: by default a reset-all code is added at the end of the
string which means that styles do not carry over. This string which means that styles do not carry over. This
can be disabled to compose styles. can be disabled to compose styles.
.. versionchanged:: 8.0
A non-string ``message`` is converted to a string.
.. versionchanged:: 8.0
Added support for 256 and RGB color codes.
.. versionchanged:: 8.0
Added the ``strikethrough``, ``italic``, and ``overline``
parameters.
.. versionchanged:: 7.0
Added support for bright colors.
.. versionadded:: 2.0
""" """
if not isinstance(text, str):
text = str(text)
bits = [] bits = []
if fg: if fg:
try: try:
bits.append("\033[{}m".format(_ansi_colors[fg])) bits.append(f"\033[{_interpret_color(fg)}m")
except KeyError: except KeyError:
raise TypeError("Unknown color '{}'".format(fg)) raise TypeError(f"Unknown color {fg!r}") from None
if bg: if bg:
try: try:
bits.append("\033[{}m".format(_ansi_colors[bg] + 10)) bits.append(f"\033[{_interpret_color(bg, 10)}m")
except KeyError: except KeyError:
raise TypeError("Unknown color '{}'".format(bg)) raise TypeError(f"Unknown color {bg!r}") from None
if bold is not None: if bold is not None:
bits.append("\033[{}m".format(1 if bold else 22)) bits.append(f"\033[{1 if bold else 22}m")
if dim is not None: if dim is not None:
bits.append("\033[{}m".format(2 if dim else 22)) bits.append(f"\033[{2 if dim else 22}m")
if underline is not None: if underline is not None:
bits.append("\033[{}m".format(4 if underline else 24)) bits.append(f"\033[{4 if underline else 24}m")
if overline is not None:
bits.append(f"\033[{53 if overline else 55}m")
if italic is not None:
bits.append(f"\033[{3 if italic else 23}m")
if blink is not None: if blink is not None:
bits.append("\033[{}m".format(5 if blink else 25)) bits.append(f"\033[{5 if blink else 25}m")
if reverse is not None: if reverse is not None:
bits.append("\033[{}m".format(7 if reverse else 27)) bits.append(f"\033[{7 if reverse else 27}m")
if strikethrough is not None:
bits.append(f"\033[{9 if strikethrough else 29}m")
bits.append(text) bits.append(text)
if reset: if reset:
bits.append(_ansi_reset_all) bits.append(_ansi_reset_all)
return "".join(bits) return "".join(bits)
def unstyle(text): def unstyle(text: str) -> str:
"""Removes ANSI styling information from a string. Usually it's not """Removes ANSI styling information from a string. Usually it's not
necessary to use this function as Click's echo function will necessary to use this function as Click's echo function will
automatically remove styling if necessary. automatically remove styling if necessary.
@ -531,7 +623,14 @@ def unstyle(text):
return strip_ansi(text) return strip_ansi(text)
def secho(message=None, file=None, nl=True, err=False, color=None, **styles): def secho(
message: t.Optional[t.Any] = None,
file: t.Optional[t.IO] = None,
nl: bool = True,
err: bool = False,
color: t.Optional[bool] = None,
**styles: t.Any,
) -> None:
"""This function combines :func:`echo` and :func:`style` into one """This function combines :func:`echo` and :func:`style` into one
call. As such the following two calls are the same:: call. As such the following two calls are the same::
@ -541,16 +640,31 @@ def secho(message=None, file=None, nl=True, err=False, color=None, **styles):
All keyword arguments are forwarded to the underlying functions All keyword arguments are forwarded to the underlying functions
depending on which one they go with. depending on which one they go with.
Non-string types will be converted to :class:`str`. However,
:class:`bytes` are passed directly to :meth:`echo` without applying
style. If you want to style bytes that represent text, call
:meth:`bytes.decode` first.
.. versionchanged:: 8.0
A non-string ``message`` is converted to a string. Bytes are
passed through without style applied.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
if message is not None: if message is not None and not isinstance(message, (bytes, bytearray)):
message = style(message, **styles) message = style(message, **styles)
return echo(message, file=file, nl=nl, err=err, color=color) return echo(message, file=file, nl=nl, err=err, color=color)
def edit( def edit(
text=None, editor=None, env=None, require_save=True, extension=".txt", filename=None text: t.Optional[t.AnyStr] = None,
): editor: t.Optional[str] = None,
env: t.Optional[t.Mapping[str, str]] = None,
require_save: bool = True,
extension: str = ".txt",
filename: t.Optional[str] = None,
) -> t.Optional[t.AnyStr]:
r"""Edits the given text in the defined editor. If an editor is given r"""Edits the given text in the defined editor. If an editor is given
(should be the full path to the executable but the regular operating (should be the full path to the executable but the regular operating
system search path is used for finding the executable) it overrides system search path is used for finding the executable) it overrides
@ -580,15 +694,16 @@ def edit(
""" """
from ._termui_impl import Editor from ._termui_impl import Editor
editor = Editor( ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension)
editor=editor, env=env, require_save=require_save, extension=extension
)
if filename is None: if filename is None:
return editor.edit(text) return ed.edit(text)
editor.edit_file(filename)
ed.edit_file(filename)
return None
def launch(url, wait=False, locate=False): def launch(url: str, wait: bool = False, locate: bool = False) -> int:
"""This function launches the given URL (or filename) in the default """This function launches the given URL (or filename) in the default
viewer application for this file type. If this is an executable, it viewer application for this file type. If this is an executable, it
might launch the executable in a new session. The return value is might launch the executable in a new session. The return value is
@ -603,7 +718,9 @@ def launch(url, wait=False, locate=False):
.. versionadded:: 2.0 .. versionadded:: 2.0
:param url: URL or filename of the thing to launch. :param url: URL or filename of the thing to launch.
:param wait: waits for the program to stop. :param wait: Wait for the program to exit before returning. This
only works if the launched program blocks. In particular,
``xdg-open`` on Linux does not block.
:param locate: if this is set to `True` then instead of launching the :param locate: if this is set to `True` then instead of launching the
application associated with the URL it will attempt to application associated with the URL it will attempt to
launch a file manager with the file located. This launch a file manager with the file located. This
@ -617,10 +734,10 @@ def launch(url, wait=False, locate=False):
# If this is provided, getchar() calls into this instead. This is used # If this is provided, getchar() calls into this instead. This is used
# for unittesting purposes. # for unittesting purposes.
_getchar = None _getchar: t.Optional[t.Callable[[bool], str]] = None
def getchar(echo=False): def getchar(echo: bool = False) -> str:
"""Fetches a single character from the terminal and returns it. This """Fetches a single character from the terminal and returns it. This
will always return a unicode character and under certain rare will always return a unicode character and under certain rare
circumstances this might return more than one character. The circumstances this might return more than one character. The
@ -640,19 +757,23 @@ def getchar(echo=False):
:param echo: if set to `True`, the character read will also show up on :param echo: if set to `True`, the character read will also show up on
the terminal. The default is to not show it. the terminal. The default is to not show it.
""" """
f = _getchar global _getchar
if f is None:
if _getchar is None:
from ._termui_impl import getchar as f from ._termui_impl import getchar as f
return f(echo)
_getchar = f
return _getchar(echo)
def raw_terminal(): def raw_terminal() -> t.ContextManager[int]:
from ._termui_impl import raw_terminal as f from ._termui_impl import raw_terminal as f
return f() return f()
def pause(info="Press any key to continue ...", err=False): def pause(info: t.Optional[str] = None, err: bool = False) -> None:
"""This command stops execution and waits for the user to press any """This command stops execution and waits for the user to press any
key to continue. This is similar to the Windows batch "pause" key to continue. This is similar to the Windows batch "pause"
command. If the program is not run through a terminal, this command command. If the program is not run through a terminal, this command
@ -663,12 +784,17 @@ def pause(info="Press any key to continue ...", err=False):
.. versionadded:: 4.0 .. versionadded:: 4.0
Added the `err` parameter. Added the `err` parameter.
:param info: the info string to print before pausing. :param info: The message to print before pausing. Defaults to
``"Press any key to continue..."``.
:param err: if set to message goes to ``stderr`` instead of :param err: if set to message goes to ``stderr`` instead of
``stdout``, the same as with echo. ``stdout``, the same as with echo.
""" """
if not isatty(sys.stdin) or not isatty(sys.stdout): if not isatty(sys.stdin) or not isatty(sys.stdout):
return return
if info is None:
info = _("Press any key to continue...")
try: try:
if info: if info:
echo(info, nl=False, err=err) echo(info, nl=False, err=err)

View file

@ -1,77 +1,117 @@
import contextlib import contextlib
import io
import os import os
import shlex import shlex
import shutil import shutil
import sys import sys
import tempfile import tempfile
import typing as t
from types import TracebackType
from . import formatting from . import formatting
from . import termui from . import termui
from . import utils from . import utils
from ._compat import iteritems from ._compat import _find_binary_reader
from ._compat import PY2
from ._compat import string_types if t.TYPE_CHECKING:
from .core import BaseCommand
if PY2: class EchoingStdin:
from cStringIO import StringIO def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None:
else:
import io
from ._compat import _find_binary_reader
class EchoingStdin(object):
def __init__(self, input, output):
self._input = input self._input = input
self._output = output self._output = output
self._paused = False
def __getattr__(self, x): def __getattr__(self, x: str) -> t.Any:
return getattr(self._input, x) return getattr(self._input, x)
def _echo(self, rv): def _echo(self, rv: bytes) -> bytes:
if not self._paused:
self._output.write(rv) self._output.write(rv)
return rv return rv
def read(self, n=-1): def read(self, n: int = -1) -> bytes:
return self._echo(self._input.read(n)) return self._echo(self._input.read(n))
def readline(self, n=-1): def read1(self, n: int = -1) -> bytes:
return self._echo(self._input.read1(n)) # type: ignore
def readline(self, n: int = -1) -> bytes:
return self._echo(self._input.readline(n)) return self._echo(self._input.readline(n))
def readlines(self): def readlines(self) -> t.List[bytes]:
return [self._echo(x) for x in self._input.readlines()] return [self._echo(x) for x in self._input.readlines()]
def __iter__(self): def __iter__(self) -> t.Iterator[bytes]:
return iter(self._echo(x) for x in self._input) return iter(self._echo(x) for x in self._input)
def __repr__(self): def __repr__(self) -> str:
return repr(self._input) return repr(self._input)
def make_input_stream(input, charset): @contextlib.contextmanager
def _pause_echo(stream: t.Optional[EchoingStdin]) -> t.Iterator[None]:
if stream is None:
yield
else:
stream._paused = True
yield
stream._paused = False
class _NamedTextIOWrapper(io.TextIOWrapper):
def __init__(
self, buffer: t.BinaryIO, name: str, mode: str, **kwargs: t.Any
) -> None:
super().__init__(buffer, **kwargs)
self._name = name
self._mode = mode
@property
def name(self) -> str:
return self._name
@property
def mode(self) -> str:
return self._mode
def make_input_stream(
input: t.Optional[t.Union[str, bytes, t.IO]], charset: str
) -> t.BinaryIO:
# Is already an input stream. # Is already an input stream.
if hasattr(input, "read"): if hasattr(input, "read"):
if PY2: rv = _find_binary_reader(t.cast(t.IO, input))
return input
rv = _find_binary_reader(input)
if rv is not None: if rv is not None:
return rv return rv
raise TypeError("Could not find binary reader for input stream.") raise TypeError("Could not find binary reader for input stream.")
if input is None: if input is None:
input = b"" input = b""
elif not isinstance(input, bytes): elif isinstance(input, str):
input = input.encode(charset) input = input.encode(charset)
if PY2:
return StringIO(input) return io.BytesIO(t.cast(bytes, input))
return io.BytesIO(input)
class Result(object): class Result:
"""Holds the captured result of an invoked CLI script.""" """Holds the captured result of an invoked CLI script."""
def __init__( def __init__(
self, runner, stdout_bytes, stderr_bytes, exit_code, exception, exc_info=None self,
runner: "CliRunner",
stdout_bytes: bytes,
stderr_bytes: t.Optional[bytes],
return_value: t.Any,
exit_code: int,
exception: t.Optional[BaseException],
exc_info: t.Optional[
t.Tuple[t.Type[BaseException], BaseException, TracebackType]
] = None,
): ):
#: The runner that created the result #: The runner that created the result
self.runner = runner self.runner = runner
@ -79,6 +119,10 @@ class Result(object):
self.stdout_bytes = stdout_bytes self.stdout_bytes = stdout_bytes
#: The standard error as bytes, or None if not available #: The standard error as bytes, or None if not available
self.stderr_bytes = stderr_bytes self.stderr_bytes = stderr_bytes
#: The value returned from the invoked command.
#:
#: .. versionadded:: 8.0
self.return_value = return_value
#: The exit code as integer. #: The exit code as integer.
self.exit_code = exit_code self.exit_code = exit_code
#: The exception that happened if one did. #: The exception that happened if one did.
@ -87,19 +131,19 @@ class Result(object):
self.exc_info = exc_info self.exc_info = exc_info
@property @property
def output(self): def output(self) -> str:
"""The (standard) output as unicode string.""" """The (standard) output as unicode string."""
return self.stdout return self.stdout
@property @property
def stdout(self): def stdout(self) -> str:
"""The standard output as unicode string.""" """The standard output as unicode string."""
return self.stdout_bytes.decode(self.runner.charset, "replace").replace( return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
"\r\n", "\n" "\r\n", "\n"
) )
@property @property
def stderr(self): def stderr(self) -> str:
"""The standard error as unicode string.""" """The standard error as unicode string."""
if self.stderr_bytes is None: if self.stderr_bytes is None:
raise ValueError("stderr not separately captured") raise ValueError("stderr not separately captured")
@ -107,21 +151,18 @@ class Result(object):
"\r\n", "\n" "\r\n", "\n"
) )
def __repr__(self): def __repr__(self) -> str:
return "<{} {}>".format( exc_str = repr(self.exception) if self.exception else "okay"
type(self).__name__, repr(self.exception) if self.exception else "okay" return f"<{type(self).__name__} {exc_str}>"
)
class CliRunner(object): class CliRunner:
"""The CLI runner provides functionality to invoke a Click command line """The CLI runner provides functionality to invoke a Click command line
script for unittesting purposes in a isolated environment. This only script for unittesting purposes in a isolated environment. This only
works in single-threaded systems without any concurrency as it changes the works in single-threaded systems without any concurrency as it changes the
global interpreter state. global interpreter state.
:param charset: the character set for the input and output data. This is :param charset: the character set for the input and output data.
UTF-8 by default and should not be changed currently as
the reporting to Click only works in Python 2 properly.
:param env: a dictionary with environment variables for overriding. :param env: a dictionary with environment variables for overriding.
:param echo_stdin: if this is set to `True`, then reading from stdin writes :param echo_stdin: if this is set to `True`, then reading from stdin writes
to stdout. This is useful for showing examples in to stdout. This is useful for showing examples in
@ -134,22 +175,28 @@ class CliRunner(object):
independently independently
""" """
def __init__(self, charset=None, env=None, echo_stdin=False, mix_stderr=True): def __init__(
if charset is None: self,
charset = "utf-8" charset: str = "utf-8",
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
echo_stdin: bool = False,
mix_stderr: bool = True,
) -> None:
self.charset = charset self.charset = charset
self.env = env or {} self.env = env or {}
self.echo_stdin = echo_stdin self.echo_stdin = echo_stdin
self.mix_stderr = mix_stderr self.mix_stderr = mix_stderr
def get_default_prog_name(self, cli): def get_default_prog_name(self, cli: "BaseCommand") -> str:
"""Given a command object it will return the default program name """Given a command object it will return the default program name
for it. The default is the `name` attribute or ``"root"`` if not for it. The default is the `name` attribute or ``"root"`` if not
set. set.
""" """
return cli.name or "root" return cli.name or "root"
def make_env(self, overrides=None): def make_env(
self, overrides: t.Optional[t.Mapping[str, t.Optional[str]]] = None
) -> t.Mapping[str, t.Optional[str]]:
"""Returns the environment overrides for invoking a script.""" """Returns the environment overrides for invoking a script."""
rv = dict(self.env) rv = dict(self.env)
if overrides: if overrides:
@ -157,7 +204,12 @@ class CliRunner(object):
return rv return rv
@contextlib.contextmanager @contextlib.contextmanager
def isolation(self, input=None, env=None, color=False): def isolation(
self,
input: t.Optional[t.Union[str, bytes, t.IO]] = None,
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
color: bool = False,
) -> t.Iterator[t.Tuple[io.BytesIO, t.Optional[io.BytesIO]]]:
"""A context manager that sets up the isolation for invoking of a """A context manager that sets up the isolation for invoking of a
command line tool. This sets up stdin with the given input data command line tool. This sets up stdin with the given input data
and `os.environ` with the overrides from the given dictionary. and `os.environ` with the overrides from the given dictionary.
@ -166,15 +218,20 @@ class CliRunner(object):
This is automatically done in the :meth:`invoke` method. This is automatically done in the :meth:`invoke` method.
.. versionadded:: 4.0
The ``color`` parameter was added.
:param input: the input stream to put into sys.stdin. :param input: the input stream to put into sys.stdin.
:param env: the environment overrides as dictionary. :param env: the environment overrides as dictionary.
:param color: whether the output should contain color codes. The :param color: whether the output should contain color codes. The
application can still override this explicitly. application can still override this explicitly.
.. versionchanged:: 8.0
``stderr`` is opened with ``errors="backslashreplace"``
instead of the default ``"strict"``.
.. versionchanged:: 4.0
Added the ``color`` parameter.
""" """
input = make_input_stream(input, self.charset) bytes_input = make_input_stream(input, self.charset)
echo_input = None
old_stdin = sys.stdin old_stdin = sys.stdin
old_stdout = sys.stdout old_stdout = sys.stdout
@ -184,51 +241,68 @@ class CliRunner(object):
env = self.make_env(env) env = self.make_env(env)
if PY2:
bytes_output = 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() bytes_output = io.BytesIO()
if self.echo_stdin:
input = EchoingStdin(input, bytes_output)
input = io.TextIOWrapper(input, encoding=self.charset)
sys.stdout = io.TextIOWrapper(bytes_output, encoding=self.charset)
if not self.mix_stderr:
bytes_error = io.BytesIO()
sys.stderr = io.TextIOWrapper(bytes_error, encoding=self.charset)
if self.echo_stdin:
bytes_input = echo_input = t.cast(
t.BinaryIO, EchoingStdin(bytes_input, bytes_output)
)
sys.stdin = text_input = _NamedTextIOWrapper(
bytes_input, encoding=self.charset, name="<stdin>", mode="r"
)
if self.echo_stdin:
# Force unbuffered reads, otherwise TextIOWrapper reads a
# large chunk which is echoed early.
text_input._CHUNK_SIZE = 1 # type: ignore
sys.stdout = _NamedTextIOWrapper(
bytes_output, encoding=self.charset, name="<stdout>", mode="w"
)
bytes_error = None
if self.mix_stderr: if self.mix_stderr:
sys.stderr = sys.stdout sys.stderr = sys.stdout
else:
bytes_error = io.BytesIO()
sys.stderr = _NamedTextIOWrapper(
bytes_error,
encoding=self.charset,
name="<stderr>",
mode="w",
errors="backslashreplace",
)
sys.stdin = input @_pause_echo(echo_input) # type: ignore
def visible_input(prompt: t.Optional[str] = None) -> str:
def visible_input(prompt=None):
sys.stdout.write(prompt or "") sys.stdout.write(prompt or "")
val = input.readline().rstrip("\r\n") val = text_input.readline().rstrip("\r\n")
sys.stdout.write("{}\n".format(val)) sys.stdout.write(f"{val}\n")
sys.stdout.flush() sys.stdout.flush()
return val return val
def hidden_input(prompt=None): @_pause_echo(echo_input) # type: ignore
sys.stdout.write("{}\n".format(prompt or "")) def hidden_input(prompt: t.Optional[str] = None) -> str:
sys.stdout.write(f"{prompt or ''}\n")
sys.stdout.flush() sys.stdout.flush()
return input.readline().rstrip("\r\n") return text_input.readline().rstrip("\r\n")
def _getchar(echo): @_pause_echo(echo_input) # type: ignore
def _getchar(echo: bool) -> str:
char = sys.stdin.read(1) char = sys.stdin.read(1)
if echo: if echo:
sys.stdout.write(char) sys.stdout.write(char)
sys.stdout.flush() sys.stdout.flush()
return char return char
default_color = color default_color = color
def should_strip_ansi(stream=None, color=None): def should_strip_ansi(
stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None
) -> bool:
if color is None: if color is None:
return not default_color return not default_color
return not color return not color
@ -236,15 +310,15 @@ class CliRunner(object):
old_visible_prompt_func = termui.visible_prompt_func old_visible_prompt_func = termui.visible_prompt_func
old_hidden_prompt_func = termui.hidden_prompt_func old_hidden_prompt_func = termui.hidden_prompt_func
old__getchar_func = termui._getchar old__getchar_func = termui._getchar
old_should_strip_ansi = utils.should_strip_ansi old_should_strip_ansi = utils.should_strip_ansi # type: ignore
termui.visible_prompt_func = visible_input termui.visible_prompt_func = visible_input
termui.hidden_prompt_func = hidden_input termui.hidden_prompt_func = hidden_input
termui._getchar = _getchar termui._getchar = _getchar
utils.should_strip_ansi = should_strip_ansi utils.should_strip_ansi = should_strip_ansi # type: ignore
old_env = {} old_env = {}
try: try:
for key, value in iteritems(env): for key, value in env.items():
old_env[key] = os.environ.get(key) old_env[key] = os.environ.get(key)
if value is None: if value is None:
try: try:
@ -253,9 +327,9 @@ class CliRunner(object):
pass pass
else: else:
os.environ[key] = value os.environ[key] = value
yield (bytes_output, not self.mix_stderr and bytes_error) yield (bytes_output, bytes_error)
finally: finally:
for key, value in iteritems(old_env): for key, value in old_env.items():
if value is None: if value is None:
try: try:
del os.environ[key] del os.environ[key]
@ -269,19 +343,19 @@ class CliRunner(object):
termui.visible_prompt_func = old_visible_prompt_func termui.visible_prompt_func = old_visible_prompt_func
termui.hidden_prompt_func = old_hidden_prompt_func termui.hidden_prompt_func = old_hidden_prompt_func
termui._getchar = old__getchar_func termui._getchar = old__getchar_func
utils.should_strip_ansi = old_should_strip_ansi utils.should_strip_ansi = old_should_strip_ansi # type: ignore
formatting.FORCED_WIDTH = old_forced_width formatting.FORCED_WIDTH = old_forced_width
def invoke( def invoke(
self, self,
cli, cli: "BaseCommand",
args=None, args: t.Optional[t.Union[str, t.Sequence[str]]] = None,
input=None, input: t.Optional[t.Union[str, bytes, t.IO]] = None,
env=None, env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
catch_exceptions=True, catch_exceptions: bool = True,
color=False, color: bool = False,
**extra **extra: t.Any,
): ) -> Result:
"""Invokes a command in an isolated environment. The arguments are """Invokes a command in an isolated environment. The arguments are
forwarded directly to the command line script, the `extra` keyword forwarded directly to the command line script, the `extra` keyword
arguments are passed to the :meth:`~clickpkg.Command.main` function of arguments are passed to the :meth:`~clickpkg.Command.main` function of
@ -289,16 +363,6 @@ class CliRunner(object):
This returns a :class:`Result` object. This returns a :class:`Result` object.
.. versionadded:: 3.0
The ``catch_exceptions`` parameter was added.
.. versionchanged:: 3.0
The result object now has an `exc_info` attribute with the
traceback if available.
.. versionadded:: 4.0
The ``color`` parameter was added.
:param cli: the command to invoke :param cli: the command to invoke
:param args: the arguments to invoke. It may be given as an iterable :param args: the arguments to invoke. It may be given as an iterable
or a string. When given as string it will be interpreted or a string. When given as string it will be interpreted
@ -311,13 +375,28 @@ class CliRunner(object):
:param extra: the keyword arguments to pass to :meth:`main`. :param extra: the keyword arguments to pass to :meth:`main`.
:param color: whether the output should contain color codes. The :param color: whether the output should contain color codes. The
application can still override this explicitly. application can still override this explicitly.
.. versionchanged:: 8.0
The result object has the ``return_value`` attribute with
the value returned from the invoked command.
.. versionchanged:: 4.0
Added the ``color`` parameter.
.. versionchanged:: 3.0
Added the ``catch_exceptions`` parameter.
.. versionchanged:: 3.0
The result object has the ``exc_info`` attribute with the
traceback if available.
""" """
exc_info = None exc_info = None
with self.isolation(input=input, env=env, color=color) as outstreams: with self.isolation(input=input, env=env, color=color) as outstreams:
exception = None return_value = None
exception: t.Optional[BaseException] = None
exit_code = 0 exit_code = 0
if isinstance(args, string_types): if isinstance(args, str):
args = shlex.split(args) args = shlex.split(args)
try: try:
@ -326,20 +405,23 @@ class CliRunner(object):
prog_name = self.get_default_prog_name(cli) prog_name = self.get_default_prog_name(cli)
try: try:
cli.main(args=args or (), prog_name=prog_name, **extra) return_value = cli.main(args=args or (), prog_name=prog_name, **extra)
except SystemExit as e: except SystemExit as e:
exc_info = sys.exc_info() exc_info = sys.exc_info()
exit_code = e.code e_code = t.cast(t.Optional[t.Union[int, t.Any]], e.code)
if exit_code is None:
exit_code = 0
if exit_code != 0: if e_code is None:
e_code = 0
if e_code != 0:
exception = e exception = e
if not isinstance(exit_code, int): if not isinstance(e_code, int):
sys.stdout.write(str(exit_code)) sys.stdout.write(str(e_code))
sys.stdout.write("\n") sys.stdout.write("\n")
exit_code = 1 e_code = 1
exit_code = e_code
except Exception as e: except Exception as e:
if not catch_exceptions: if not catch_exceptions:
@ -353,30 +435,45 @@ class CliRunner(object):
if self.mix_stderr: if self.mix_stderr:
stderr = None stderr = None
else: else:
stderr = outstreams[1].getvalue() stderr = outstreams[1].getvalue() # type: ignore
return Result( return Result(
runner=self, runner=self,
stdout_bytes=stdout, stdout_bytes=stdout,
stderr_bytes=stderr, stderr_bytes=stderr,
return_value=return_value,
exit_code=exit_code, exit_code=exit_code,
exception=exception, exception=exception,
exc_info=exc_info, exc_info=exc_info, # type: ignore
) )
@contextlib.contextmanager @contextlib.contextmanager
def isolated_filesystem(self): def isolated_filesystem(
"""A context manager that creates a temporary folder and changes self, temp_dir: t.Optional[t.Union[str, os.PathLike]] = None
the current working directory to it for isolated filesystem tests. ) -> t.Iterator[str]:
"""A context manager that creates a temporary directory and
changes the current working directory to it. This isolates tests
that affect the contents of the CWD to prevent them from
interfering with each other.
:param temp_dir: Create the temporary directory under this
directory. If given, the created directory is not removed
when exiting.
.. versionchanged:: 8.0
Added the ``temp_dir`` parameter.
""" """
cwd = os.getcwd() cwd = os.getcwd()
t = tempfile.mkdtemp() t = tempfile.mkdtemp(dir=temp_dir)
os.chdir(t) os.chdir(t)
try: try:
yield t yield t
finally: finally:
os.chdir(cwd) os.chdir(cwd)
if temp_dir is None:
try: try:
shutil.rmtree(t) shutil.rmtree(t)
except (OSError, IOError): # noqa: B014 except OSError: # noqa: B014
pass pass

View file

@ -1,37 +1,47 @@
import os import os
import stat import stat
import typing as t
from datetime import datetime from datetime import datetime
from gettext import gettext as _
from gettext import ngettext
from ._compat import _get_argv_encoding from ._compat import _get_argv_encoding
from ._compat import filename_to_ui
from ._compat import get_filesystem_encoding from ._compat import get_filesystem_encoding
from ._compat import get_streerror
from ._compat import open_stream from ._compat import open_stream
from ._compat import PY2
from ._compat import text_type
from .exceptions import BadParameter from .exceptions import BadParameter
from .utils import LazyFile from .utils import LazyFile
from .utils import safecall from .utils import safecall
if t.TYPE_CHECKING:
import typing_extensions as te
from .core import Context
from .core import Parameter
from .shell_completion import CompletionItem
class ParamType(object):
"""Helper for converting values through types. The following is
necessary for a valid type:
* it needs a name class ParamType:
* it needs to pass through None unchanged """Represents the type of a parameter. Validates and converts values
* it needs to convert from a string from the command line or Python into the correct type.
* it needs to convert its result type through unchanged
(eg: needs to be idempotent) To implement a custom type, subclass and implement at least the
* it needs to be able to deal with param and context being `None`. following:
This can be the case when the object is used with prompt
inputs. - The :attr:`name` class attribute must be set.
- Calling an instance of the type with ``None`` must return
``None``. This is already implemented by default.
- :meth:`convert` must convert string values to the correct type.
- :meth:`convert` must accept values that are already the correct
type.
- It must be able to convert a value if the ``ctx`` and ``param``
arguments are ``None``. This can occur when converting prompt
input.
""" """
is_composite = False is_composite: t.ClassVar[bool] = False
arity: t.ClassVar[int] = 1
#: the descriptive name of this type #: the descriptive name of this type
name = None name: str
#: if a list of this type is expected and the value is pulled from a #: if a list of this type is expected and the value is pulled from a
#: string environment variable, this is what splits it up. `None` #: string environment variable, this is what splits it up. `None`
@ -39,29 +49,66 @@ class ParamType(object):
#: whitespace splits them up. The exception are paths and files which #: whitespace splits them up. The exception are paths and files which
#: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
#: Windows). #: Windows).
envvar_list_splitter = None envvar_list_splitter: t.ClassVar[t.Optional[str]] = None
def __call__(self, value, param=None, ctx=None): def to_info_dict(self) -> t.Dict[str, t.Any]:
"""Gather information that could be useful for a tool generating
user-facing documentation.
Use :meth:`click.Context.to_info_dict` to traverse the entire
CLI structure.
.. versionadded:: 8.0
"""
# The class name without the "ParamType" suffix.
param_type = type(self).__name__.partition("ParamType")[0]
param_type = param_type.partition("ParameterType")[0]
return {"param_type": param_type, "name": self.name}
def __call__(
self,
value: t.Any,
param: t.Optional["Parameter"] = None,
ctx: t.Optional["Context"] = None,
) -> t.Any:
if value is not None: if value is not None:
return self.convert(value, param, ctx) return self.convert(value, param, ctx)
def get_metavar(self, param): def get_metavar(self, param: "Parameter") -> t.Optional[str]:
"""Returns the metavar default for this param if it provides one.""" """Returns the metavar default for this param if it provides one."""
def get_missing_message(self, param): def get_missing_message(self, param: "Parameter") -> t.Optional[str]:
"""Optionally might return extra information about a missing """Optionally might return extra information about a missing
parameter. parameter.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
def convert(self, value, param, ctx): def convert(
"""Converts the value. This is not invoked for values that are self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
`None` (the missing value). ) -> t.Any:
"""Convert the value to the correct type. This is not called if
the value is ``None`` (the missing value).
This must accept string values from the command line, as well as
values that are already the correct type. It may also convert
other compatible types.
The ``param`` and ``ctx`` arguments may be ``None`` in certain
situations, such as when converting prompt input.
If the value cannot be converted, call :meth:`fail` with a
descriptive message.
:param value: The value to convert.
:param param: The parameter that is using this type to convert
its value. May be ``None``.
:param ctx: The current context that arrived at this value. May
be ``None``.
""" """
return value return value
def split_envvar_value(self, rv): def split_envvar_value(self, rv: str) -> t.Sequence[str]:
"""Given a value from an environment variable this splits it up """Given a value from an environment variable this splits it up
into small chunks depending on the defined envvar list splitter. into small chunks depending on the defined envvar list splitter.
@ -71,49 +118,83 @@ class ParamType(object):
""" """
return (rv or "").split(self.envvar_list_splitter) return (rv or "").split(self.envvar_list_splitter)
def fail(self, message, param=None, ctx=None): def fail(
self,
message: str,
param: t.Optional["Parameter"] = None,
ctx: t.Optional["Context"] = None,
) -> "t.NoReturn":
"""Helper method to fail with an invalid value message.""" """Helper method to fail with an invalid value message."""
raise BadParameter(message, ctx=ctx, param=param) raise BadParameter(message, ctx=ctx, param=param)
def shell_complete(
self, ctx: "Context", param: "Parameter", incomplete: str
) -> t.List["CompletionItem"]:
"""Return a list of
:class:`~click.shell_completion.CompletionItem` objects for the
incomplete value. Most types do not provide completions, but
some do, and this allows custom types to provide custom
completions as well.
:param ctx: Invocation context for this command.
:param param: The parameter that is requesting completion.
:param incomplete: Value being completed. May be empty.
.. versionadded:: 8.0
"""
return []
class CompositeParamType(ParamType): class CompositeParamType(ParamType):
is_composite = True is_composite = True
@property @property
def arity(self): def arity(self) -> int: # type: ignore
raise NotImplementedError() raise NotImplementedError()
class FuncParamType(ParamType): class FuncParamType(ParamType):
def __init__(self, func): def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None:
self.name = func.__name__ self.name = func.__name__
self.func = func self.func = func
def convert(self, value, param, ctx): def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
info_dict["func"] = self.func
return info_dict
def convert(
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
) -> t.Any:
try: try:
return self.func(value) return self.func(value)
except ValueError: except ValueError:
try: try:
value = text_type(value) value = str(value)
except UnicodeError: except UnicodeError:
value = str(value).decode("utf-8", "replace") value = value.decode("utf-8", "replace")
self.fail(value, param, ctx) self.fail(value, param, ctx)
class UnprocessedParamType(ParamType): class UnprocessedParamType(ParamType):
name = "text" name = "text"
def convert(self, value, param, ctx): def convert(
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
) -> t.Any:
return value return value
def __repr__(self): def __repr__(self) -> str:
return "UNPROCESSED" return "UNPROCESSED"
class StringParamType(ParamType): class StringParamType(ParamType):
name = "text" name = "text"
def convert(self, value, param, ctx): def convert(
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
) -> t.Any:
if isinstance(value, bytes): if isinstance(value, bytes):
enc = _get_argv_encoding() enc = _get_argv_encoding()
try: try:
@ -128,9 +209,9 @@ class StringParamType(ParamType):
else: else:
value = value.decode("utf-8", "replace") value = value.decode("utf-8", "replace")
return value return value
return value return str(value)
def __repr__(self): def __repr__(self) -> str:
return "STRING" return "STRING"
@ -153,17 +234,32 @@ class Choice(ParamType):
name = "choice" name = "choice"
def __init__(self, choices, case_sensitive=True): def __init__(self, choices: t.Sequence[str], case_sensitive: bool = True) -> None:
self.choices = choices self.choices = choices
self.case_sensitive = case_sensitive self.case_sensitive = case_sensitive
def get_metavar(self, param): def to_info_dict(self) -> t.Dict[str, t.Any]:
return "[{}]".format("|".join(self.choices)) info_dict = super().to_info_dict()
info_dict["choices"] = self.choices
info_dict["case_sensitive"] = self.case_sensitive
return info_dict
def get_missing_message(self, param): def get_metavar(self, param: "Parameter") -> str:
return "Choose from:\n\t{}.".format(",\n\t".join(self.choices)) choices_str = "|".join(self.choices)
def convert(self, value, param, ctx): # Use curly braces to indicate a required argument.
if param.required and param.param_type_name == "argument":
return f"{{{choices_str}}}"
# Use square braces to indicate an option or optional argument.
return f"[{choices_str}]"
def get_missing_message(self, param: "Parameter") -> str:
return _("Choose from:\n\t{choices}").format(choices=",\n\t".join(self.choices))
def convert(
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
) -> t.Any:
# Match through normalization and case sensitivity # Match through normalization and case sensitivity
# first do token_normalize_func, then lowercase # first do token_normalize_func, then lowercase
# preserve original `value` to produce an accurate message in # preserve original `value` to produce an accurate message in
@ -179,30 +275,51 @@ class Choice(ParamType):
} }
if not self.case_sensitive: if not self.case_sensitive:
if PY2: normed_value = normed_value.casefold()
lower = str.lower
else:
lower = str.casefold
normed_value = lower(normed_value)
normed_choices = { normed_choices = {
lower(normed_choice): original normed_choice.casefold(): original
for normed_choice, original in normed_choices.items() for normed_choice, original in normed_choices.items()
} }
if normed_value in normed_choices: if normed_value in normed_choices:
return normed_choices[normed_value] return normed_choices[normed_value]
choices_str = ", ".join(map(repr, self.choices))
self.fail( self.fail(
"invalid choice: {}. (choose from {})".format( ngettext(
value, ", ".join(self.choices) "{value!r} is not {choice}.",
), "{value!r} is not one of {choices}.",
len(self.choices),
).format(value=value, choice=choices_str, choices=choices_str),
param, param,
ctx, ctx,
) )
def __repr__(self): def __repr__(self) -> str:
return "Choice('{}')".format(list(self.choices)) return f"Choice({list(self.choices)})"
def shell_complete(
self, ctx: "Context", param: "Parameter", incomplete: str
) -> t.List["CompletionItem"]:
"""Complete choices that start with the incomplete value.
:param ctx: Invocation context for this command.
:param param: The parameter that is requesting completion.
:param incomplete: Value being completed. May be empty.
.. versionadded:: 8.0
"""
from click.shell_completion import CompletionItem
str_choices = map(str, self.choices)
if self.case_sensitive:
matched = (c for c in str_choices if c.startswith(incomplete))
else:
incomplete = incomplete.lower()
matched = (c for c in str_choices if c.lower().startswith(incomplete))
return [CompletionItem(c) for c in matched]
class DateTime(ParamType): class DateTime(ParamType):
@ -228,212 +345,285 @@ class DateTime(ParamType):
name = "datetime" name = "datetime"
def __init__(self, formats=None): def __init__(self, formats: t.Optional[t.Sequence[str]] = None):
self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
def get_metavar(self, param): def to_info_dict(self) -> t.Dict[str, t.Any]:
return "[{}]".format("|".join(self.formats)) info_dict = super().to_info_dict()
info_dict["formats"] = self.formats
return info_dict
def _try_to_convert_date(self, value, format): def get_metavar(self, param: "Parameter") -> str:
return f"[{'|'.join(self.formats)}]"
def _try_to_convert_date(self, value: t.Any, format: str) -> t.Optional[datetime]:
try: try:
return datetime.strptime(value, format) return datetime.strptime(value, format)
except ValueError: except ValueError:
return None return None
def convert(self, value, param, ctx): def convert(
# Exact match self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
) -> t.Any:
if isinstance(value, datetime):
return value
for format in self.formats: for format in self.formats:
dtime = self._try_to_convert_date(value, format) converted = self._try_to_convert_date(value, format)
if dtime:
return dtime
if converted is not None:
return converted
formats_str = ", ".join(map(repr, self.formats))
self.fail( self.fail(
"invalid datetime format: {}. (choose from {})".format( ngettext(
value, ", ".join(self.formats) "{value!r} does not match the format {format}.",
) "{value!r} does not match the formats {formats}.",
len(self.formats),
).format(value=value, format=formats_str, formats=formats_str),
param,
ctx,
) )
def __repr__(self): def __repr__(self) -> str:
return "DateTime" return "DateTime"
class IntParamType(ParamType): class _NumberParamTypeBase(ParamType):
name = "integer" _number_class: t.ClassVar[t.Type]
def convert(self, value, param, ctx): def convert(
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
) -> t.Any:
try: try:
return int(value) return self._number_class(value)
except ValueError: except ValueError:
self.fail("{} is not a valid integer".format(value), param, ctx) self.fail(
_("{value!r} is not a valid {number_type}.").format(
value=value, number_type=self.name
),
param,
ctx,
)
def __repr__(self):
class _NumberRangeBase(_NumberParamTypeBase):
def __init__(
self,
min: t.Optional[float] = None,
max: t.Optional[float] = None,
min_open: bool = False,
max_open: bool = False,
clamp: bool = False,
) -> None:
self.min = min
self.max = max
self.min_open = min_open
self.max_open = max_open
self.clamp = clamp
def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
info_dict.update(
min=self.min,
max=self.max,
min_open=self.min_open,
max_open=self.max_open,
clamp=self.clamp,
)
return info_dict
def convert(
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
) -> t.Any:
import operator
rv = super().convert(value, param, ctx)
lt_min: bool = self.min is not None and (
operator.le if self.min_open else operator.lt
)(rv, self.min)
gt_max: bool = self.max is not None and (
operator.ge if self.max_open else operator.gt
)(rv, self.max)
if self.clamp:
if lt_min:
return self._clamp(self.min, 1, self.min_open) # type: ignore
if gt_max:
return self._clamp(self.max, -1, self.max_open) # type: ignore
if lt_min or gt_max:
self.fail(
_("{value} is not in the range {range}.").format(
value=rv, range=self._describe_range()
),
param,
ctx,
)
return rv
def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float:
"""Find the valid value to clamp to bound in the given
direction.
:param bound: The boundary value.
:param dir: 1 or -1 indicating the direction to move.
:param open: If true, the range does not include the bound.
"""
raise NotImplementedError
def _describe_range(self) -> str:
"""Describe the range for use in help text."""
if self.min is None:
op = "<" if self.max_open else "<="
return f"x{op}{self.max}"
if self.max is None:
op = ">" if self.min_open else ">="
return f"x{op}{self.min}"
lop = "<" if self.min_open else "<="
rop = "<" if self.max_open else "<="
return f"{self.min}{lop}x{rop}{self.max}"
def __repr__(self) -> str:
clamp = " clamped" if self.clamp else ""
return f"<{type(self).__name__} {self._describe_range()}{clamp}>"
class IntParamType(_NumberParamTypeBase):
name = "integer"
_number_class = int
def __repr__(self) -> str:
return "INT" return "INT"
class IntRange(IntParamType): class IntRange(_NumberRangeBase, IntParamType):
"""A parameter that works similar to :data:`click.INT` but restricts """Restrict an :data:`click.INT` value to a range of accepted
the value to fit into a range. The default behavior is to fail if the values. See :ref:`ranges`.
value falls outside the range, but it can also be silently clamped
between the two edges.
See :ref:`ranges` for an example. If ``min`` or ``max`` are not passed, any value is accepted in that
direction. If ``min_open`` or ``max_open`` are enabled, the
corresponding boundary is not included in the range.
If ``clamp`` is enabled, a value outside the range is clamped to the
boundary instead of failing.
.. versionchanged:: 8.0
Added the ``min_open`` and ``max_open`` parameters.
""" """
name = "integer range" name = "integer range"
def __init__(self, min=None, max=None, clamp=False): def _clamp( # type: ignore
self.min = min self, bound: int, dir: "te.Literal[1, -1]", open: bool
self.max = max ) -> int:
self.clamp = clamp if not open:
return bound
def convert(self, value, param, ctx): return bound + dir
rv = IntParamType.convert(self, value, param, ctx)
if self.clamp:
if self.min is not None and rv < self.min:
return self.min
if self.max is not None and rv > self.max:
return self.max
if (
self.min is not None
and rv < self.min
or self.max is not None
and rv > self.max
):
if self.min is None:
self.fail(
"{} is bigger than the maximum valid value {}.".format(
rv, self.max
),
param,
ctx,
)
elif self.max is None:
self.fail(
"{} is smaller than the minimum valid value {}.".format(
rv, self.min
),
param,
ctx,
)
else:
self.fail(
"{} is not in the valid range of {} to {}.".format(
rv, self.min, self.max
),
param,
ctx,
)
return rv
def __repr__(self):
return "IntRange({}, {})".format(self.min, self.max)
class FloatParamType(ParamType): class FloatParamType(_NumberParamTypeBase):
name = "float" name = "float"
_number_class = float
def convert(self, value, param, ctx): def __repr__(self) -> str:
try:
return float(value)
except ValueError:
self.fail(
"{} is not a valid floating point value".format(value), param, ctx
)
def __repr__(self):
return "FLOAT" return "FLOAT"
class FloatRange(FloatParamType): class FloatRange(_NumberRangeBase, FloatParamType):
"""A parameter that works similar to :data:`click.FLOAT` but restricts """Restrict a :data:`click.FLOAT` value to a range of accepted
the value to fit into a range. The default behavior is to fail if the values. See :ref:`ranges`.
value falls outside the range, but it can also be silently clamped
between the two edges.
See :ref:`ranges` for an example. If ``min`` or ``max`` are not passed, any value is accepted in that
direction. If ``min_open`` or ``max_open`` are enabled, the
corresponding boundary is not included in the range.
If ``clamp`` is enabled, a value outside the range is clamped to the
boundary instead of failing. This is not supported if either
boundary is marked ``open``.
.. versionchanged:: 8.0
Added the ``min_open`` and ``max_open`` parameters.
""" """
name = "float range" name = "float range"
def __init__(self, min=None, max=None, clamp=False): def __init__(
self.min = min self,
self.max = max min: t.Optional[float] = None,
self.clamp = clamp max: t.Optional[float] = None,
min_open: bool = False,
max_open: bool = False,
clamp: bool = False,
) -> None:
super().__init__(
min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp
)
def convert(self, value, param, ctx): if (min_open or max_open) and clamp:
rv = FloatParamType.convert(self, value, param, ctx) raise TypeError("Clamping is not supported for open bounds.")
if self.clamp:
if self.min is not None and rv < self.min:
return self.min
if self.max is not None and rv > self.max:
return self.max
if (
self.min is not None
and rv < self.min
or self.max is not None
and rv > self.max
):
if self.min is None:
self.fail(
"{} is bigger than the maximum valid value {}.".format(
rv, self.max
),
param,
ctx,
)
elif self.max is None:
self.fail(
"{} is smaller than the minimum valid value {}.".format(
rv, self.min
),
param,
ctx,
)
else:
self.fail(
"{} is not in the valid range of {} to {}.".format(
rv, self.min, self.max
),
param,
ctx,
)
return rv
def __repr__(self): def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float:
return "FloatRange({}, {})".format(self.min, self.max) if not open:
return bound
# Could use Python 3.9's math.nextafter here, but clamping an
# open float range doesn't seem to be particularly useful. It's
# left up to the user to write a callback to do it if needed.
raise RuntimeError("Clamping is not supported for open bounds.")
class BoolParamType(ParamType): class BoolParamType(ParamType):
name = "boolean" name = "boolean"
def convert(self, value, param, ctx): def convert(
if isinstance(value, bool): self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
) -> t.Any:
if value in {False, True}:
return bool(value) return bool(value)
value = value.lower()
if value in ("true", "t", "1", "yes", "y"):
return True
elif value in ("false", "f", "0", "no", "n"):
return False
self.fail("{} is not a valid boolean".format(value), param, ctx)
def __repr__(self): norm = value.strip().lower()
if norm in {"1", "true", "t", "yes", "y", "on"}:
return True
if norm in {"0", "false", "f", "no", "n", "off"}:
return False
self.fail(
_("{value!r} is not a valid boolean.").format(value=value), param, ctx
)
def __repr__(self) -> str:
return "BOOL" return "BOOL"
class UUIDParameterType(ParamType): class UUIDParameterType(ParamType):
name = "uuid" name = "uuid"
def convert(self, value, param, ctx): def convert(
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
) -> t.Any:
import uuid import uuid
if isinstance(value, uuid.UUID):
return value
value = value.strip()
try: try:
if PY2 and isinstance(value, text_type):
value = value.encode("ascii")
return uuid.UUID(value) return uuid.UUID(value)
except ValueError: except ValueError:
self.fail("{} is not a valid UUID value".format(value), param, ctx) self.fail(
_("{value!r} is not a valid UUID.").format(value=value), param, ctx
)
def __repr__(self): def __repr__(self) -> str:
return "UUID" return "UUID"
@ -468,15 +658,25 @@ class File(ParamType):
envvar_list_splitter = os.path.pathsep envvar_list_splitter = os.path.pathsep
def __init__( def __init__(
self, mode="r", encoding=None, errors="strict", lazy=None, atomic=False self,
): mode: str = "r",
encoding: t.Optional[str] = None,
errors: t.Optional[str] = "strict",
lazy: t.Optional[bool] = None,
atomic: bool = False,
) -> None:
self.mode = mode self.mode = mode
self.encoding = encoding self.encoding = encoding
self.errors = errors self.errors = errors
self.lazy = lazy self.lazy = lazy
self.atomic = atomic self.atomic = atomic
def resolve_lazy_flag(self, value): def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
info_dict.update(mode=self.mode, encoding=self.encoding)
return info_dict
def resolve_lazy_flag(self, value: t.Any) -> bool:
if self.lazy is not None: if self.lazy is not None:
return self.lazy return self.lazy
if value == "-": if value == "-":
@ -485,7 +685,9 @@ class File(ParamType):
return True return True
return False return False
def convert(self, value, param, ctx): def convert(
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
) -> t.Any:
try: try:
if hasattr(value, "read") or hasattr(value, "write"): if hasattr(value, "read") or hasattr(value, "write"):
return value return value
@ -493,16 +695,22 @@ class File(ParamType):
lazy = self.resolve_lazy_flag(value) lazy = self.resolve_lazy_flag(value)
if lazy: if lazy:
f = LazyFile( f: t.IO = t.cast(
t.IO,
LazyFile(
value, self.mode, self.encoding, self.errors, atomic=self.atomic value, self.mode, self.encoding, self.errors, atomic=self.atomic
),
) )
if ctx is not None: if ctx is not None:
ctx.call_on_close(f.close_intelligently) ctx.call_on_close(f.close_intelligently) # type: ignore
return f return f
f, should_close = open_stream( f, should_close = open_stream(
value, self.mode, self.encoding, self.errors, atomic=self.atomic value, self.mode, self.encoding, self.errors, atomic=self.atomic
) )
# If a context is provided, we automatically close the file # If a context is provided, we automatically close the file
# at the end of the context execution (or flush out). If a # at the end of the context execution (or flush out). If a
# context does not exist, it's the caller's responsibility to # context does not exist, it's the caller's responsibility to
@ -513,15 +721,26 @@ class File(ParamType):
ctx.call_on_close(safecall(f.close)) ctx.call_on_close(safecall(f.close))
else: else:
ctx.call_on_close(safecall(f.flush)) ctx.call_on_close(safecall(f.flush))
return f return f
except (IOError, OSError) as e: # noqa: B014 except OSError as e: # noqa: B014
self.fail( self.fail(f"{os.fsdecode(value)!r}: {e.strerror}", param, ctx)
"Could not open file: {}: {}".format(
filename_to_ui(value), get_streerror(e) def shell_complete(
), self, ctx: "Context", param: "Parameter", incomplete: str
param, ) -> t.List["CompletionItem"]:
ctx, """Return a special completion marker that tells the completion
) system to use the shell to provide file path completions.
:param ctx: Invocation context for this command.
:param param: The parameter that is requesting completion.
:param incomplete: Value being completed. May be empty.
.. versionadded:: 8.0
"""
from click.shell_completion import CompletionItem
return [CompletionItem(incomplete, type="file")]
class Path(ParamType): class Path(ParamType):
@ -530,9 +749,6 @@ class Path(ParamType):
handle it returns just the filename. Secondly, it can perform various handle it returns just the filename. Secondly, it can perform various
basic checks about what the file or directory should be. basic checks about what the file or directory should be.
.. versionchanged:: 6.0
`allow_dash` was added.
:param exists: if set to true, the file or directory needs to exist for :param exists: if set to true, the file or directory needs to exist for
this value to be valid. If this is not required and a this value to be valid. If this is not required and a
file does indeed not exist, then all further checks are file does indeed not exist, then all further checks are
@ -548,25 +764,29 @@ class Path(ParamType):
supposed to be done by the shell only. supposed to be done by the shell only.
:param allow_dash: If this is set to `True`, a single dash to indicate :param allow_dash: If this is set to `True`, a single dash to indicate
standard streams is permitted. standard streams is permitted.
:param path_type: optionally a string type that should be used to :param path_type: Convert the incoming path value to this type. If
represent the path. The default is `None` which ``None``, keep Python's default, which is ``str``. Useful to
means the return value will be either bytes or convert to :class:`pathlib.Path`.
unicode depending on what makes most sense given the
input data Click deals with. .. versionchanged:: 8.0
Allow passing ``type=pathlib.Path``.
.. versionchanged:: 6.0
Added the ``allow_dash`` parameter.
""" """
envvar_list_splitter = os.path.pathsep envvar_list_splitter = os.path.pathsep
def __init__( def __init__(
self, self,
exists=False, exists: bool = False,
file_okay=True, file_okay: bool = True,
dir_okay=True, dir_okay: bool = True,
writable=False, writable: bool = False,
readable=True, readable: bool = True,
resolve_path=False, resolve_path: bool = False,
allow_dash=False, allow_dash: bool = False,
path_type=None, path_type: t.Optional[t.Type] = None,
): ):
self.exists = exists self.exists = exists
self.file_okay = file_okay self.file_okay = file_okay
@ -578,31 +798,58 @@ class Path(ParamType):
self.type = path_type self.type = path_type
if self.file_okay and not self.dir_okay: if self.file_okay and not self.dir_okay:
self.name = "file" self.name = _("file")
self.path_type = "File"
elif self.dir_okay and not self.file_okay: elif self.dir_okay and not self.file_okay:
self.name = "directory" self.name = _("directory")
self.path_type = "Directory"
else: else:
self.name = "path" self.name = _("path")
self.path_type = "Path"
def coerce_path_result(self, rv): def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
info_dict.update(
exists=self.exists,
file_okay=self.file_okay,
dir_okay=self.dir_okay,
writable=self.writable,
readable=self.readable,
allow_dash=self.allow_dash,
)
return info_dict
def coerce_path_result(self, rv: t.Any) -> t.Any:
if self.type is not None and not isinstance(rv, self.type): if self.type is not None and not isinstance(rv, self.type):
if self.type is text_type: if self.type is str:
rv = rv.decode(get_filesystem_encoding()) rv = os.fsdecode(rv)
elif self.type is bytes:
rv = os.fsencode(rv)
else: else:
rv = rv.encode(get_filesystem_encoding()) rv = self.type(rv)
return rv return rv
def convert(self, value, param, ctx): def convert(
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
) -> t.Any:
rv = value rv = value
is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-") is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
if not is_dash: if not is_dash:
if self.resolve_path: if self.resolve_path:
rv = os.path.realpath(rv) # Get the absolute directory containing the path.
dir_ = os.path.dirname(os.path.abspath(rv))
# Resolve a symlink. realpath on Windows Python < 3.9
# doesn't resolve symlinks. This might return a relative
# path even if the path to the link is absolute.
if os.path.islink(rv):
rv = os.readlink(rv)
# Join dir_ with the resolved symlink if the resolved
# path is relative. This will make it relative to the
# original containing directory.
if not os.path.isabs(rv):
rv = os.path.join(dir_, rv)
try: try:
st = os.stat(rv) st = os.stat(rv)
@ -610,8 +857,8 @@ class Path(ParamType):
if not self.exists: if not self.exists:
return self.coerce_path_result(rv) return self.coerce_path_result(rv)
self.fail( self.fail(
"{} '{}' does not exist.".format( _("{name} {filename!r} does not exist.").format(
self.path_type, filename_to_ui(value) name=self.name.title(), filename=os.fsdecode(value)
), ),
param, param,
ctx, ctx,
@ -619,30 +866,32 @@ class Path(ParamType):
if not self.file_okay and stat.S_ISREG(st.st_mode): if not self.file_okay and stat.S_ISREG(st.st_mode):
self.fail( self.fail(
"{} '{}' is a file.".format(self.path_type, filename_to_ui(value)), _("{name} {filename!r} is a file.").format(
name=self.name.title(), filename=os.fsdecode(value)
),
param, param,
ctx, ctx,
) )
if not self.dir_okay and stat.S_ISDIR(st.st_mode): if not self.dir_okay and stat.S_ISDIR(st.st_mode):
self.fail( self.fail(
"{} '{}' is a directory.".format( _("{name} {filename!r} is a directory.").format(
self.path_type, filename_to_ui(value) name=self.name.title(), filename=os.fsdecode(value)
), ),
param, param,
ctx, ctx,
) )
if self.writable and not os.access(value, os.W_OK): if self.writable and not os.access(rv, os.W_OK):
self.fail( self.fail(
"{} '{}' is not writable.".format( _("{name} {filename!r} is not writable.").format(
self.path_type, filename_to_ui(value) name=self.name.title(), filename=os.fsdecode(value)
), ),
param, param,
ctx, ctx,
) )
if self.readable and not os.access(value, os.R_OK): if self.readable and not os.access(rv, os.R_OK):
self.fail( self.fail(
"{} '{}' is not readable.".format( _("{name} {filename!r} is not readable.").format(
self.path_type, filename_to_ui(value) name=self.name.title(), filename=os.fsdecode(value)
), ),
param, param,
ctx, ctx,
@ -650,6 +899,24 @@ class Path(ParamType):
return self.coerce_path_result(rv) return self.coerce_path_result(rv)
def shell_complete(
self, ctx: "Context", param: "Parameter", incomplete: str
) -> t.List["CompletionItem"]:
"""Return a special completion marker that tells the completion
system to use the shell to provide path completions for only
directories or any paths.
:param ctx: Invocation context for this command.
:param param: The parameter that is requesting completion.
:param incomplete: Value being completed. May be empty.
.. versionadded:: 8.0
"""
from click.shell_completion import CompletionItem
type = "dir" if self.dir_okay and not self.file_okay else "file"
return [CompletionItem(incomplete, type=type)]
class Tuple(CompositeParamType): class Tuple(CompositeParamType):
"""The default behavior of Click is to apply a type on a value directly. """The default behavior of Click is to apply a type on a value directly.
@ -665,75 +932,107 @@ class Tuple(CompositeParamType):
:param types: a list of types that should be used for the tuple items. :param types: a list of types that should be used for the tuple items.
""" """
def __init__(self, types): def __init__(self, types: t.Sequence[t.Union[t.Type, ParamType]]) -> None:
self.types = [convert_type(ty) for ty in types] self.types = [convert_type(ty) for ty in types]
@property def to_info_dict(self) -> t.Dict[str, t.Any]:
def name(self): info_dict = super().to_info_dict()
return "<{}>".format(" ".join(ty.name for ty in self.types)) info_dict["types"] = [t.to_info_dict() for t in self.types]
return info_dict
@property @property
def arity(self): def name(self) -> str: # type: ignore
return f"<{' '.join(ty.name for ty in self.types)}>"
@property
def arity(self) -> int: # type: ignore
return len(self.types) return len(self.types)
def convert(self, value, param, ctx): def convert(
if len(value) != len(self.types): self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
raise TypeError( ) -> t.Any:
"It would appear that nargs is set to conflict with the" len_type = len(self.types)
" composite type arity." len_value = len(value)
if len_value != len_type:
self.fail(
ngettext(
"{len_type} values are required, but {len_value} was given.",
"{len_type} values are required, but {len_value} were given.",
len_value,
).format(len_type=len_type, len_value=len_value),
param=param,
ctx=ctx,
) )
return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value)) return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value))
def convert_type(ty, default=None): def convert_type(ty: t.Optional[t.Any], default: t.Optional[t.Any] = None) -> ParamType:
"""Converts a callable or python type into the most appropriate """Find the most appropriate :class:`ParamType` for the given Python
param type. type. If the type isn't provided, it can be inferred from a default
value.
""" """
guessed_type = False guessed_type = False
if ty is None and default is not None: if ty is None and default is not None:
if isinstance(default, tuple): if isinstance(default, (tuple, list)):
ty = tuple(map(type, default)) # If the default is empty, ty will remain None and will
# return STRING.
if default:
item = default[0]
# A tuple of tuples needs to detect the inner types.
# Can't call convert recursively because that would
# incorrectly unwind the tuple to a single type.
if isinstance(item, (tuple, list)):
ty = tuple(map(type, item))
else:
ty = type(item)
else: else:
ty = type(default) ty = type(default)
guessed_type = True guessed_type = True
if isinstance(ty, tuple): if isinstance(ty, tuple):
return Tuple(ty) return Tuple(ty)
if isinstance(ty, ParamType): if isinstance(ty, ParamType):
return ty return ty
if ty is text_type or ty is str or ty is None:
if ty is str or ty is None:
return STRING return STRING
if ty is int: if ty is int:
return INT return INT
# Booleans are only okay if not guessed. This is done because for
# flags the default value is actually a bit of a lie in that it
# indicates which of the flags is the one we want. See get_default()
# for more information.
if ty is bool and not guessed_type:
return BOOL
if ty is float: if ty is float:
return FLOAT return FLOAT
if ty is bool:
return BOOL
if guessed_type: if guessed_type:
return STRING return STRING
# Catch a common mistake
if __debug__: if __debug__:
try: try:
if issubclass(ty, ParamType): if issubclass(ty, ParamType):
raise AssertionError( raise AssertionError(
"Attempted to use an uninstantiated parameter type ({}).".format(ty) f"Attempted to use an uninstantiated parameter type ({ty})."
) )
except TypeError: except TypeError:
# ty is an instance (correct), so issubclass fails.
pass pass
return FuncParamType(ty) return FuncParamType(ty)
#: A dummy parameter type that just does nothing. From a user's #: A dummy parameter type that just does nothing. From a user's
#: perspective this appears to just be the same as `STRING` but internally #: perspective this appears to just be the same as `STRING` but
#: no string conversion takes place. This is necessary to achieve the #: internally no string conversion takes place if the input was bytes.
#: same bytes/unicode behavior on Python 2/3 in situations where you want #: This is usually useful when working with file paths as they can
#: to not convert argument types. This is usually useful when working #: appear in bytes and unicode.
#: with file paths as they can appear in bytes and unicode.
#: #:
#: For path related uses the :class:`Path` type is a better choice but #: For path related uses the :class:`Path` type is a better choice but
#: there are situations where an unprocessed type is useful which is why #: there are situations where an unprocessed type is useful which is why

View file

@ -1,86 +1,105 @@
import os import os
import sys import sys
import typing as t
from functools import update_wrapper
from types import ModuleType
from ._compat import _default_text_stderr from ._compat import _default_text_stderr
from ._compat import _default_text_stdout from ._compat import _default_text_stdout
from ._compat import _find_binary_writer
from ._compat import auto_wrap_for_ansi from ._compat import auto_wrap_for_ansi
from ._compat import binary_streams from ._compat import binary_streams
from ._compat import filename_to_ui
from ._compat import get_filesystem_encoding from ._compat import get_filesystem_encoding
from ._compat import get_streerror
from ._compat import is_bytes
from ._compat import open_stream from ._compat import open_stream
from ._compat import PY2
from ._compat import should_strip_ansi from ._compat import should_strip_ansi
from ._compat import string_types
from ._compat import strip_ansi from ._compat import strip_ansi
from ._compat import text_streams from ._compat import text_streams
from ._compat import text_type
from ._compat import WIN from ._compat import WIN
from .globals import resolve_color_default from .globals import resolve_color_default
if not PY2: if t.TYPE_CHECKING:
from ._compat import _find_binary_writer import typing_extensions as te
elif WIN:
from ._winconsole import _get_windows_argv
from ._winconsole import _hash_py_argv
from ._winconsole import _initial_argv_hash
echo_native_types = string_types + (bytes, bytearray) F = t.TypeVar("F", bound=t.Callable[..., t.Any])
def _posixify(name): def _posixify(name: str) -> str:
return "-".join(name.split()).lower() return "-".join(name.split()).lower()
def safecall(func): def safecall(func: F) -> F:
"""Wraps a function so that it swallows exceptions.""" """Wraps a function so that it swallows exceptions."""
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs): # type: ignore
try: try:
return func(*args, **kwargs) return func(*args, **kwargs)
except Exception: except Exception:
pass pass
return wrapper return update_wrapper(t.cast(F, wrapper), func)
def make_str(value): def make_str(value: t.Any) -> str:
"""Converts a value into a valid string.""" """Converts a value into a valid string."""
if isinstance(value, bytes): if isinstance(value, bytes):
try: try:
return value.decode(get_filesystem_encoding()) return value.decode(get_filesystem_encoding())
except UnicodeError: except UnicodeError:
return value.decode("utf-8", "replace") return value.decode("utf-8", "replace")
return text_type(value) return str(value)
def make_default_short_help(help, max_length=45): def make_default_short_help(help: str, max_length: int = 45) -> str:
"""Return a condensed version of help string.""" """Returns a condensed version of help string."""
# Consider only the first paragraph.
paragraph_end = help.find("\n\n")
if paragraph_end != -1:
help = help[:paragraph_end]
# Collapse newlines, tabs, and spaces.
words = help.split() words = help.split()
if not words:
return ""
# The first paragraph started with a "no rewrap" marker, ignore it.
if words[0] == "\b":
words = words[1:]
total_length = 0 total_length = 0
result = [] last_index = len(words) - 1
done = False
for word in words: for i, word in enumerate(words):
if word[-1:] == ".": total_length += len(word) + (i > 0)
done = True
new_length = 1 + len(word) if result else len(word) if total_length > max_length: # too long, truncate
if total_length + new_length > max_length:
result.append("...")
done = True
else:
if result:
result.append(" ")
result.append(word)
if done:
break break
total_length += new_length
return "".join(result) if word[-1] == ".": # sentence end, truncate without "..."
return " ".join(words[: i + 1])
if total_length == max_length and i != last_index:
break # not at sentence end, truncate with "..."
else:
return " ".join(words) # no truncation needed
# Account for the length of the suffix.
total_length += len("...")
# remove words until the length is short enough
while i > 0:
total_length -= len(words[i]) + (i > 0)
if total_length <= max_length:
break
i -= 1
return " ".join(words[:i]) + "..."
class LazyFile(object): class LazyFile:
"""A lazy file works like a regular file but it does not fully open """A lazy file works like a regular file but it does not fully open
the file but it does perform some basic checks early to see if the the file but it does perform some basic checks early to see if the
filename parameter does make sense. This is useful for safely opening filename parameter does make sense. This is useful for safely opening
@ -88,13 +107,19 @@ class LazyFile(object):
""" """
def __init__( def __init__(
self, filename, mode="r", encoding=None, errors="strict", atomic=False self,
filename: str,
mode: str = "r",
encoding: t.Optional[str] = None,
errors: t.Optional[str] = "strict",
atomic: bool = False,
): ):
self.name = filename self.name = filename
self.mode = mode self.mode = mode
self.encoding = encoding self.encoding = encoding
self.errors = errors self.errors = errors
self.atomic = atomic self.atomic = atomic
self._f: t.Optional[t.IO]
if filename == "-": if filename == "-":
self._f, self.should_close = open_stream(filename, mode, encoding, errors) self._f, self.should_close = open_stream(filename, mode, encoding, errors)
@ -107,15 +132,15 @@ class LazyFile(object):
self._f = None self._f = None
self.should_close = True self.should_close = True
def __getattr__(self, name): def __getattr__(self, name: str) -> t.Any:
return getattr(self.open(), name) return getattr(self.open(), name)
def __repr__(self): def __repr__(self) -> str:
if self._f is not None: if self._f is not None:
return repr(self._f) return repr(self._f)
return "<unopened file '{}' {}>".format(self.name, self.mode) return f"<unopened file '{self.name}' {self.mode}>"
def open(self): def open(self) -> t.IO:
"""Opens the file if it's not yet open. This call might fail with """Opens the file if it's not yet open. This call might fail with
a :exc:`FileError`. Not handling this error will produce an error a :exc:`FileError`. Not handling this error will produce an error
that Click shows. that Click shows.
@ -126,102 +151,100 @@ class LazyFile(object):
rv, self.should_close = open_stream( rv, self.should_close = open_stream(
self.name, self.mode, self.encoding, self.errors, atomic=self.atomic self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
) )
except (IOError, OSError) as e: # noqa: E402 except OSError as e: # noqa: E402
from .exceptions import FileError from .exceptions import FileError
raise FileError(self.name, hint=get_streerror(e)) raise FileError(self.name, hint=e.strerror) from e
self._f = rv self._f = rv
return rv return rv
def close(self): def close(self) -> None:
"""Closes the underlying file, no matter what.""" """Closes the underlying file, no matter what."""
if self._f is not None: if self._f is not None:
self._f.close() self._f.close()
def close_intelligently(self): def close_intelligently(self) -> None:
"""This function only closes the file if it was opened by the lazy """This function only closes the file if it was opened by the lazy
file wrapper. For instance this will never close stdin. file wrapper. For instance this will never close stdin.
""" """
if self.should_close: if self.should_close:
self.close() self.close()
def __enter__(self): def __enter__(self) -> "LazyFile":
return self return self
def __exit__(self, exc_type, exc_value, tb): def __exit__(self, exc_type, exc_value, tb): # type: ignore
self.close_intelligently() self.close_intelligently()
def __iter__(self): def __iter__(self) -> t.Iterator[t.AnyStr]:
self.open() self.open()
return iter(self._f) return iter(self._f) # type: ignore
class KeepOpenFile(object): class KeepOpenFile:
def __init__(self, file): def __init__(self, file: t.IO) -> None:
self._file = file self._file = file
def __getattr__(self, name): def __getattr__(self, name: str) -> t.Any:
return getattr(self._file, name) return getattr(self._file, name)
def __enter__(self): def __enter__(self) -> "KeepOpenFile":
return self return self
def __exit__(self, exc_type, exc_value, tb): def __exit__(self, exc_type, exc_value, tb): # type: ignore
pass pass
def __repr__(self): def __repr__(self) -> str:
return repr(self._file) return repr(self._file)
def __iter__(self): def __iter__(self) -> t.Iterator[t.AnyStr]:
return iter(self._file) return iter(self._file)
def echo(message=None, file=None, nl=True, err=False, color=None): def echo(
"""Prints a message plus a newline to the given file or stdout. On message: t.Optional[t.Any] = None,
first sight, this looks like the print function, but it has improved file: t.Optional[t.IO] = None,
support for handling Unicode and binary data that does not fail no nl: bool = True,
matter how badly configured the system is. err: bool = False,
color: t.Optional[bool] = None,
) -> None:
"""Print a message and newline to stdout or a file. This should be
used instead of :func:`print` because it provides better support
for different data, files, and environments.
Primarily it means that you can print binary data as well as Unicode Compared to :func:`print`, this does the following:
data on both 2.x and 3.x to the given file in the most appropriate way
possible. This is a very carefree function in that it will try its
best to not fail. As of Click 6.0 this includes support for unicode
output on the Windows console.
In addition to that, if `colorama`_ is installed, the echo function will - Ensures that the output encoding is not misconfigured on Linux.
also support clever handling of ANSI codes. Essentially it will then - Supports Unicode in the Windows console.
do the following: - Supports writing to binary outputs, and supports writing bytes
to text outputs.
- Supports colors and styles on Windows.
- Removes ANSI color and style codes if the output does not look
like an interactive terminal.
- Always flushes the output.
- add transparent handling of ANSI color codes on Windows. :param message: The string or bytes to output. Other objects are
- hide ANSI codes automatically if the destination file is not a converted to strings.
terminal. :param file: The file to write to. Defaults to ``stdout``.
:param err: Write to ``stderr`` instead of ``stdout``.
.. _colorama: https://pypi.org/project/colorama/ :param nl: Print a newline after the message. Enabled by default.
:param color: Force showing or hiding colors and other styles. By
default Click will remove color if the output does not look like
an interactive terminal.
.. versionchanged:: 6.0 .. versionchanged:: 6.0
As of Click 6.0 the echo function will properly support unicode Support Unicode output on the Windows console. Click does not
output on the windows console. Not that click does not modify modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()``
the interpreter in any way which means that `sys.stdout` or the will still not support Unicode.
print statement or function will still not provide unicode support.
.. versionchanged:: 2.0
Starting with version 2.0 of Click, the echo function will work
with colorama if it's installed.
.. versionadded:: 3.0
The `err` parameter was added.
.. versionchanged:: 4.0 .. versionchanged:: 4.0
Added the `color` flag. Added the ``color`` parameter.
:param message: the message to print .. versionadded:: 3.0
:param file: the file to write to (defaults to ``stdout``) Added the ``err`` parameter.
:param err: if set to true the file defaults to ``stderr`` instead of
``stdout``. This is faster and easier than calling .. versionchanged:: 2.0
:func:`get_text_stderr` yourself. Support colors on Windows if colorama is installed.
:param nl: if set to `True` (the default) a newline is printed afterwards.
:param color: controls if the terminal supports ANSI colors or not. The
default is autodetection.
""" """
if file is None: if file is None:
if err: if err:
@ -230,70 +253,73 @@ def echo(message=None, file=None, nl=True, err=False, color=None):
file = _default_text_stdout() file = _default_text_stdout()
# Convert non bytes/text into the native string type. # Convert non bytes/text into the native string type.
if message is not None and not isinstance(message, echo_native_types): if message is not None and not isinstance(message, (str, bytes, bytearray)):
message = text_type(message) out: t.Optional[t.Union[str, bytes]] = str(message)
else:
out = message
if nl: if nl:
message = message or u"" out = out or ""
if isinstance(message, text_type): if isinstance(out, str):
message += u"\n" out += "\n"
else: else:
message += b"\n" out += b"\n"
# If there is a message, and we're in Python 3, and the value looks if not out:
# like bytes, we manually need to find the binary stream and write the file.flush()
# message in there. This is done separately so that most stream return
# types will work as you would expect. Eg: you can write to StringIO
# for other cases. # If there is a message and the value looks like bytes, we manually
if message and not PY2 and is_bytes(message): # need to find the binary stream and write the message in there.
# This is done separately so that most stream types will work as you
# would expect. Eg: you can write to StringIO for other cases.
if isinstance(out, (bytes, bytearray)):
binary_file = _find_binary_writer(file) binary_file = _find_binary_writer(file)
if binary_file is not None: if binary_file is not None:
file.flush() file.flush()
binary_file.write(message) binary_file.write(out)
binary_file.flush() binary_file.flush()
return return
# ANSI-style support. If there is no message or we are dealing with # ANSI style code support. For no message or bytes, nothing happens.
# bytes nothing is happening. If we are connected to a file we want # When outputting to a file instead of a terminal, strip codes.
# to strip colors. If we are on windows we either wrap the stream else:
# to strip the color or we use the colorama support to translate the
# ansi codes to API calls.
if message and not is_bytes(message):
color = resolve_color_default(color) color = resolve_color_default(color)
if should_strip_ansi(file, color): if should_strip_ansi(file, color):
message = strip_ansi(message) out = strip_ansi(out)
elif WIN: elif WIN:
if auto_wrap_for_ansi is not None: if auto_wrap_for_ansi is not None:
file = auto_wrap_for_ansi(file) file = auto_wrap_for_ansi(file) # type: ignore
elif not color: elif not color:
message = strip_ansi(message) out = strip_ansi(out)
if message: file.write(out) # type: ignore
file.write(message)
file.flush() file.flush()
def get_binary_stream(name): def get_binary_stream(name: "te.Literal['stdin', 'stdout', 'stderr']") -> t.BinaryIO:
"""Returns a system stream for byte processing. This essentially """Returns a system stream for byte processing.
returns the stream from the sys module with the given name but it
solves some compatibility issues between different Python versions.
Primarily this function is necessary for getting binary streams on
Python 3.
:param name: the name of the stream to open. Valid names are ``'stdin'``, :param name: the name of the stream to open. Valid names are ``'stdin'``,
``'stdout'`` and ``'stderr'`` ``'stdout'`` and ``'stderr'``
""" """
opener = binary_streams.get(name) opener = binary_streams.get(name)
if opener is None: if opener is None:
raise TypeError("Unknown standard stream '{}'".format(name)) raise TypeError(f"Unknown standard stream '{name}'")
return opener() return opener()
def get_text_stream(name, encoding=None, errors="strict"): def get_text_stream(
name: "te.Literal['stdin', 'stdout', 'stderr']",
encoding: t.Optional[str] = None,
errors: t.Optional[str] = "strict",
) -> t.TextIO:
"""Returns a system stream for text processing. This usually returns """Returns a system stream for text processing. This usually returns
a wrapped stream around a binary stream returned from a wrapped stream around a binary stream returned from
:func:`get_binary_stream` but it also can take shortcuts on Python 3 :func:`get_binary_stream` but it also can take shortcuts for already
for already correctly configured streams. correctly configured streams.
:param name: the name of the stream to open. Valid names are ``'stdin'``, :param name: the name of the stream to open. Valid names are ``'stdin'``,
``'stdout'`` and ``'stderr'`` ``'stdout'`` and ``'stderr'``
@ -302,13 +328,18 @@ def get_text_stream(name, encoding=None, errors="strict"):
""" """
opener = text_streams.get(name) opener = text_streams.get(name)
if opener is None: if opener is None:
raise TypeError("Unknown standard stream '{}'".format(name)) raise TypeError(f"Unknown standard stream '{name}'")
return opener(encoding, errors) return opener(encoding, errors)
def open_file( def open_file(
filename, mode="r", encoding=None, errors="strict", lazy=False, atomic=False filename: str,
): mode: str = "r",
encoding: t.Optional[str] = None,
errors: t.Optional[str] = "strict",
lazy: bool = False,
atomic: bool = False,
) -> t.IO:
"""This is similar to how the :class:`File` works but for manual """This is similar to how the :class:`File` works but for manual
usage. Files are opened non lazy by default. This can open regular usage. Files are opened non lazy by default. This can open regular
files as well as stdin/stdout if ``'-'`` is passed. files as well as stdin/stdout if ``'-'`` is passed.
@ -332,35 +363,35 @@ def open_file(
moved on close. moved on close.
""" """
if lazy: if lazy:
return LazyFile(filename, mode, encoding, errors, atomic=atomic) return t.cast(t.IO, LazyFile(filename, mode, encoding, errors, atomic=atomic))
f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
if not should_close: if not should_close:
f = KeepOpenFile(f) f = t.cast(t.IO, KeepOpenFile(f))
return f return f
def get_os_args(): def get_os_args() -> t.Sequence[str]:
"""This returns the argument part of sys.argv in the most appropriate """Returns the argument part of ``sys.argv``, removing the first
form for processing. What this means is that this return value is in value which is the name of the script.
a format that works for Click to process but does not necessarily
correspond well to what's actually standard for the interpreter.
On most environments the return value is ``sys.argv[:1]`` unchanged. .. deprecated:: 8.0
However if you are on Windows and running Python 2 the return value Will be removed in Click 8.1. Access ``sys.argv[1:]`` directly
will actually be a list of unicode strings instead because the instead.
default behavior on that platform otherwise will not be able to
carry all possible values that sys.argv can have.
.. versionadded:: 6.0
""" """
# We can only extract the unicode argv if sys.argv has not been import warnings
# changed since the startup of the application.
if PY2 and WIN and _initial_argv_hash == _hash_py_argv(): warnings.warn(
return _get_windows_argv() "'get_os_args' is deprecated and will be removed in Click 8.1."
" Access 'sys.argv[1:]' directly instead.",
DeprecationWarning,
stacklevel=2,
)
return sys.argv[1:] return sys.argv[1:]
def format_filename(filename, shorten=False): def format_filename(
filename: t.Union[str, bytes, os.PathLike], shorten: bool = False
) -> str:
"""Formats a filename for user display. The main purpose of this """Formats a filename for user display. The main purpose of this
function is to ensure that the filename can be displayed at all. This function is to ensure that the filename can be displayed at all. This
will decode the filename to unicode if necessary in a way that it will will decode the filename to unicode if necessary in a way that it will
@ -374,10 +405,11 @@ def format_filename(filename, shorten=False):
""" """
if shorten: if shorten:
filename = os.path.basename(filename) filename = os.path.basename(filename)
return filename_to_ui(filename)
return os.fsdecode(filename)
def get_app_dir(app_name, roaming=True, force_posix=False): def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str:
r"""Returns the config folder for the application. The default behavior r"""Returns the config folder for the application. The default behavior
is to return whatever is most appropriate for the operating system. is to return whatever is most appropriate for the operating system.
@ -392,13 +424,9 @@ def get_app_dir(app_name, roaming=True, force_posix=False):
``~/.config/foo-bar`` ``~/.config/foo-bar``
Unix (POSIX): Unix (POSIX):
``~/.foo-bar`` ``~/.foo-bar``
Win XP (roaming): Windows (roaming):
``C:\Documents and Settings\<user>\Local Settings\Application Data\Foo Bar``
Win XP (not roaming):
``C:\Documents and Settings\<user>\Application Data\Foo Bar``
Win 7 (roaming):
``C:\Users\<user>\AppData\Roaming\Foo Bar`` ``C:\Users\<user>\AppData\Roaming\Foo Bar``
Win 7 (not roaming): Windows (not roaming):
``C:\Users\<user>\AppData\Local\Foo Bar`` ``C:\Users\<user>\AppData\Local\Foo Bar``
.. versionadded:: 2.0 .. versionadded:: 2.0
@ -419,7 +447,7 @@ def get_app_dir(app_name, roaming=True, force_posix=False):
folder = os.path.expanduser("~") folder = os.path.expanduser("~")
return os.path.join(folder, app_name) return os.path.join(folder, app_name)
if force_posix: if force_posix:
return os.path.join(os.path.expanduser("~/.{}".format(_posixify(app_name)))) return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}"))
if sys.platform == "darwin": if sys.platform == "darwin":
return os.path.join( return os.path.join(
os.path.expanduser("~/Library/Application Support"), app_name os.path.expanduser("~/Library/Application Support"), app_name
@ -430,7 +458,7 @@ def get_app_dir(app_name, roaming=True, force_posix=False):
) )
class PacifyFlushWrapper(object): class PacifyFlushWrapper:
"""This wrapper is used to catch and suppress BrokenPipeErrors resulting """This wrapper is used to catch and suppress BrokenPipeErrors resulting
from ``.flush()`` being called on broken pipe during the shutdown/final-GC from ``.flush()`` being called on broken pipe during the shutdown/final-GC
of the Python interpreter. Notably ``.flush()`` is always called on of the Python interpreter. Notably ``.flush()`` is always called on
@ -439,17 +467,113 @@ class PacifyFlushWrapper(object):
pipe, all calls and attributes are proxied. pipe, all calls and attributes are proxied.
""" """
def __init__(self, wrapped): def __init__(self, wrapped: t.IO) -> None:
self.wrapped = wrapped self.wrapped = wrapped
def flush(self): def flush(self) -> None:
try: try:
self.wrapped.flush() self.wrapped.flush()
except IOError as e: except OSError as e:
import errno import errno
if e.errno != errno.EPIPE: if e.errno != errno.EPIPE:
raise raise
def __getattr__(self, attr): def __getattr__(self, attr: str) -> t.Any:
return getattr(self.wrapped, attr) return getattr(self.wrapped, attr)
def _detect_program_name(
path: t.Optional[str] = None, _main: ModuleType = sys.modules["__main__"]
) -> str:
"""Determine the command used to run the program, for use in help
text. If a file or entry point was executed, the file name is
returned. If ``python -m`` was used to execute a module or package,
``python -m name`` is returned.
This doesn't try to be too precise, the goal is to give a concise
name for help text. Files are only shown as their name without the
path. ``python`` is only shown for modules, and the full path to
``sys.executable`` is not shown.
:param path: The Python file being executed. Python puts this in
``sys.argv[0]``, which is used by default.
:param _main: The ``__main__`` module. This should only be passed
during internal testing.
.. versionadded:: 8.0
Based on command args detection in the Werkzeug reloader.
:meta private:
"""
if not path:
path = sys.argv[0]
# The value of __package__ indicates how Python was called. It may
# not exist if a setuptools script is installed as an egg. It may be
# set incorrectly for entry points created with pip on Windows.
if getattr(_main, "__package__", None) is None or (
os.name == "nt"
and _main.__package__ == ""
and not os.path.exists(path)
and os.path.exists(f"{path}.exe")
):
# Executed a file, like "python app.py".
return os.path.basename(path)
# Executed a module, like "python -m example".
# Rewritten by Python from "-m script" to "/path/to/script.py".
# Need to look at main module to determine how it was executed.
py_module = t.cast(str, _main.__package__)
name = os.path.splitext(os.path.basename(path))[0]
# A submodule like "example.cli".
if name != "__main__":
py_module = f"{py_module}.{name}"
return f"python -m {py_module.lstrip('.')}"
def _expand_args(
args: t.Iterable[str],
*,
user: bool = True,
env: bool = True,
glob_recursive: bool = True,
) -> t.List[str]:
"""Simulate Unix shell expansion with Python functions.
See :func:`glob.glob`, :func:`os.path.expanduser`, and
:func:`os.path.expandvars`.
This intended for use on Windows, where the shell does not do any
expansion. It may not exactly match what a Unix shell would do.
:param args: List of command line arguments to expand.
:param user: Expand user home directory.
:param env: Expand environment variables.
:param glob_recursive: ``**`` matches directories recursively.
.. versionadded:: 8.0
:meta private:
"""
from glob import glob
out = []
for arg in args:
if user:
arg = os.path.expanduser(arg)
if env:
arg = os.path.expandvars(arg)
matches = glob(arg, recursive=glob_recursive)
if not matches:
out.append(arg)
else:
out.extend(matches)
return out

View file

@ -1,3 +1,7 @@
import os
import shutil
import tempfile
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
@ -6,3 +10,22 @@ from click.testing import CliRunner
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def runner(request): def runner(request):
return CliRunner() return CliRunner()
def check_symlink_impl():
"""This function checks if using symlinks is allowed
on the host machine"""
tempdir = tempfile.mkdtemp(prefix="click-")
test_pth = os.path.join(tempdir, "check_sym_impl")
sym_pth = os.path.join(tempdir, "link")
open(test_pth, "w").close()
rv = True
try:
os.symlink(test_pth, sym_pth)
except (NotImplementedError, OSError):
# Creating symlinks on Windows require elevated access.
# OSError is thrown if the function is called without it.
rv = False
finally:
shutil.rmtree(tempdir, ignore_errors=True)
return rv

View file

@ -1,11 +1,8 @@
# -*- coding: utf-8 -*-
import sys import sys
import pytest import pytest
import click import click
from click._compat import PY2
from click._compat import text_type
def test_nargs_star(runner): def test_nargs_star(runner):
@ -13,19 +10,19 @@ def test_nargs_star(runner):
@click.argument("src", nargs=-1) @click.argument("src", nargs=-1)
@click.argument("dst") @click.argument("dst")
def copy(src, dst): def copy(src, dst):
click.echo("src={}".format("|".join(src))) click.echo(f"src={'|'.join(src)}")
click.echo("dst={}".format(dst)) click.echo(f"dst={dst}")
result = runner.invoke(copy, ["foo.txt", "bar.txt", "dir"]) result = runner.invoke(copy, ["foo.txt", "bar.txt", "dir"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == ["src=foo.txt|bar.txt", "dst=dir"] assert result.output.splitlines() == ["src=foo.txt|bar.txt", "dst=dir"]
def test_nargs_default(runner): def test_argument_unbounded_nargs_cant_have_default(runner):
with pytest.raises(TypeError, match="nargs=-1"): with pytest.raises(TypeError, match="nargs=-1"):
@click.command() @click.command()
@click.argument("src", nargs=-1, default=42) @click.argument("src", nargs=-1, default=["42"])
def copy(src): def copy(src):
pass pass
@ -35,8 +32,9 @@ def test_nargs_tup(runner):
@click.argument("name", nargs=1) @click.argument("name", nargs=1)
@click.argument("point", nargs=2, type=click.INT) @click.argument("point", nargs=2, type=click.INT)
def copy(name, point): def copy(name, point):
click.echo("name={}".format(name)) click.echo(f"name={name}")
click.echo("point={0[0]}/{0[1]}".format(point)) x, y = point
click.echo(f"point={x}/{y}")
result = runner.invoke(copy, ["peter", "1", "2"]) result = runner.invoke(copy, ["peter", "1", "2"])
assert not result.exception assert not result.exception
@ -56,7 +54,8 @@ def test_nargs_tup_composite(runner):
@click.command() @click.command()
@click.argument("item", **opts) @click.argument("item", **opts)
def copy(item): def copy(item):
click.echo("name={0[0]} id={0[1]:d}".format(item)) name, id = item
click.echo(f"name={name} id={id:d}")
result = runner.invoke(copy, ["peter", "1"]) result = runner.invoke(copy, ["peter", "1"])
assert not result.exception assert not result.exception
@ -83,22 +82,17 @@ def test_bytes_args(runner, monkeypatch):
@click.argument("arg") @click.argument("arg")
def from_bytes(arg): def from_bytes(arg):
assert isinstance( assert isinstance(
arg, text_type arg, str
), "UTF-8 encoded argument should be implicitly converted to Unicode" ), "UTF-8 encoded argument should be implicitly converted to Unicode"
# Simulate empty locale environment variables # Simulate empty locale environment variables
if PY2:
monkeypatch.setattr(sys.stdin, "encoding", "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.stdin, "encoding", "utf-8")
monkeypatch.setattr(sys, "getfilesystemencoding", lambda: "utf-8") monkeypatch.setattr(sys, "getfilesystemencoding", lambda: "utf-8")
monkeypatch.setattr(sys, "getdefaultencoding", lambda: "utf-8") monkeypatch.setattr(sys, "getdefaultencoding", lambda: "utf-8")
runner.invoke( runner.invoke(
from_bytes, from_bytes,
[u"Something outside of ASCII range: 林".encode("UTF-8")], ["Something outside of ASCII range: 林".encode()],
catch_exceptions=False, catch_exceptions=False,
) )
@ -169,33 +163,55 @@ def test_stdout_default(runner):
assert result.output == "Foo bar baz\n" assert result.output == "Foo bar baz\n"
def test_nargs_envvar(runner): @pytest.mark.parametrize(
@click.command() ("nargs", "value", "expect"),
@click.option("--arg", nargs=2) [
def cmd(arg): (2, "", None),
click.echo("|".join(arg)) (2, "a", "Takes 2 values but 1 was given."),
(2, "a b", ("a", "b")),
result = runner.invoke( (2, "a b c", "Takes 2 values but 3 were given."),
cmd, [], auto_envvar_prefix="TEST", env={"TEST_ARG": "foo bar"} (-1, "a b c", ("a", "b", "c")),
) (-1, "", ()),
assert not result.exception ],
assert result.output == "foo|bar\n" )
def test_nargs_envvar(runner, nargs, value, expect):
if nargs == -1:
param = click.argument("arg", envvar="X", nargs=nargs)
else:
param = click.option("--arg", envvar="X", nargs=nargs)
@click.command() @click.command()
@click.option("--arg", envvar="X", nargs=2) @param
def cmd(arg): def cmd(arg):
click.echo("|".join(arg)) return arg
result = runner.invoke(cmd, [], env={"X": "foo bar"}) result = runner.invoke(cmd, env={"X": value}, standalone_mode=False)
assert not result.exception
assert result.output == "foo|bar\n" if isinstance(expect, str):
assert isinstance(result.exception, click.BadParameter)
assert expect in result.exception.format_message()
else:
assert result.return_value == expect
def test_nargs_envvar_only_if_values_empty(runner):
@click.command()
@click.argument("arg", envvar="X", nargs=-1)
def cli(arg):
return arg
result = runner.invoke(cli, ["a", "b"], standalone_mode=False)
assert result.return_value == ("a", "b")
result = runner.invoke(cli, env={"X": "a"}, standalone_mode=False)
assert result.return_value == ("a",)
def test_empty_nargs(runner): def test_empty_nargs(runner):
@click.command() @click.command()
@click.argument("arg", nargs=-1) @click.argument("arg", nargs=-1)
def cmd(arg): def cmd(arg):
click.echo("arg:{}".format("|".join(arg))) click.echo(f"arg:{'|'.join(arg)}")
result = runner.invoke(cmd, []) result = runner.invoke(cmd, [])
assert result.exit_code == 0 assert result.exit_code == 0
@ -204,7 +220,7 @@ def test_empty_nargs(runner):
@click.command() @click.command()
@click.argument("arg", nargs=-1, required=True) @click.argument("arg", nargs=-1, required=True)
def cmd2(arg): def cmd2(arg):
click.echo("arg:{}".format("|".join(arg))) click.echo(f"arg:{'|'.join(arg)}")
result = runner.invoke(cmd2, []) result = runner.invoke(cmd2, [])
assert result.exit_code == 2 assert result.exit_code == 2
@ -215,7 +231,7 @@ def test_missing_arg(runner):
@click.command() @click.command()
@click.argument("arg") @click.argument("arg")
def cmd(arg): def cmd(arg):
click.echo("arg:{}".format(arg)) click.echo(f"arg:{arg}")
result = runner.invoke(cmd, []) result = runner.invoke(cmd, [])
assert result.exit_code == 2 assert result.exit_code == 2
@ -226,9 +242,9 @@ def test_missing_argument_string_cast():
ctx = click.Context(click.Command("")) ctx = click.Context(click.Command(""))
with pytest.raises(click.MissingParameter) as excinfo: with pytest.raises(click.MissingParameter) as excinfo:
click.Argument(["a"], required=True).full_process_value(ctx, None) click.Argument(["a"], required=True).process_value(ctx, None)
assert str(excinfo.value) == "missing parameter: a" assert str(excinfo.value) == "Missing parameter: a"
def test_implicit_non_required(runner): def test_implicit_non_required(runner):
@ -268,7 +284,7 @@ def test_nargs_star_ordering(runner):
click.echo(arg) click.echo(arg)
result = runner.invoke(cmd, ["a", "b", "c"]) result = runner.invoke(cmd, ["a", "b", "c"])
assert result.output.splitlines() == ["(u'a',)" if PY2 else "('a',)", "b", "c"] assert result.output.splitlines() == ["('a',)", "b", "c"]
def test_nargs_specified_plus_star_ordering(runner): def test_nargs_specified_plus_star_ordering(runner):
@ -281,11 +297,7 @@ def test_nargs_specified_plus_star_ordering(runner):
click.echo(arg) click.echo(arg)
result = runner.invoke(cmd, ["a", "b", "c", "d", "e", "f"]) result = runner.invoke(cmd, ["a", "b", "c", "d", "e", "f"])
assert result.output.splitlines() == [ assert result.output.splitlines() == ["('a', 'b', 'c')", "d", "('e', 'f')"]
"(u'a', u'b', u'c')" if PY2 else "('a', 'b', 'c')",
"d",
"(u'e', u'f')" if PY2 else "('e', 'f')",
]
def test_defaults_for_nargs(runner): def test_defaults_for_nargs(runner):
@ -303,7 +315,7 @@ def test_defaults_for_nargs(runner):
result = runner.invoke(cmd, ["3"]) result = runner.invoke(cmd, ["3"])
assert result.exception is not None assert result.exception is not None
assert "argument a takes 2 values" in result.output assert "Argument 'a' takes 2 values." in result.output
def test_multiple_param_decls_not_allowed(runner): def test_multiple_param_decls_not_allowed(runner):
@ -313,3 +325,55 @@ def test_multiple_param_decls_not_allowed(runner):
@click.argument("x", click.Choice(["a", "b"])) @click.argument("x", click.Choice(["a", "b"]))
def copy(x): def copy(x):
click.echo(x) click.echo(x)
def test_multiple_not_allowed():
with pytest.raises(TypeError, match="multiple"):
click.Argument(["a"], multiple=True)
@pytest.mark.parametrize("value", [(), ("a",), ("a", "b", "c")])
def test_nargs_bad_default(runner, value):
with pytest.raises(ValueError, match="nargs=2"):
click.Argument(["a"], nargs=2, default=value)
def test_subcommand_help(runner):
@click.group()
@click.argument("name")
@click.argument("val")
@click.option("--opt")
@click.pass_context
def cli(ctx, name, val, opt):
ctx.obj = dict(name=name, val=val)
@cli.command()
@click.pass_obj
def cmd(obj):
click.echo(f"CMD for {obj['name']} with value {obj['val']}")
result = runner.invoke(cli, ["foo", "bar", "cmd", "--help"])
assert not result.exception
assert "Usage: cli NAME VAL cmd [OPTIONS]" in result.output
def test_nested_subcommand_help(runner):
@click.group()
@click.argument("arg1")
@click.option("--opt1")
def cli(arg1, opt1):
pass
@cli.group()
@click.argument("arg2")
@click.option("--opt2")
def cmd(arg2, opt2):
pass
@cmd.command()
def subcmd():
click.echo("subcommand")
result = runner.invoke(cli, ["arg1", "cmd", "arg2", "subcmd", "--help"])
assert not result.exception
assert "Usage: cli ARG1 cmd ARG2 subcmd [OPTIONS]" in result.output

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 os
import uuid import uuid
from itertools import chain
import pytest
import click import click
@ -78,11 +80,35 @@ def test_basic_group(runner):
assert "SUBCOMMAND EXECUTED" in result.output assert "SUBCOMMAND EXECUTED" in result.output
def test_group_commands_dict(runner):
"""A Group can be built with a dict of commands."""
@click.command()
def sub():
click.echo("sub", nl=False)
cli = click.Group(commands={"other": sub})
result = runner.invoke(cli, ["other"])
assert result.output == "sub"
def test_group_from_list(runner):
"""A Group can be built with a list of commands."""
@click.command()
def sub():
click.echo("sub", nl=False)
cli = click.Group(commands=[sub])
result = runner.invoke(cli, ["sub"])
assert result.output == "sub"
def test_basic_option(runner): def test_basic_option(runner):
@click.command() @click.command()
@click.option("--foo", default="no value") @click.option("--foo", default="no value")
def cli(foo): def cli(foo):
click.echo(u"FOO:[{}]".format(foo)) click.echo(f"FOO:[{foo}]")
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
@ -94,22 +120,22 @@ def test_basic_option(runner):
result = runner.invoke(cli, ["--foo"]) result = runner.invoke(cli, ["--foo"])
assert result.exception assert result.exception
assert "--foo option requires an argument" in result.output assert "Option '--foo' requires an argument." in result.output
result = runner.invoke(cli, ["--foo="]) result = runner.invoke(cli, ["--foo="])
assert not result.exception assert not result.exception
assert "FOO:[]" in result.output assert "FOO:[]" in result.output
result = runner.invoke(cli, [u"--foo=\N{SNOWMAN}"]) result = runner.invoke(cli, ["--foo=\N{SNOWMAN}"])
assert not result.exception assert not result.exception
assert u"FOO:[\N{SNOWMAN}]" in result.output assert "FOO:[\N{SNOWMAN}]" in result.output
def test_int_option(runner): def test_int_option(runner):
@click.command() @click.command()
@click.option("--foo", default=42) @click.option("--foo", default=42)
def cli(foo): def cli(foo):
click.echo("FOO:[{}]".format(foo * 2)) click.echo(f"FOO:[{foo * 2}]")
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
@ -121,7 +147,7 @@ def test_int_option(runner):
result = runner.invoke(cli, ["--foo=bar"]) result = runner.invoke(cli, ["--foo=bar"])
assert result.exception assert result.exception
assert "Invalid value for '--foo': bar is not a valid integer" in result.output assert "Invalid value for '--foo': 'bar' is not a valid integer." in result.output
def test_uuid_option(runner): def test_uuid_option(runner):
@ -131,7 +157,7 @@ def test_uuid_option(runner):
) )
def cli(u): def cli(u):
assert type(u) is uuid.UUID assert type(u) is uuid.UUID
click.echo("U:[{}]".format(u)) click.echo(f"U:[{u}]")
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
@ -143,7 +169,7 @@ def test_uuid_option(runner):
result = runner.invoke(cli, ["--u=bar"]) result = runner.invoke(cli, ["--u=bar"])
assert result.exception assert result.exception
assert "Invalid value for '--u': bar is not a valid UUID value" in result.output assert "Invalid value for '--u': 'bar' is not a valid UUID." in result.output
def test_float_option(runner): def test_float_option(runner):
@ -151,7 +177,7 @@ def test_float_option(runner):
@click.option("--foo", default=42, type=click.FLOAT) @click.option("--foo", default=42, type=click.FLOAT)
def cli(foo): def cli(foo):
assert type(foo) is float assert type(foo) is float
click.echo("FOO:[{}]".format(foo)) click.echo(f"FOO:[{foo}]")
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
@ -163,7 +189,7 @@ def test_float_option(runner):
result = runner.invoke(cli, ["--foo=bar"]) result = runner.invoke(cli, ["--foo=bar"])
assert result.exception assert result.exception
assert "Invalid value for '--foo': bar is not a valid float" in result.output assert "Invalid value for '--foo': 'bar' is not a valid float." in result.output
def test_boolean_option(runner): def test_boolean_option(runner):
@ -182,7 +208,7 @@ def test_boolean_option(runner):
assert result.output == "False\n" assert result.output == "False\n"
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert result.output == "{}\n".format(default) assert result.output == f"{default}\n"
for default in True, False: for default in True, False:
@ -193,33 +219,30 @@ def test_boolean_option(runner):
result = runner.invoke(cli, ["--flag"]) result = runner.invoke(cli, ["--flag"])
assert not result.exception assert not result.exception
assert result.output == "{}\n".format(not default) assert result.output == f"{not default}\n"
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert result.output == "{}\n".format(default) assert result.output == f"{default}\n"
def test_boolean_conversion(runner): @pytest.mark.parametrize(
for default in True, False: ("value", "expect"),
chain(
((x, "True") for x in ("1", "true", "t", "yes", "y", "on")),
((x, "False") for x in ("0", "false", "f", "no", "n", "off")),
),
)
def test_boolean_conversion(runner, value, expect):
@click.command() @click.command()
@click.option("--flag", default=default, type=bool) @click.option("--flag", type=bool)
def cli(flag): def cli(flag):
click.echo(flag) click.echo(flag, nl=False)
for value in "true", "t", "1", "yes", "y":
result = runner.invoke(cli, ["--flag", value]) result = runner.invoke(cli, ["--flag", value])
assert not result.exception assert result.output == expect
assert result.output == "True\n"
for value in "false", "f", "0", "no", "n": result = runner.invoke(cli, ["--flag", value.title()])
result = runner.invoke(cli, ["--flag", value]) assert result.output == expect
assert not result.exception
assert result.output == "False\n"
result = runner.invoke(cli, [])
assert not result.exception
assert result.output == "{}\n".format(default)
def test_file_option(runner): def test_file_option(runner):
@ -280,10 +303,7 @@ def test_file_lazy_mode(runner):
os.mkdir("example.txt") os.mkdir("example.txt")
result_in = runner.invoke(input_non_lazy, ["--file=example.txt"]) result_in = runner.invoke(input_non_lazy, ["--file=example.txt"])
assert result_in.exit_code == 2 assert result_in.exit_code == 2
assert ( assert "Invalid value for '--file': 'example.txt'" in result_in.output
"Invalid value for '--file': Could not open file: example.txt"
in result_in.output
)
def test_path_option(runner): def test_path_option(runner):
@ -308,8 +328,8 @@ def test_path_option(runner):
@click.command() @click.command()
@click.option("-f", type=click.Path(exists=True)) @click.option("-f", type=click.Path(exists=True))
def showtype(f): def showtype(f):
click.echo("is_file={}".format(os.path.isfile(f))) click.echo(f"is_file={os.path.isfile(f)}")
click.echo("is_dir={}".format(os.path.isdir(f))) click.echo(f"is_dir={os.path.isdir(f)}")
with runner.isolated_filesystem(): with runner.isolated_filesystem():
result = runner.invoke(showtype, ["-f", "xxx"]) result = runner.invoke(showtype, ["-f", "xxx"])
@ -322,7 +342,7 @@ def test_path_option(runner):
@click.command() @click.command()
@click.option("-f", type=click.Path()) @click.option("-f", type=click.Path())
def exists(f): def exists(f):
click.echo("exists={}".format(os.path.exists(f))) click.echo(f"exists={os.path.exists(f)}")
with runner.isolated_filesystem(): with runner.isolated_filesystem():
result = runner.invoke(exists, ["-f", "xxx"]) result = runner.invoke(exists, ["-f", "xxx"])
@ -345,14 +365,35 @@ def test_choice_option(runner):
result = runner.invoke(cli, ["--method=meh"]) result = runner.invoke(cli, ["--method=meh"])
assert result.exit_code == 2 assert result.exit_code == 2
assert ( assert (
"Invalid value for '--method': invalid choice: meh." "Invalid value for '--method': 'meh' is not one of 'foo', 'bar', 'baz'."
" (choose from foo, bar, baz)" in result.output in result.output
) )
result = runner.invoke(cli, ["--help"]) result = runner.invoke(cli, ["--help"])
assert "--method [foo|bar|baz]" in result.output assert "--method [foo|bar|baz]" in result.output
def test_choice_argument(runner):
@click.command()
@click.argument("method", type=click.Choice(["foo", "bar", "baz"]))
def cli(method):
click.echo(method)
result = runner.invoke(cli, ["foo"])
assert not result.exception
assert result.output == "foo\n"
result = runner.invoke(cli, ["meh"])
assert result.exit_code == 2
assert (
"Invalid value for '{foo|bar|baz}': 'meh' is not one of 'foo',"
" 'bar', 'baz'." in result.output
)
result = runner.invoke(cli, ["--help"])
assert "{foo|bar|baz}" in result.output
def test_datetime_option_default(runner): def test_datetime_option_default(runner):
@click.command() @click.command()
@click.option("--start_date", type=click.DateTime()) @click.option("--start_date", type=click.DateTime())
@ -370,9 +411,8 @@ def test_datetime_option_default(runner):
result = runner.invoke(cli, ["--start_date=2015-09"]) result = runner.invoke(cli, ["--start_date=2015-09"])
assert result.exit_code == 2 assert result.exit_code == 2
assert ( assert (
"Invalid value for '--start_date':" "Invalid value for '--start_date': '2015-09' does not match the formats"
" invalid datetime format: 2015-09." " '%Y-%m-%d', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S'."
" (choose from %Y-%m-%d, %Y-%m-%dT%H:%M:%S, %Y-%m-%d %H:%M:%S)"
) in result.output ) in result.output
result = runner.invoke(cli, ["--help"]) result = runner.invoke(cli, ["--help"])
@ -392,76 +432,6 @@ def test_datetime_option_custom(runner):
assert result.output == "2010-06-05T00:00:00\n" assert result.output == "2010-06-05T00:00:00\n"
def test_int_range_option(runner):
@click.command()
@click.option("--x", type=click.IntRange(0, 5))
def cli(x):
click.echo(x)
result = runner.invoke(cli, ["--x=5"])
assert not result.exception
assert result.output == "5\n"
result = runner.invoke(cli, ["--x=6"])
assert result.exit_code == 2
assert (
"Invalid value for '--x': 6 is not in the valid range of 0 to 5.\n"
in result.output
)
@click.command()
@click.option("--x", type=click.IntRange(0, 5, clamp=True))
def clamp(x):
click.echo(x)
result = runner.invoke(clamp, ["--x=5"])
assert not result.exception
assert result.output == "5\n"
result = runner.invoke(clamp, ["--x=6"])
assert not result.exception
assert result.output == "5\n"
result = runner.invoke(clamp, ["--x=-1"])
assert not result.exception
assert result.output == "0\n"
def test_float_range_option(runner):
@click.command()
@click.option("--x", type=click.FloatRange(0, 5))
def cli(x):
click.echo(x)
result = runner.invoke(cli, ["--x=5.0"])
assert not result.exception
assert result.output == "5.0\n"
result = runner.invoke(cli, ["--x=6.0"])
assert result.exit_code == 2
assert (
"Invalid value for '--x': 6.0 is not in the valid range of 0 to 5.\n"
in result.output
)
@click.command()
@click.option("--x", type=click.FloatRange(0, 5, clamp=True))
def clamp(x):
click.echo(x)
result = runner.invoke(clamp, ["--x=5.0"])
assert not result.exception
assert result.output == "5.0\n"
result = runner.invoke(clamp, ["--x=6.0"])
assert not result.exception
assert result.output == "5\n"
result = runner.invoke(clamp, ["--x=-1.0"])
assert not result.exception
assert result.output == "0\n"
def test_required_option(runner): def test_required_option(runner):
@click.command() @click.command()
@click.option("--foo", required=True) @click.option("--foo", required=True)
@ -558,3 +528,39 @@ def test_hidden_group(runner):
assert result.exit_code == 0 assert result.exit_code == 0
assert "subgroup" not in result.output assert "subgroup" not in result.output
assert "nope" not in result.output assert "nope" not in result.output
def test_summary_line(runner):
@click.group()
def cli():
pass
@cli.command()
def cmd():
"""
Summary line without period
Here is a sentence. And here too.
"""
pass
result = runner.invoke(cli, ["--help"])
assert "Summary line without period" in result.output
assert "Here is a sentence." not in result.output
def test_help_invalid_default(runner):
cli = click.Command(
"cli",
params=[
click.Option(
["-a"],
type=click.Path(exists=True),
default="not found",
show_default=True,
),
],
)
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert "default: not found" in result.output

View file

@ -7,9 +7,8 @@ import click
def debug(): def debug():
click.echo( click.echo(
"{}={}".format( f"{sys._getframe(1).f_code.co_name}"
sys._getframe(1).f_code.co_name, "|".join(click.get_current_context().args) f"={'|'.join(click.get_current_context().args)}"
)
) )
@ -77,18 +76,37 @@ def test_chaining_with_options(runner):
@cli.command("sdist") @cli.command("sdist")
@click.option("--format") @click.option("--format")
def sdist(format): def sdist(format):
click.echo("sdist called {}".format(format)) click.echo(f"sdist called {format}")
@cli.command("bdist") @cli.command("bdist")
@click.option("--format") @click.option("--format")
def bdist(format): def bdist(format):
click.echo("bdist called {}".format(format)) click.echo(f"bdist called {format}")
result = runner.invoke(cli, ["bdist", "--format=1", "sdist", "--format=2"]) result = runner.invoke(cli, ["bdist", "--format=1", "sdist", "--format=2"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == ["bdist called 1", "sdist called 2"] assert result.output.splitlines() == ["bdist called 1", "sdist called 2"]
@pytest.mark.parametrize(("chain", "expect"), [(False, "None"), (True, "[]")])
def test_no_command_result_callback(runner, chain, expect):
"""When a group has ``invoke_without_command=True``, the result
callback is always invoked. A regular group invokes it with
``None``, a chained group with ``[]``.
"""
@click.group(invoke_without_command=True, chain=chain)
def cli():
pass
@cli.result_callback()
def process_result(result):
click.echo(str(result), nl=False)
result = runner.invoke(cli, [])
assert result.output == expect
def test_chaining_with_arguments(runner): def test_chaining_with_arguments(runner):
@click.group(chain=True) @click.group(chain=True)
def cli(): def cli():
@ -97,12 +115,12 @@ def test_chaining_with_arguments(runner):
@cli.command("sdist") @cli.command("sdist")
@click.argument("format") @click.argument("format")
def sdist(format): def sdist(format):
click.echo("sdist called {}".format(format)) click.echo(f"sdist called {format}")
@cli.command("bdist") @cli.command("bdist")
@click.argument("format") @click.argument("format")
def bdist(format): def bdist(format):
click.echo("bdist called {}".format(format)) click.echo(f"bdist called {format}")
result = runner.invoke(cli, ["bdist", "1", "sdist", "2"]) result = runner.invoke(cli, ["bdist", "1", "sdist", "2"])
assert not result.exception assert not result.exception
@ -115,7 +133,7 @@ def test_pipeline(runner):
def cli(input): def cli(input):
pass pass
@cli.resultcallback() @cli.result_callback()
def process_pipeline(processors, input): def process_pipeline(processors, input):
iterator = (x.rstrip("\r\n") for x in input) iterator = (x.rstrip("\r\n") for x in input)
for processor in processors: for processor in processors:
@ -192,7 +210,7 @@ def test_multicommand_arg_behavior(runner):
@click.group(chain=True) @click.group(chain=True)
@click.argument("arg") @click.argument("arg")
def cli(arg): def cli(arg):
click.echo("cli:{}".format(arg)) click.echo(f"cli:{arg}")
@cli.command() @cli.command()
def a(): def a():

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import re import re
import click import click
@ -26,7 +25,7 @@ def test_other_command_forward(runner):
@cli.command() @cli.command()
@click.option("--count", default=1) @click.option("--count", default=1)
def test(count): def test(count):
click.echo("Count: {:d}".format(count)) click.echo(f"Count: {count:d}")
@cli.command() @cli.command()
@click.option("--count", default=1) @click.option("--count", default=1)
@ -40,6 +39,28 @@ def test_other_command_forward(runner):
assert result.output == "Count: 1\nCount: 42\n" assert result.output == "Count: 1\nCount: 42\n"
def test_forwarded_params_consistency(runner):
cli = click.Group()
@cli.command()
@click.option("-a")
@click.pass_context
def first(ctx, **kwargs):
click.echo(f"{ctx.params}")
@cli.command()
@click.option("-a")
@click.option("-b")
@click.pass_context
def second(ctx, **kwargs):
click.echo(f"{ctx.params}")
ctx.forward(first)
result = runner.invoke(cli, ["second", "-a", "foo", "-b", "bar"])
assert not result.exception
assert result.output == "{'a': 'foo', 'b': 'bar'}\n{'a': 'foo', 'b': 'bar'}\n"
def test_auto_shorthelp(runner): def test_auto_shorthelp(runner):
@click.group() @click.group()
def cli(): def cli():
@ -102,7 +123,7 @@ def test_group_with_args(runner):
@click.group() @click.group()
@click.argument("obj") @click.argument("obj")
def cli(obj): def cli(obj):
click.echo("obj={}".format(obj)) click.echo(f"obj={obj}")
@cli.command() @cli.command()
def move(): def move():
@ -134,7 +155,7 @@ def test_base_command(runner):
class OptParseCommand(click.BaseCommand): class OptParseCommand(click.BaseCommand):
def __init__(self, name, parser, callback): def __init__(self, name, parser, callback):
click.BaseCommand.__init__(self, name) super().__init__(name)
self.parser = parser self.parser = parser
self.callback = callback self.callback = callback
@ -211,7 +232,7 @@ def test_object_propagation(runner):
@cli.command() @cli.command()
@click.pass_context @click.pass_context
def sync(ctx): def sync(ctx):
click.echo("Debug is {}".format("on" if ctx.obj["DEBUG"] else "off")) click.echo(f"Debug is {'on' if ctx.obj['DEBUG'] else 'off'}")
result = runner.invoke(cli, ["sync"]) result = runner.invoke(cli, ["sync"])
assert result.exception is None assert result.exception is None
@ -259,13 +280,43 @@ def test_invoked_subcommand(runner):
assert result.output == "no subcommand, use default\nin subcommand\n" assert result.output == "no subcommand, use default\nin subcommand\n"
def test_aliased_command_canonical_name(runner):
class AliasedGroup(click.Group):
def get_command(self, ctx, cmd_name):
return push
def resolve_command(self, ctx, args):
_, command, args = super().resolve_command(ctx, args)
return command.name, command, args
cli = AliasedGroup()
@cli.command()
def push():
click.echo("push command")
result = runner.invoke(cli, ["pu", "--help"])
assert not result.exception
assert result.output.startswith("Usage: root push [OPTIONS]")
def test_group_add_command_name(runner):
cli = click.Group("cli")
cmd = click.Command("a", params=[click.Option(["-x"], required=True)])
cli.add_command(cmd, "b")
# Check that the command is accessed through the registered name,
# not the original name.
result = runner.invoke(cli, ["b"], default_map={"b": {"x": 3}})
assert result.exit_code == 0
def test_unprocessed_options(runner): def test_unprocessed_options(runner):
@click.command(context_settings=dict(ignore_unknown_options=True)) @click.command(context_settings=dict(ignore_unknown_options=True))
@click.argument("args", nargs=-1, type=click.UNPROCESSED) @click.argument("args", nargs=-1, type=click.UNPROCESSED)
@click.option("--verbose", "-v", count=True) @click.option("--verbose", "-v", count=True)
def cli(verbose, args): def cli(verbose, args):
click.echo("Verbosity: {}".format(verbose)) click.echo(f"Verbosity: {verbose}")
click.echo("Args: {}".format("|".join(args))) click.echo(f"Args: {'|'.join(args)}")
result = runner.invoke(cli, ["-foo", "-vvvvx", "--muhaha", "x", "y", "-x"]) result = runner.invoke(cli, ["-foo", "-vvvvx", "--muhaha", "x", "y", "-x"])
assert not result.exception assert not result.exception
@ -282,14 +333,14 @@ def test_deprecated_in_help_messages(runner):
pass pass
result = runner.invoke(cmd_with_help, ["--help"]) result = runner.invoke(cmd_with_help, ["--help"])
assert "(DEPRECATED)" in result.output assert "(Deprecated)" in result.output
@click.command(deprecated=True) @click.command(deprecated=True)
def cmd_without_help(): def cmd_without_help():
pass pass
result = runner.invoke(cmd_without_help, ["--help"]) result = runner.invoke(cmd_without_help, ["--help"])
assert "(DEPRECATED)" in result.output assert "(Deprecated)" in result.output
def test_deprecated_in_invocation(runner): def test_deprecated_in_invocation(runner):

View file

@ -1,45 +1,8 @@
import pytest
import click
from click._compat import should_strip_ansi from click._compat import should_strip_ansi
from click._compat import WIN
def test_legacy_callbacks(runner):
def legacy_callback(ctx, value):
return value.upper()
@click.command()
@click.option("--foo", callback=legacy_callback)
def cli(foo):
click.echo(foo)
with pytest.warns(DeprecationWarning, match="2-arg style"):
result = runner.invoke(cli, ["--foo", "wat"])
assert result.exit_code == 0
assert "WAT" in result.output
def test_bash_func_name():
from click._bashcomplete import get_completion_script
script = get_completion_script("foo-bar baz_blah", "_COMPLETE_VAR", "bash").strip()
assert script.startswith("_foo_barbaz_blah_completion()")
assert "_COMPLETE_VAR=complete $1" in script
def test_zsh_func_name():
from click._bashcomplete import get_completion_script
script = get_completion_script("foo-bar", "_COMPLETE_VAR", "zsh").strip()
assert script.startswith("#compdef foo-bar")
assert "compdef _foo_bar_completion foo-bar;" in script
assert "(( ! $+commands[foo-bar] )) && return 1" in script
@pytest.mark.xfail(WIN, reason="Jupyter not tested/supported on Windows")
def test_is_jupyter_kernel_output(): def test_is_jupyter_kernel_output():
class JupyterKernelFakeStream(object): class JupyterKernelFakeStream:
pass pass
# implementation detail, aka cheapskate test # implementation detail, aka cheapskate test

View file

@ -1,9 +1,14 @@
# -*- coding: utf-8 -*- from contextlib import contextmanager
import pytest
import click import click
from click.core import ParameterSource
from click.decorators import pass_meta_key
def test_ensure_context_objects(runner): def test_ensure_context_objects(runner):
class Foo(object): class Foo:
def __init__(self): def __init__(self):
self.title = "default" self.title = "default"
@ -25,7 +30,7 @@ def test_ensure_context_objects(runner):
def test_get_context_objects(runner): def test_get_context_objects(runner):
class Foo(object): class Foo:
def __init__(self): def __init__(self):
self.title = "default" self.title = "default"
@ -48,7 +53,7 @@ def test_get_context_objects(runner):
def test_get_context_objects_no_ensuring(runner): def test_get_context_objects_no_ensuring(runner):
class Foo(object): class Foo:
def __init__(self): def __init__(self):
self.title = "default" self.title = "default"
@ -71,7 +76,7 @@ def test_get_context_objects_no_ensuring(runner):
def test_get_context_objects_missing(runner): def test_get_context_objects_missing(runner):
class Foo(object): class Foo:
pass pass
pass_foo = click.make_pass_decorator(Foo) pass_foo = click.make_pass_decorator(Foo)
@ -129,7 +134,7 @@ def test_global_context_object(runner):
def test_context_meta(runner): def test_context_meta(runner):
LANG_KEY = "{}.lang".format(__name__) LANG_KEY = f"{__name__}.lang"
def set_language(value): def set_language(value):
click.get_current_context().meta[LANG_KEY] = value click.get_current_context().meta[LANG_KEY] = value
@ -147,6 +152,28 @@ def test_context_meta(runner):
runner.invoke(cli, [], catch_exceptions=False) runner.invoke(cli, [], catch_exceptions=False)
def test_make_pass_meta_decorator(runner):
@click.group()
@click.pass_context
def cli(ctx):
ctx.meta["value"] = "good"
@cli.command()
@pass_meta_key("value")
def show(value):
return value
result = runner.invoke(cli, ["show"], standalone_mode=False)
assert result.return_value == "good"
def test_make_pass_meta_decorator_doc():
pass_value = pass_meta_key("value")
assert "the 'value' key from :attr:`click.Context.meta`" in pass_value.__doc__
pass_value = pass_meta_key("value", doc_description="the test value")
assert "passes the test value" in pass_value.__doc__
def test_context_pushing(): def test_context_pushing():
rv = [] rv = []
@ -210,13 +237,29 @@ def test_close_before_pop(runner):
assert called == [True] assert called == [True]
def test_with_resource():
@contextmanager
def manager():
val = [1]
yield val
val[0] = 0
ctx = click.Context(click.Command("test"))
with ctx.scope():
rv = ctx.with_resource(manager())
assert rv[0] == 1
assert rv == [0]
def test_make_pass_decorator_args(runner): def test_make_pass_decorator_args(runner):
""" """
Test to check that make_pass_decorator doesn't consume arguments based on Test to check that make_pass_decorator doesn't consume arguments based on
invocation order. invocation order.
""" """
class Foo(object): class Foo:
title = "foocmd" title = "foocmd"
pass_foo = click.make_pass_decorator(Foo) pass_foo = click.make_pass_decorator(Foo)
@ -247,6 +290,20 @@ def test_make_pass_decorator_args(runner):
assert result.output == "foocmd\n" assert result.output == "foocmd\n"
def test_propagate_show_default_setting(runner):
"""A context's ``show_default`` setting defaults to the value from
the parent context.
"""
group = click.Group(
commands={
"sub": click.Command("sub", params=[click.Option(["-a"], default="a")]),
},
context_settings={"show_default": True},
)
result = runner.invoke(group, ["sub", "--help"])
assert "[default: a]" in result.output
def test_exit_not_standalone(): def test_exit_not_standalone():
@click.command() @click.command()
@click.pass_context @click.pass_context
@ -261,3 +318,50 @@ def test_exit_not_standalone():
ctx.exit(0) ctx.exit(0)
assert cli.main([], "test_exit_not_standalone", standalone_mode=False) == 0 assert cli.main([], "test_exit_not_standalone", standalone_mode=False) == 0
@pytest.mark.parametrize(
("option_args", "invoke_args", "expect"),
[
pytest.param({}, {}, ParameterSource.DEFAULT, id="default"),
pytest.param(
{},
{"default_map": {"option": 1}},
ParameterSource.DEFAULT_MAP,
id="default_map",
),
pytest.param(
{},
{"args": ["-o", "1"]},
ParameterSource.COMMANDLINE,
id="commandline short",
),
pytest.param(
{},
{"args": ["--option", "1"]},
ParameterSource.COMMANDLINE,
id="commandline long",
),
pytest.param(
{},
{"auto_envvar_prefix": "TEST", "env": {"TEST_OPTION": "1"}},
ParameterSource.ENVIRONMENT,
id="environment auto",
),
pytest.param(
{"envvar": "NAME"},
{"env": {"NAME": "1"}},
ParameterSource.ENVIRONMENT,
id="environment manual",
),
],
)
def test_parameter_source(runner, option_args, invoke_args, expect):
@click.command()
@click.pass_context
@click.option("-o", "--option", default=1, **option_args)
def cli(ctx, option):
return ctx.get_parameter_source("option")
rv = runner.invoke(cli, standalone_mode=False, **invoke_args)
assert rv.return_value == expect

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) @click.option("--foo", default=42, type=click.FLOAT)
def cli(foo): def cli(foo):
assert type(foo) is float assert type(foo) is float
click.echo("FOO:[{}]".format(foo)) click.echo(f"FOO:[{foo}]")
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
@ -32,8 +32,8 @@ def test_nargs_plus_multiple(runner):
"--arg", default=((1, 2), (3, 4)), nargs=2, multiple=True, type=click.INT "--arg", default=((1, 2), (3, 4)), nargs=2, multiple=True, type=click.INT
) )
def cli(arg): def cli(arg):
for item in arg: for a, b in arg:
click.echo("<{0[0]:d}|{0[1]:d}>".format(item)) click.echo(f"<{a:d}|{b:d}>")
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception

Some files were not shown because too many files have changed in this diff Show more