Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: pallets/click
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: python-trio/asyncclick
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.

Commits on Jan 24, 2018

  1. drop PY2

    smurfix committed Jan 24, 2018
    Copy the full SHA
    1ba9ef4 View commit details
  2. Save old stdin/out/err in sys

    helps with invoking the debugger
    smurfix committed Jan 24, 2018
    Copy the full SHA
    c41e375 View commit details
  3. Trio-ified.

    Several interesting methods are now async.
    smurfix committed Jan 24, 2018
    Copy the full SHA
    376f68e View commit details
  4. Documentation update

    smurfix committed Jan 24, 2018
    Copy the full SHA
    d1bc7af View commit details
  5. Rename: click > trio_click

    This is done by way of a symlink so that importing updates from Click still
    works.
    smurfix committed Jan 24, 2018
    Copy the full SHA
    085d247 View commit details
  6. Debianized.

    smurfix committed Jan 24, 2018
    Copy the full SHA
    a756c9e View commit details
  7. doc update

    smurfix committed Jan 24, 2018
    Copy the full SHA
    275327a View commit details

Commits on Jan 28, 2018

  1. Copy the full SHA
    807d0fe View commit details
  2. change author

    for pypi upload
    smurfix committed Jan 28, 2018
    Copy the full SHA
    779106d View commit details
  3. fix debian/.gitignore

    smurfix committed Jan 28, 2018
    Copy the full SHA
    7da5cb9 View commit details
  4. Copy the full SHA
    2b67b4f View commit details

Commits on Mar 12, 2018

  1. Copy the full SHA
    8195a1d View commit details
  2. Copy the full SHA
    83ce87f View commit details

Commits on Apr 11, 2018

  1. New icon locations

    smurfix committed Apr 11, 2018
    Copy the full SHA
    98c7bfc View commit details
  2. Merge current master

    smurfix committed Apr 11, 2018
    Copy the full SHA
    a9c3dd5 View commit details

Commits on May 28, 2018

  1. Copy the full SHA
    50b3ba4 View commit details
  2. Fix test failures

    smurfix committed May 28, 2018
    Copy the full SHA
    aee020f View commit details
  3. * Merge to current Upstream.

    smurfix committed May 28, 2018
    Copy the full SHA
    7d25198 View commit details
  4. * Merge to current Upstream.

    smurfix committed May 28, 2018
    Copy the full SHA
    8580faf View commit details

Commits on Jun 8, 2018

  1. Fix test failure

    smurfix committed Jun 8, 2018
    Copy the full SHA
    8680b0f View commit details
  2. * Updated README

    smurfix committed Jun 8, 2018
    Copy the full SHA
    37e8409 View commit details

Commits on Jun 11, 2018

  1. Merge

    smurfix committed Jun 11, 2018
    Copy the full SHA
    a3dea22 View commit details

Commits on Jun 12, 2018

  1. fix versioning

    smurfix committed Jun 12, 2018
    Copy the full SHA
    d3ad1af View commit details
  2. setup.py typo

    smurfix committed Jun 12, 2018
    Copy the full SHA
    cc00d4d View commit details

Commits on Dec 20, 2018

  1. Merge v7.0

    smurfix committed Dec 20, 2018
    Copy the full SHA
    8090fbf View commit details
  2. Fix a couple of tests

    smurfix committed Dec 20, 2018
    Copy the full SHA
    2630231 View commit details
  3. Copy the full SHA
    ae39b48 View commit details
  4. fix setup.py

    smurfix committed Dec 20, 2018
    Copy the full SHA
    ad45ba3 View commit details
  5. Use AnyIO instead

    smurfix committed Dec 20, 2018
    Copy the full SHA
    575bed3 View commit details
  6. Version 7.0.1

    smurfix committed Dec 20, 2018
    Copy the full SHA
    5492ace View commit details
  7. Debian packaging updated

    smurfix committed Dec 20, 2018
    Copy the full SHA
    f299df6 View commit details

Commits on Dec 21, 2018

  1. Copy the full SHA
    ac08825 View commit details
  2. Add [async]exitstack managing

    smurfix committed Dec 21, 2018
    Copy the full SHA
    917a010 View commit details

Commits on Jan 2, 2019

  1. Copy the full SHA
    5eb7982 View commit details
  2. * Fix 3.6 compatibility

    smurfix committed Jan 2, 2019
    Copy the full SHA
    1fdc5f1 View commit details

Commits on Jan 7, 2019

  1. Copy the full SHA
    9676da7 View commit details
  2. Version uptick

    smurfix committed Jan 7, 2019
    Copy the full SHA
    f52450e View commit details

Commits on Jan 27, 2019

  1. Copy the full SHA
    c8fe013 View commit details

Commits on Jun 21, 2019

  1. rename to asyncclick

    smurfix committed Jun 21, 2019
    Copy the full SHA
    53cd410 View commit details
  2. nonsense

    smurfix committed Jun 21, 2019
    Copy the full SHA
    2eadead View commit details
  3. need anyio

    smurfix committed Jun 21, 2019
    Copy the full SHA
    ea172d2 View commit details
  4. test!

    smurfix committed Jun 21, 2019
    Copy the full SHA
    3ae8f93 View commit details

Commits on Aug 27, 2019

  1. fix readme

    smurfix committed Aug 27, 2019
    Copy the full SHA
    c622b63 View commit details
  2. fix version

    smurfix committed Aug 27, 2019
    Copy the full SHA
    33c6cd0 View commit details

Commits on Oct 8, 2019

  1. updated README

    smurfix committed Oct 8, 2019
    Copy the full SHA
    ef9e6b4 View commit details

Commits on Feb 13, 2020

  1. Copy the full SHA
    1e7432b View commit details
  2. Use Trio for testing

    asyncio reports "too many open files" for some reason
    smurfix committed Feb 13, 2020
    Copy the full SHA
    293a925 View commit details
  3. * Merge current 7.1

    smurfix committed Feb 13, 2020
    Copy the full SHA
    6dffcf2 View commit details
  4. Async-ize bash completion

    smurfix committed Feb 13, 2020
    Copy the full SHA
    bac4f54 View commit details
  5. consistently test with anyio

    smurfix committed Feb 13, 2020
    Copy the full SHA
    4803aae View commit details
Showing with 1,562 additions and 781 deletions.
  1. +1 −5 .github/workflows/publish.yaml
  2. +0 −2 .github/workflows/tests.yaml
  3. +7 −0 .gitignore
  4. +1 −1 .pre-commit-config.yaml
  5. +14 −1 CHANGES.rst
  6. +13 −0 Makefile
  7. +16 −2 README.md
  8. +6 −0 debian/.gitignore
  9. +138 −0 debian/changelog
  10. +1 −0 debian/compat
  11. +19 −0 debian/control
  12. +10 −0 debian/rules
  13. +1 −0 debian/source/format
  14. +4 −0 debian/watch
  15. +4 −2 docs/advanced.rst
  16. +5 −5 docs/api.rst
  17. +64 −172 docs/arguments.rst
  18. +4 −4 docs/commands.rst
  19. +6 −6 docs/complex.rst
  20. +37 −0 docs/contrib.rst
  21. +3 −3 docs/exceptions.rst
  22. +90 −0 docs/handling-files.rst
  23. +12 −7 docs/index.rst
  24. +3 −1 docs/parameters.rst
  25. +1 −1 docs/quickstart.rst
  26. +1 −1 docs/setuptools.rst
  27. +3 −3 docs/shell-completion.rst
  28. +28 −21 docs/testing.rst
  29. +18 −1 docs/upgrading.rst
  30. +11 −11 docs/utils.rst
  31. +1 −1 examples/aliases/aliases.py
  32. +1 −1 examples/colors/colors.py
  33. +2 −2 examples/completion/completion.py
  34. +1 −1 examples/complex/complex/cli.py
  35. +1 −1 examples/complex/complex/commands/cmd_init.py
  36. +1 −1 examples/complex/complex/commands/cmd_status.py
  37. +1 −1 examples/imagepipe/imagepipe.py
  38. +1 −1 examples/inout/inout.py
  39. +1 −1 examples/naval/naval.py
  40. +1 −1 examples/repo/repo.py
  41. +1 −1 examples/termui/termui.py
  42. +1 −1 examples/validation/validation.py
  43. +9 −9 pyproject.toml
  44. +9 −4 requirements/build.txt
  45. +2 −0 requirements/dev.in
  46. +48 −21 requirements/dev.txt
  47. +13 −8 requirements/docs.txt
  48. +1 −0 requirements/tests.in
  49. +24 −5 requirements/tests.txt
  50. +10 −5 requirements/typing.txt
  51. +1 −1 src/{click → asyncclick}/__init__.py
  52. 0 src/{click → asyncclick}/_compat.py
  53. +79 −31 src/{click → asyncclick}/_termui_impl.py
  54. 0 src/{click → asyncclick}/_textwrap.py
  55. 0 src/{click → asyncclick}/_winconsole.py
  56. +174 −90 src/{click → asyncclick}/core.py
  57. +4 −4 src/{click → asyncclick}/decorators.py
  58. 0 src/{click → asyncclick}/exceptions.py
  59. 0 src/{click → asyncclick}/formatting.py
  60. 0 src/{click → asyncclick}/globals.py
  61. +1 −1 src/{click → asyncclick}/parser.py
  62. 0 src/{click → asyncclick}/py.typed
  63. +23 −16 src/{click → asyncclick}/shell_completion.py
  64. 0 src/{click → asyncclick}/termui.py
  65. +7 −4 src/{click → asyncclick}/testing.py
  66. +26 −23 src/{click → asyncclick}/types.py
  67. 0 src/{click → asyncclick}/utils.py
  68. +22 −2 tests/conftest.py
  69. +6 −2 tests/test_arguments.py
  70. +8 −6 tests/test_basic.py
  71. +1 −1 tests/test_chain.py
  72. +3 −1 tests/test_command_decorators.py
  73. +179 −30 tests/test_commands.py
  74. +1 −1 tests/test_compat.py
  75. +67 −14 tests/test_context.py
  76. +8 −5 tests/test_custom_classes.py
  77. +1 −1 tests/test_defaults.py
  78. +1 −1 tests/test_formatting.py
  79. +7 −5 tests/test_imports.py
  80. +14 −9 tests/test_info_dict.py
  81. +1 −1 tests/test_normalization.py
  82. +4 −4 tests/test_options.py
  83. +3 −3 tests/test_parser.py
  84. +134 −110 tests/test_shell_completion.py
  85. +8 −7 tests/test_termui.py
  86. +109 −74 tests/test_testing.py
  87. +2 −2 tests/test_types.py
  88. +8 −8 tests/test_utils.py
  89. +1 −1 tests/typing/typing_aliased_group.py
  90. +1 −1 tests/typing/typing_confirmation_option.py
  91. +1 −1 tests/typing/typing_group_kw_options.py
  92. +1 −1 tests/typing/typing_help_option.py
  93. +1 −1 tests/typing/typing_options.py
  94. +1 −1 tests/typing/typing_password_option.py
  95. +1 −1 tests/typing/typing_simple_example.py
  96. +1 −1 tests/typing/typing_version_option.py
  97. +2 −2 tox.ini
6 changes: 1 addition & 5 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -64,10 +64,6 @@ jobs:
id-token: write
steps:
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
- uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # v1.10.3
with:
repository-url: https://test.pypi.org/legacy/
packages-dir: artifact/
- uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # v1.10.3
- uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2
with:
packages-dir: artifact/
2 changes: 0 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -20,8 +20,6 @@ jobs:
- {python: '3.11'}
- {python: '3.10'}
- {python: '3.9'}
- {python: '3.8'}
- {python: '3.7'}
- {name: PyPy, python: 'pypy-3.10', tox: pypy310}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
src/*.egg-info/
/.hypothesis/
/build/
/.pybuild/
.cache/
.idea/
.vscode/
.venv*/
venv*/
__pycache__/
/dist/
/.pytest_cache/
dist/
.coverage*
htmlcov/
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.1
rev: v0.8.1
hooks:
- id: ruff
- id: ruff-format
15 changes: 14 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
Version 8.1.8
-------------

Unreleased
Released 2024-12-19

- Fix an issue with type hints for ``click.open_file()``. :issue:`2717`
- Fix issue where error message for invalid ``click.Path`` displays on
@@ -16,6 +16,19 @@ Unreleased
:issue:`2632`
- Fix ``click.echo(color=...)`` passing ``color`` to coloroma so it can be
forced on Windows. :issue:`2606`.
- More robust bash version check, fixing problem on Windows with git-bash.
:issue:`2638`
- Cache the help option generated by the ``help_option_names`` setting to
respect its eagerness. :pr:`2811`
- Replace uses of ``os.system`` with ``subprocess.Popen``. :issue:`1476`
- Exceptions generated during a command will use the context's ``color``
setting when being displayed. :issue:`2193`
- Error message when defining option with invalid name is more descriptive.
:issue:`2452`
- Refactor code generating default ``--help`` option to deduplicate code.
:pr:`2563`
- Test ``CLIRunner`` resets patched ``_compat.should_strip_ansi``.
:issue:`2732`


Version 8.1.7
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/make -f

PACKAGE=asyncclick
ifneq ($(wildcard /usr/share/sourcemgr/make/py),)
include /usr/share/sourcemgr/make/py
# available via http://github.com/smurfix/sourcemgr

else
%:
@echo "Please use 'python setup.py'."
@exit 1
endif

18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# $ asyncclick_

Asyncclick is a fork of Click (described below) that works with trio or asyncio.

AsyncClick allows you to seamlessly use async command and subcommand handlers.


# $ click_

Click is a Python package for creating beautiful command line interfaces
@@ -19,18 +26,21 @@ Click in three points:
## A Simple Example

```python
import click
import asyncclick as click
import anyio

@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):
async def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for _ in range(count):
click.echo(f"Hello, {name}!")
await anyio.sleep(0.2)

if __name__ == '__main__':
hello()
# alternately: anyio.run(hello.main)
```

```
@@ -50,3 +60,7 @@ allow the maintainers to devote more time to the projects, [please
donate today][].

[please donate today]: https://palletsprojects.com/donate

The AsyncClick fork is maintained by Matthias Urlichs <matthias@urlichs.de>.
It's not a lot of work, so if you'd like to motivate me, donate to the
charity of your choice and tell me that you've done so. ;-)
6 changes: 6 additions & 0 deletions debian/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/files
/*.log
/*.debhelper
/debhelper-build-stamp
/*.substvars
/python3-asyncclick
138 changes: 138 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
asyncclick (1:8.1.8-1) unstable; urgency=medium

* new Upstream

-- Matthias Urlichs <matthias@urlichs.de> Mon, 06 Jan 2025 10:42:05 +0100

asyncclick (1:8.1.7-2) unstable; urgency=medium

* Fixups

-- Matthias Urlichs <matthias@urlichs.de> Sun, 10 Mar 2024 10:28:31 +0100

asyncclick (1:8.1.3-2) unstable; urgency=medium

* new Upstream

-- Matthias Urlichs <matthias@urlichs.de> Tue, 07 Jun 2022 18:53:38 +0200

asyncclick (1:8.0.3-6) unstable; urgency=medium

* fix test

-- Matthias Urlichs <matthias@urlichs.de> Sat, 15 Jan 2022 22:22:34 +0100

asyncclick (1:8.0.3-5) unstable; urgency=medium

* enter_context => with_resource

-- Matthias Urlichs <matthias@urlichs.de> Sat, 15 Jan 2022 22:21:09 +0100

asyncclick (1:8.0.3-4) unstable; urgency=medium

* missed anyio markers

-- Matthias Urlichs <matthias@urlichs.de> Wed, 05 Jan 2022 11:44:55 +0100

asyncclick (1:8.0.3-3) unstable; urgency=medium

* missed a comflict marker

-- Matthias Urlichs <matthias@urlichs.de> Wed, 05 Jan 2022 11:43:07 +0100

asyncclick (1:8.0.3-2) unstable; urgency=medium

* Fixed PYTHONPATH for tests

-- Matthias Urlichs <matthias@urlichs.de> Wed, 05 Jan 2022 11:42:14 +0100

asyncclick (1:8.0.3-1) unstable; urgency=medium

* Merge 8.0.3

-- Matthias Urlichs <matthias@urlichs.de> Wed, 05 Jan 2022 11:33:57 +0100

asyncclick (1:7.0.90-1) unstable; urgency=medium

* Merge current 7.1

-- Matthias Urlichs <matthias@urlichs.de> Thu, 13 Feb 2020 12:27:50 +0100

asyncclick (1:7.0.4-2) unstable; urgency=medium

* move to anyio

-- Matthias Urlichs <matthias@urlichs.de> Fri, 21 Jun 2019 09:36:52 +0200

trio-click (1:7.0.3-1) unstable; urgency=medium

* Removed debug output mistakenly left in the code

-- Matthias Urlichs <matthias@urlichs.de> Sun, 27 Jan 2019 14:30:39 +0100

trio-click (1:7.0.2-1) unstable; urgency=medium

* Mistakenly imported the "real" click

-- Matthias Urlichs <matthias@urlichs.de> Mon, 07 Jan 2019 19:18:44 +0100

trio-click (1:7.0.1-3) unstable; urgency=medium

* Fix 3.6 compatibility

-- Matthias Urlichs <matthias@urlichs.de> Wed, 02 Jan 2019 15:56:11 +0100

trio-click (1:7.0.1-2) unstable; urgency=medium

* Add an async context manager

-- Matthias Urlichs <matthias@urlichs.de> Wed, 02 Jan 2019 15:49:40 +0100

trio-click (1:7.0.1-1) unstable; urgency=medium

* Merge to 7.0
* Switched to AnyIO (instead of directly using Trio).

-- Matthias Urlichs <matthias@urlichs.de> Thu, 20 Dec 2018 14:44:04 +0100

trio-click (1:7.0~dev5-1) unstable; urgency=medium

* fix version

-- Matthias Urlichs <matthias@urlichs.de> Tue, 12 Jun 2018 05:41:03 +0200

trio-click (1:7.0~dev4-1) unstable; urgency=medium

* Updated README

-- Matthias Urlichs <matthias@urlichs.de> Fri, 08 Jun 2018 12:09:19 +0200

trio-click (1:7.0~dev3-1) unstable; urgency=medium

* Merge to current Upstream.

-- Matthias Urlichs <matthias@urlichs.de> Mon, 28 May 2018 03:38:15 +0200

trio-click (7.0+trio-dev3-1) unstable; urgency=medium

* Merge to current Upstream.

-- Matthias Urlichs <matthias@urlichs.de> Mon, 28 May 2018 03:38:15 +0200

trio-click (7.0+trio-dev2-1) unstable; urgency=medium

* Merge to current Upstream.

-- Matthias Urlichs <matthias@urlichs.de> Wed, 11 Apr 2018 06:19:22 +0200

trio-click (7.0+trio-dev1-1) unstable; urgency=medium

* Updated version number to something trackable

-- Matthias Urlichs <matthias@urlichs.de> Sun, 28 Jan 2018 09:59:28 +0100

trio-click (7.0~dev0-1) smurf; urgency=low

* source package automatically created by stdeb 0.8.5

-- Matthias Urlichs <matthias@urlichs.de> Wed, 24 Jan 2018 22:45:37 +0100
1 change: 1 addition & 0 deletions debian/compat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
13
19 changes: 19 additions & 0 deletions debian/control
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Source: asyncclick
Maintainer: Matthias Urlichs <matthias@urlichs.de>
Section: python
Priority: optional
Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9),
python3-anyio,
python3-trio,
python3-pytest,
python3-pytest-runner,
Standards-Version: 3.9.6
Homepage: http://github.com/pallets/click

Package: python3-asyncclick
Architecture: all
Depends: ${misc:Depends}, ${python3:Depends},
python3-anyio,
Recommends: python3-trio
Description: A simple wrapper around optparse for powerful command line u

10 changes: 10 additions & 0 deletions debian/rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/make -f

# This file was automatically generated by stdeb 0.8.5 at
# Wed, 24 Jan 2018 22:45:36 +0100
export PYBUILD_NAME=asyncclick
%:
dh $@ --with python3 --buildsystem=pybuild
override_dh_auto_test:
env PYTHONPATH=src python3 -mpytest -sxv tests/

1 change: 1 addition & 0 deletions debian/source/format
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.0 (quilt)
4 changes: 4 additions & 0 deletions debian/watch
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# please also check http://pypi.debian.net/asyncclick/watch
version=3
opts=uversionmangle=s/(rc|a|b|c)/~$1/ \
http://pypi.debian.net/asyncclick/asyncclick-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
6 changes: 4 additions & 2 deletions docs/advanced.rst
Original file line number Diff line number Diff line change
@@ -166,6 +166,8 @@ whereas :func:`Context.forward` fills in the arguments from the current
command. Both accept the command as the first argument and everything else
is passed onwards as you would expect.

These methods are asynchrous.

Example:

.. click:example::
@@ -181,8 +183,8 @@ Example:
@click.option('--count', default=1)
@click.pass_context
def dist(ctx, count):
ctx.forward(test)
ctx.invoke(test, count=42)
await ctx.forward(test)
await ctx.invoke(test, count=42)

And what it looks like:

10 changes: 5 additions & 5 deletions docs/api.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
API
===

.. module:: click
.. module:: asyncclick

This part of the documentation lists the full API reference of all public
classes and functions.
@@ -31,7 +31,7 @@ Decorators

.. autofunction:: make_pass_decorator

.. autofunction:: click.decorators.pass_meta_key
.. autofunction:: asyncclick.decorators.pass_meta_key


Utilities
@@ -109,7 +109,7 @@ Context

.. autofunction:: get_current_context

.. autoclass:: click.core.ParameterSource
.. autoclass:: asyncclick.core.ParameterSource
:members:
:member-order: bysource

@@ -186,7 +186,7 @@ Shell Completion
See :doc:`/shell-completion` for information about enabling and
customizing Click's shell completion system.

.. currentmodule:: click.shell_completion
.. currentmodule:: asyncclick.shell_completion

.. autoclass:: CompletionItem

@@ -200,7 +200,7 @@ customizing Click's shell completion system.
Testing
-------

.. currentmodule:: click.testing
.. currentmodule:: asyncclick.testing

.. autoclass:: CliRunner
:members:
236 changes: 64 additions & 172 deletions docs/arguments.rst
Original file line number Diff line number Diff line change
@@ -5,195 +5,118 @@ Arguments

.. currentmodule:: click

Arguments work similarly to :ref:`options <options>` but are positional.
They also only support a subset of the features of options due to their
syntactical nature. Click will also not attempt to document arguments for
you and wants you to :ref:`document them manually <documenting-arguments>`
in order to avoid ugly help pages.
Arguments are:

* Are positional in nature.
* Similar to a limited version of :ref:`options <options>` that can take an arbitrary number of inputs
* :ref:`Documented manually <documenting-arguments>`.

Useful and often used kwargs are:

* ``default``: Passes a default.
* ``nargs``: Sets the number of arguments. Set to -1 to take an arbitrary number.

Basic Arguments
---------------

The most basic option is a simple string argument of one value. If no
type is provided, the type of the default value is used, and if no default
value is provided, the type is assumed to be :data:`STRING`.
A minimal :class:`click.Argument` solely takes one string argument: the name of the argument. This will assume the argument is required, has no default, and is of the type ``str``.

Example:

.. click:example::
@click.command()
@click.argument('filename')
def touch(filename):
def touch(filename: str):
"""Print FILENAME."""
click.echo(filename)

And what it looks like:
And from the command line:

.. click:run::
invoke(touch, args=['foo.txt'])

Variadic Arguments
------------------

The second most common version is variadic arguments where a specific (or
unlimited) number of arguments is accepted. This can be controlled with
the ``nargs`` parameter. If it is set to ``-1``, then an unlimited number
of arguments is accepted.
An argument may be assigned a :ref:`parameter type <parameter-types>`. If no type is provided, the type of the default value is used. If no default value is provided, the type is assumed to be :data:`STRING`.

The value is then passed as a tuple. Note that only one argument can be
set to ``nargs=-1``, as it will eat up all arguments.
.. admonition:: Note on Required Arguments

Example:
It is possible to make an argument required by setting ``required=True``. It is not recommended since we think command line tools should gracefully degrade into becoming no ops. We think this because command line tools are often invoked with wildcard inputs and they should not error out if the wildcard is empty.

Multiple Arguments
-----------------------------------

To set the number of argument use the ``nargs`` kwarg. It can be set to any positive integer and -1. Setting it to -1, makes the number of arguments arbitrary (which is called variadic) and can only be used once. The arguments are then packed as a tuple and passed to the function.

.. click:example::
@click.command()
@click.argument('src', nargs=-1)
@click.argument('dst', nargs=1)
def copy(src, dst):
@click.argument('src', nargs=1)
@click.argument('dsts', nargs=-1)
def copy(src: str, dsts: tuple[str, ...]):
"""Move file SRC to DST."""
for fn in src:
click.echo(f"move {fn} to folder {dst}")
for destination in dsts:
click.echo(f"Copy {src} to folder {destination}")

And what it looks like:
And from the command line:

.. click:run::
invoke(copy, args=['foo.txt', 'bar.txt', 'my_folder'])

Note that this is not how you would write this application. The reason
for this is that in this particular example the arguments are defined as
strings. Filenames, however, are not strings! They might be on certain
operating systems, but not necessarily on all. For better ways to write
this, see the next sections.
invoke(copy, args=['foo.txt', 'usr/david/foo.txt', 'usr/mitsuko/foo.txt'])

.. admonition:: Note on Non-Empty Variadic Arguments
.. admonition:: Note on Handling Files

If you come from ``argparse``, you might be missing support for setting
``nargs`` to ``+`` to indicate that at least one argument is required.
This is not how you should handle files and files paths. This merely used as a simple example. See :ref:`handling-files` to learn more about how to handle files in parameters.

This is supported by setting ``required=True``. However, this should
not be used if you can avoid it as we believe scripts should gracefully
degrade into becoming noops if a variadic argument is empty. The
reason for this is that very often, scripts are invoked with wildcard
inputs from the command line and they should not error out if the
wildcard is empty.
Argument Escape Sequences
---------------------------

.. _file-args:
If you want to process arguments that look like options, like a file named ``-foo.txt`` or ``--foo.txt`` , you must pass the ``--`` separator first. After you pass the ``--``, you may only pass arguments. This is a common feature for POSIX command line tools.

File Arguments
--------------

Since all the examples have already worked with filenames, it makes sense
to explain how to deal with files properly. Command line tools are more
fun if they work with files the Unix way, which is to accept ``-`` as a
special file that refers to stdin/stdout.

Click supports this through the :class:`click.File` type which
intelligently handles files for you. It also deals with Unicode and bytes
correctly for all versions of Python so your script stays very portable.

Example:
Example usage:

.. click:example::
@click.command()
@click.argument('input', type=click.File('rb'))
@click.argument('output', type=click.File('wb'))
def inout(input, output):
"""Copy contents of INPUT to OUTPUT."""
while True:
chunk = input.read(1024)
if not chunk:
break
output.write(chunk)

And what it does:

.. click:run::
@click.argument('files', nargs=-1, type=click.Path())
def touch(files):
"""Print all FILES file names."""
for filename in files:
click.echo(filename)

with isolated_filesystem():
invoke(inout, args=['-', 'hello.txt'], input=['hello'],
terminate_input=True)
invoke(inout, args=['hello.txt', '-'])
And from the command line:

File Path Arguments
-------------------
.. click:run::
In the previous example, the files were opened immediately. But what if
we just want the filename? The naïve way is to use the default string
argument type. The :class:`Path` type has several checks available which raise nice
errors if they fail, such as existence. Filenames in these error messages are formatted
with :func:`format_filename`, so any undecodable bytes will be printed nicely.
invoke(touch, ['--', '-foo.txt', 'bar.txt'])

Example:
If you don't like the ``--`` marker, you can set ignore_unknown_options to True to avoid checking unknown options:

.. click:example::
@click.command()
@click.argument('filename', type=click.Path(exists=True))
def touch(filename):
"""Print FILENAME if the file exists."""
click.echo(click.format_filename(filename))
@click.command(context_settings={"ignore_unknown_options": True})
@click.argument('files', nargs=-1, type=click.Path())
def touch(files):
"""Print all FILES file names."""
for filename in files:
click.echo(filename)

And what it does:
And from the command line:

.. click:run::
with isolated_filesystem():
with open('hello.txt', 'w') as f:
f.write('Hello World!\n')
invoke(touch, args=['hello.txt'])
println()
invoke(touch, args=['missing.txt'])


File Opening Safety
-------------------

The :class:`FileType` type has one problem it needs to deal with, and that
is to decide when to open a file. The default behavior is to be
"intelligent" about it. What this means is that it will open stdin/stdout
and files opened for reading immediately. This will give the user direct
feedback when a file cannot be opened, but it will only open files
for writing the first time an IO operation is performed by automatically
wrapping the file in a special wrapper.

This behavior can be forced by passing ``lazy=True`` or ``lazy=False`` to
the constructor. If the file is opened lazily, it will fail its first IO
operation by raising an :exc:`FileError`.

Since files opened for writing will typically immediately empty the file,
the lazy mode should only be disabled if the developer is absolutely sure
that this is intended behavior.

Forcing lazy mode is also very useful to avoid resource handling
confusion. If a file is opened in lazy mode, it will receive a
``close_intelligently`` method that can help figure out if the file
needs closing or not. This is not needed for parameters, but is
necessary for manually prompting with the :func:`prompt` function as you
do not know if a stream like stdout was opened (which was already open
before) or a real file that needs closing.

Starting with Click 2.0, it is also possible to open files in atomic mode by
passing ``atomic=True``. In atomic mode, all writes go into a separate
file in the same folder, and upon completion, the file will be moved over to
the original location. This is useful if a file regularly read by other
users is modified.
invoke(touch, ['-foo.txt', 'bar.txt'])


.. _environment-variables:

Environment Variables
---------------------

Like options, arguments can also grab values from an environment variable.
Unlike options, however, this is only supported for explicitly named
environment variables.
Arguments can use environment variables. To do so, pass the name(s) of the environment variable(s) via `envvar` in ``click.argument``.

Example usage:
Checking one environment variable:

.. click:example::
@@ -208,59 +131,28 @@ And from the command line:
.. click:run::
with isolated_filesystem():
# Writing the file in the filesystem.
with open('hello.txt', 'w') as f:
f.write('Hello World!')
invoke(echo, env={'SRC': 'hello.txt'})

In that case, it can also be a list of different environment variables
where the first one is picked.

Generally, this feature is not recommended because it can cause the user
a lot of confusion.

Option-Like Arguments
---------------------

Sometimes, you want to process arguments that look like options. For
instance, imagine you have a file named ``-foo.txt``. If you pass this as
an argument in this manner, Click will treat it as an option.

To solve this, Click does what any POSIX style command line script does,
and that is to accept the string ``--`` as a separator for options and
arguments. After the ``--`` marker, all further parameters are accepted as
arguments.

Example usage:
Checking multiple environment variables:

.. click:example::
@click.command()
@click.argument('files', nargs=-1, type=click.Path())
def touch(files):
"""Print all FILES file names."""
for filename in files:
click.echo(filename)

And from the command line:

.. click:run::
invoke(touch, ['--', '-foo.txt', 'bar.txt'])

If you don't like the ``--`` marker, you can set ignore_unknown_options to
True to avoid checking unknown options:

.. click:example::
@click.command(context_settings={"ignore_unknown_options": True})
@click.argument('files', nargs=-1, type=click.Path())
def touch(files):
"""Print all FILES file names."""
for filename in files:
click.echo(filename)
@click.argument('src', envvar=['SRC', 'SRC_2'], type=click.File('r'))
def echo(src):
"""Print value of SRC environment variable."""
click.echo(src.read())

And from the command line:

.. click:run::
invoke(touch, ['-foo.txt', 'bar.txt'])
with isolated_filesystem():
# Writing the file in the filesystem.
with open('hello.txt', 'w') as f:
f.write('Hello World from second variable!')
invoke(echo, env={'SRC_2': 'hello.txt'})
8 changes: 4 additions & 4 deletions docs/commands.rst
Original file line number Diff line number Diff line change
@@ -192,7 +192,7 @@ A custom multi command just needs to implement a list and load method:

.. click:example::
import click
import asyncclick as click
import os

plugin_folder = os.path.join(os.path.dirname(__file__), 'commands')
@@ -245,7 +245,7 @@ Example usage:

.. click:example::
import click
import asyncclick as click

@click.group()
def cli1():
@@ -457,7 +457,7 @@ Example usage:

.. click:example::
import click
import asyncclick as click

@click.group()
def cli():
@@ -499,7 +499,7 @@ This example does the same as the previous example:

.. click:example::
import click
import asyncclick as click

CONTEXT_SETTINGS = dict(
default_map={'runserver': {'port': 5000}}
12 changes: 6 additions & 6 deletions docs/complex.rst
Original file line number Diff line number Diff line change
@@ -77,7 +77,7 @@ state of our tool:
.. click:example::
import os
import click
import asyncclick as click


class Repo(object):
@@ -249,7 +249,7 @@ stores a mapping from subcommand names to the information for importing them.
# in lazy_group.py
import importlib
import click
import asyncclick as click
class LazyGroup(click.Group):
def __init__(self, *args, lazy_subcommands=None, **kwargs):
@@ -295,7 +295,7 @@ subcommands like so:
.. code-block:: python
# in main.py
import click
import asyncclick as click
from lazy_group import LazyGroup
@click.group(
@@ -307,14 +307,14 @@ subcommands like so:
pass
# in foo.py
import click
import asyncclick as click
@click.group(help="foo command for lazy example")
def cli():
pass
# in bar.py
import click
import asyncclick as click
from lazy_group import LazyGroup
@click.group(
@@ -326,7 +326,7 @@ subcommands like so:
pass
# in baz.py
import click
import asyncclick as click
@click.group(help="baz command for lazy example")
def cli():
37 changes: 37 additions & 0 deletions docs/contrib.rst
Original file line number Diff line number Diff line change
@@ -18,4 +18,41 @@ Please note that the quality and stability of those packages may be different
than Click itself. While published under a common organization, they are still
separate from Click and the Pallets maintainers.


Third-party projects
--------------------

Other projects that extend Click's features are available outside of the
click-contrib_ organization.

Some of the most popular and actively maintained are listed below:

========================================================== =========================================================================================== ================================================================================================= ======================================================================================================
Project Description Popularity Activity
========================================================== =========================================================================================== ================================================================================================= ======================================================================================================
`Typer <https://github.com/fastapi/typer>`_ Use Python type hints to create CLI apps. .. image:: https://img.shields.io/github/stars/fastapi/typer?label=%20&style=flat-square .. image:: https://img.shields.io/github/last-commit/fastapi/typer?label=%20&style=flat-square
:alt: GitHub stars :alt: Last commit
`rich-click <https://github.com/ewels/rich-click>`_ Format help outputwith Rich. .. image:: https://img.shields.io/github/stars/ewels/rich-click?label=%20&style=flat-square .. image:: https://img.shields.io/github/last-commit/ewels/rich-click?label=%20&style=flat-square
:alt: GitHub stars :alt: Last commit
`click-app <https://github.com/simonw/click-app>`_ Cookiecutter template for creating new CLIs. .. image:: https://img.shields.io/github/stars/simonw/click-app?label=%20&style=flat-square .. image:: https://img.shields.io/github/last-commit/simonw/click-app?label=%20&style=flat-square
:alt: GitHub stars :alt: Last commit
`Cloup <https://github.com/janluke/cloup>`_ Adds option groups, constraints, command aliases, help themes, suggestions and more. .. image:: https://img.shields.io/github/stars/janluke/cloup?label=%20&style=flat-square .. image:: https://img.shields.io/github/last-commit/janluke/cloup?label=%20&style=flat-square
:alt: GitHub stars :alt: Last commit
`Click Extra <https://github.com/kdeldycke/click-extra>`_ Cloup + colorful ``--help``, ``--config``, ``--show-params``, ``--verbosity`` options, etc. .. image:: https://img.shields.io/github/stars/kdeldycke/click-extra?label=%20&style=flat-square .. image:: https://img.shields.io/github/last-commit/kdeldycke/click-extra?label=%20&style=flat-square
:alt: GitHub stars :alt: Last commit
========================================================== =========================================================================================== ================================================================================================= ======================================================================================================

.. note::

To make it into the list above, a project:

- must be actively maintained (at least one commit in the last year)
- must have a reasonable number of stars (at least 20)

If you have a project that meets these criteria, please open a pull request
to add it to the list.

If a project is no longer maintained or does not meet the criteria above,
please open a pull request to remove it from the list.

.. _click-contrib: https://github.com/click-contrib/
6 changes: 3 additions & 3 deletions docs/exceptions.rst
Original file line number Diff line number Diff line change
@@ -33,9 +33,9 @@ Generally you always have the option to invoke the :meth:`invoke` method
yourself. For instance if you have a :class:`Command` you can invoke it
manually like this::

ctx = command.make_context('command-name', ['args', 'go', 'here'])
ctx = await command.make_context('command-name', ['args', 'go', 'here'])
with ctx:
result = command.invoke(ctx)
result = await command.invoke(ctx)

In this case exceptions will not be handled at all and bubbled up as you
would expect.
@@ -46,7 +46,7 @@ exception handling and disable the implicit :meth:`sys.exit` at the end.

So you can do something like this::

command.main(['command-name', 'args', 'go', 'here'],
await command.main(['command-name', 'args', 'go', 'here'],
standalone_mode=False)

Which Exceptions Exist?
90 changes: 90 additions & 0 deletions docs/handling-files.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
.. _handling-files:

Handling Files
================

.. currentmodule:: click

Click has built in features to support file and file path handling. The examples use arguments but the same principle applies to options as well.

.. _file-args:

File Arguments
-----------------

Click supports working with files with the :class:`File` type. Some notable features are:

* Support for ``-`` to mean a special file that refers to stdin when used for reading, and stdout when used for writing. This is a common pattern for POSIX command line utilities.
* Deals with ``str`` and ``bytes`` correctly for all versions of Python.

Example:

.. click:example::
@click.command()
@click.argument('input', type=click.File('rb'))
@click.argument('output', type=click.File('wb'))
def inout(input, output):
"""Copy contents of INPUT to OUTPUT."""
while True:
chunk = input.read(1024)
if not chunk:
break
output.write(chunk)

And from the command line:

.. click:run::
with isolated_filesystem():
invoke(inout, args=['-', 'hello.txt'], input=['hello'],
terminate_input=True)
invoke(inout, args=['hello.txt', '-'])

File Path Arguments
----------------------

For handling paths, the :class:`Path` type is better than a ``str``. Some notable features are:

* The ``exists`` argument will verify whether the path exists.
* ``readable``, ``writable``, and ``executable`` can perform permission checks.
* ``file_okay`` and ``dir_okay`` allow specifying whether files/directories are accepted.
* Error messages are nicely formatted using :func:`format_filename` so any undecodable bytes will be printed nicely.

See :class:`Path` for all features.

Example:

.. click:example::
@click.command()
@click.argument('filename', type=click.Path(exists=True))
def touch(filename):
"""Print FILENAME if the file exists."""
click.echo(click.format_filename(filename))

And from the command line:

.. click:run::
with isolated_filesystem():
with open('hello.txt', 'w') as f:
f.write('Hello World!\n')
invoke(touch, args=['hello.txt'])
println()
invoke(touch, args=['missing.txt'])


File Opening Behaviors
-----------------------------

The :class:`File` type attempts to be "intelligent" about when to open a file. Stdin/stdout and files opened for reading will be opened immediately. This will give the user direct feedback when a file cannot be opened. Files opened for writing will only be open on the first IO operation. This is done by automatically wrapping the file in a special wrapper.

File open behavior can be controlled by the boolean kwarg ``lazy``. If a file is opened lazily:

* A failure at first IO operation will happen by raising an :exc:`FileError`.
* It can help minimize resource handling confusion. If a file is opened in lazy mode, it will call :meth:`LazyFile.close_intelligently` to help figure out if the file needs closing or not. This is not needed for parameters, but is necessary for manually prompting. For manual prompts with the :func:`prompt` function you do not know if a stream like stdout was opened (which was already open before) or a real file was opened (that needs closing).

Since files opened for writing will typically empty the file, the lazy mode should only be disabled if the developer is absolutely sure that this is intended behavior.

It is also possible to open files in atomic mode by passing ``atomic=True``. In atomic mode, all writes go into a separate file in the same folder, and upon completion, the file will be moved over to the original location. This is useful if a file regularly read by other users is modified.
19 changes: 12 additions & 7 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
Welcome to the AsyncClick Documentation
==================================
.. rst-class:: hide-header

Welcome to Click
================

.. image:: _static/click-logo.png
:align: center
:scale: 50%
:target: https://palletsprojects.com/p/click/

Click is a Python package for creating beautiful command line interfaces
AsyncClick is a fork of Click that works well with (some) async
frameworks. Supported: asyncio, trio, and curio.

Click, in turn, 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.
@@ -27,15 +29,17 @@ What does it look like? Here is an example of a simple Click program:

.. click:example::
import click
import asyncclick as click
import anyio

@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):
async def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for x in range(count):
if x: await anyio.sleep(0.1)
click.echo(f"Hello {name}!")

if __name__ == '__main__':
@@ -55,7 +59,7 @@ It automatically generates nicely formatted help pages:

You can get the library directly from PyPI::

pip install click
pip install asyncclick

Documentation
-------------
@@ -75,6 +79,7 @@ usage patterns.
arguments
commands
prompts
handling-files
documentation
complex
advanced
4 changes: 3 additions & 1 deletion docs/parameters.rst
Original file line number Diff line number Diff line change
@@ -53,6 +53,8 @@ And what it looks like when run:
invoke(multi_echo, ['--times=3', 'index.txt'], prog_name='multi_echo')

.. _parameter-types:

Parameter Types
---------------

@@ -106,7 +108,7 @@ integers.

.. code-block:: python
import click
import asyncclick as click
class BasedIntParamType(click.ParamType):
name = "integer"
2 changes: 1 addition & 1 deletion docs/quickstart.rst
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ with this decorator will make it into a callable script:

.. click:example::
import click
import asyncclick as click

@click.command()
def hello():
2 changes: 1 addition & 1 deletion docs/setuptools.rst
Original file line number Diff line number Diff line change
@@ -54,7 +54,7 @@ Contents of ``yourscript.py``:

.. click:example::
import click
import asyncclick as click

@click.command()
def cli():
6 changes: 3 additions & 3 deletions docs/shell-completion.rst
Original file line number Diff line number Diff line change
@@ -217,8 +217,8 @@ 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
from asyncclick.shell_completion import add_completion_class
from asyncclick.shell_completion import ShellComplete
_mysh_source = """\
%(complete_func)s {
@@ -243,7 +243,7 @@ method must return a ``(args, incomplete)`` tuple.
.. code-block:: python
import os
from click.parser import split_arg_string
from asyncclick.parser import split_arg_string
class MyshComplete(ShellComplete):
...
49 changes: 28 additions & 21 deletions docs/testing.rst
Original file line number Diff line number Diff line change
@@ -19,28 +19,33 @@ The basic functionality for testing Click applications is the
:meth:`CliRunner.invoke` method runs the command line script in isolation
and captures the output as both bytes and binary data.

Note that :meth:`CliRunner.invoke` is asynchronous. The :func:`runner`
fixture, which most Click tests use, contains a synchronous :attr:`invoke`
for your convenience.

The return value is a :class:`Result` object, which has the captured output
data, exit code, and optional exception attached:

.. code-block:: python
:caption: hello.py
import click
import asyncclick as click
@click.command()
@click.argument('name')
def hello(name):
async def hello(name):
click.echo(f'Hello {name}!')
.. code-block:: python
:caption: test_hello.py
from click.testing import CliRunner
from asyncclick.testing import CliRunner
from hello import hello
def test_hello_world():
@pytest.mark.anyio
async def test_hello_world():
runner = CliRunner()
result = runner.invoke(hello, ['Peter'])
result = await runner.invoke(hello, ['Peter'])
assert result.exit_code == 0
assert result.output == 'Hello Peter!\n'
@@ -49,26 +54,27 @@ For subcommand testing, a subcommand name must be specified in the `args` parame
.. code-block:: python
:caption: sync.py
import click
import asyncclick as click
@click.group()
@click.option('--debug/--no-debug', default=False)
def cli(debug):
async def cli(debug):
click.echo(f"Debug mode is {'on' if debug else 'off'}")
@cli.command()
def sync():
async def sync():
click.echo('Syncing')
.. code-block:: python
:caption: test_sync.py
from click.testing import CliRunner
from asyncclick.testing import CliRunner
from sync import cli
def test_sync():
@pytest.mark.anyio
async def test_sync():
runner = CliRunner()
result = runner.invoke(cli, ['--debug', 'sync'])
result = await runner.invoke(cli, ['--debug', 'sync'])
assert result.exit_code == 0
assert 'Debug mode is on' in result.output
assert 'Syncing' in result.output
@@ -77,7 +83,7 @@ Additional keyword arguments passed to ``.invoke()`` will be used to construct t
For example, if you want to run your tests against a fixed terminal width you can use the following::

runner = CliRunner()
result = runner.invoke(cli, ['--debug', 'sync'], terminal_width=60)
result = await runner.invoke(cli, ['--debug', 'sync'], terminal_width=60)

File System Isolation
---------------------
@@ -89,26 +95,27 @@ current working directory to a new, empty folder.
.. code-block:: python
:caption: cat.py
import click
import asyncclick as click
@click.command()
@click.argument('f', type=click.File())
def cat(f):
async def cat(f):
click.echo(f.read())
.. code-block:: python
:caption: test_cat.py
from click.testing import CliRunner
from asyncclick.testing import CliRunner
from cat import cat
def test_cat():
@pytest.mark.anyio
async def test_cat():
runner = CliRunner()
with runner.isolated_filesystem():
with open('hello.txt', 'w') as f:
f.write('Hello World!')
result = runner.invoke(cat, ['hello.txt'])
result = await runner.invoke(cat, ['hello.txt'])
assert result.exit_code == 0
assert result.output == 'Hello World!\n'
@@ -134,22 +141,22 @@ stream (stdin). This is very useful for testing prompts, for instance:
.. code-block:: python
:caption: prompt.py
import click
import asyncclick as click
@click.command()
@click.option('--foo', prompt=True)
def prompt(foo):
async def prompt(foo):
click.echo(f"foo={foo}")
.. code-block:: python
:caption: test_prompt.py
from click.testing import CliRunner
from asyncclick.testing import CliRunner
from prompt import prompt
def test_prompts():
runner = CliRunner()
result = runner.invoke(prompt, input='wau wau\n')
result = await runner.invoke(prompt, input='wau wau\n')
assert not result.exception
assert result.output == 'Foo: wau wau\nfoo=wau wau\n'
19 changes: 18 additions & 1 deletion docs/upgrading.rst
Original file line number Diff line number Diff line change
@@ -6,6 +6,23 @@ this is not entirely possible. In case we need to break backwards
compatibility this document gives you information about how to upgrade or
handle backwards compatibility properly.

.. _upgrade-to-anyio:

Upgrading to asyncclick
-----------------------

The anyio-compatible version of Click is mostly backwards compatible.

Several methods, most notably :meth:`BaseCommand.main` and
:meth:`Context.invoke`, are now asynchronous.
The :meth:`BaseCommand.__call__` alias invokes the main entry point via
`anyio.run`. If you already have an async main program, simply use
``await cmd.main()`` instead of ``cmd()``.

Commands and callbacks may be asynchronous; Click auto-``await``s them.
Support for Python 2.x was dropped.
.. _upgrade-to-7.0:
Upgrading to 7.0
@@ -110,7 +127,7 @@ to upgrade.
In case you want to support both Click 1.0 and Click 2.0, you can make a
simple decorator that adjusts the signatures::

import click
import asyncclick as click
from functools import update_wrapper

def compatcallback(f):
22 changes: 11 additions & 11 deletions docs/utils.rst
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ that it works the same in many different terminal environments.

Example::

import click
import asyncclick as click

click.echo('Hello World!')

@@ -69,7 +69,7 @@ can still call that in your code, but it's not required for Click.

For styling a string, the :func:`style` function can be used::

import click
import asyncclick as click

click.echo(click.style('Hello World!', fg='green'))
click.echo(click.style('Some more text', bg='blue', fg='white'))
@@ -124,7 +124,7 @@ clears the entire visible screen in a platform-agnostic way:

::

import click
import asyncclick as click
click.clear()


@@ -146,7 +146,7 @@ is instead a pipe.

Example::

import click
import asyncclick as click

click.echo('Continue? [yn] ', nl=False)
c = click.getchar()
@@ -184,7 +184,7 @@ run interactively.

Example::

import click
import asyncclick as click
click.pause()


@@ -201,7 +201,7 @@ will be ``None``, otherwise the entered text.

Example usage::

import click
import asyncclick as click

def get_commit_message():
MARKER = '# Everything below is ignored\n'
@@ -214,7 +214,7 @@ a specific filename. In this case, the return value is always `None`.

Example usage::

import click
import asyncclick as click
click.edit(filename='/etc/passwd')


@@ -269,7 +269,7 @@ stream object (except in very odd cases; see :doc:`/unicode-support`).

Example::

import click
import asyncclick as click

stdin_text = click.get_text_stream('stdin')
stdout_binary = click.get_binary_stream('stdout')
@@ -292,7 +292,7 @@ intelligently open stdin/stdout as well as any other file.

Example::

import click
import asyncclick as click

stdout = click.open_file('-', 'w')
test_file = click.open_file('test.txt', 'w')
@@ -320,7 +320,7 @@ for per-user config files for your application depending on the OS.
Example usage::

import os
import click
import asyncclick as click
import ConfigParser

APP_NAME = 'My Application'
@@ -361,7 +361,7 @@ time to do processing. So say you have a loop like this::
To hook this up with an automatically updating progress bar, all you need
to do is to change the code to this::

import click
import asyncclick as click

with click.progressbar(all_the_users_to_process) as bar:
for user in bar:
2 changes: 1 addition & 1 deletion examples/aliases/aliases.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import configparser
import os

import click
import asyncclick as click


class Config:
2 changes: 1 addition & 1 deletion examples/colors/colors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import click
import asyncclick as click


all_colors = (
4 changes: 2 additions & 2 deletions examples/completion/completion.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os

import click
from click.shell_completion import CompletionItem
import asyncclick as click
from asyncclick.shell_completion import CompletionItem


@click.group()
2 changes: 1 addition & 1 deletion examples/complex/complex/cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import sys

import click
import asyncclick as click


CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX")
2 changes: 1 addition & 1 deletion examples/complex/complex/commands/cmd_init.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from complex.cli import pass_environment

import click
import asyncclick as click


@click.command("init", short_help="Initializes a repo.")
2 changes: 1 addition & 1 deletion examples/complex/complex/commands/cmd_status.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from complex.cli import pass_environment

import click
import asyncclick as click


@click.command("status", short_help="Shows file changes.")
2 changes: 1 addition & 1 deletion examples/imagepipe/imagepipe.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
from PIL import ImageEnhance
from PIL import ImageFilter

import click
import asyncclick as click


@click.group(chain=True)
2 changes: 1 addition & 1 deletion examples/inout/inout.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import click
import asyncclick as click


@click.command()
2 changes: 1 addition & 1 deletion examples/naval/naval.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import click
import asyncclick as click


@click.group()
2 changes: 1 addition & 1 deletion examples/repo/repo.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
import posixpath
import sys

import click
import asyncclick as click


class Repo:
2 changes: 1 addition & 1 deletion examples/termui/termui.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
import random
import time

import click
import asyncclick as click


@click.group()
2 changes: 1 addition & 1 deletion examples/validation/validation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from urllib import parse as urlparse

import click
import asyncclick as click


def validate_count(ctx, param, value):
18 changes: 9 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "click"
description = "Composable command line interface toolkit"
name = "asyncclick"
description = "Composable command line interface toolkit, "
readme = "README.md"
license = {file = "LICENSE.txt"}
maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}]
@@ -12,26 +12,26 @@ classifiers = [
"Programming Language :: Python",
"Typing :: Typed",
]
requires-python = ">=3.7"
requires-python = ">=3.9"
dependencies = [
"colorama; platform_system == 'Windows'",
"importlib-metadata; python_version < '3.8'",
"anyio ~= 4.0",
]
dynamic = ["version"]

[project.urls]
Donate = "https://palletsprojects.com/donate"
Documentation = "https://click.palletsprojects.com/"
Changes = "https://click.palletsprojects.com/changes/"
Source = "https://github.com/pallets/click/"
Source = "https://github.com/python-trio/asyncclick"
Chat = "https://discord.gg/pallets"

[build-system]
requires = ["flit_core<4"]
build-backend = "flit_core.buildapi"

[tool.flit.module]
name = "click"
name = "asyncclick"

[tool.flit.sdist]
include = [
@@ -53,14 +53,14 @@ filterwarnings = [

[tool.coverage.run]
branch = true
source = ["click", "tests"]
source = ["asyncclick", "tests"]

[tool.coverage.paths]
source = ["src", "*/site-packages"]

[tool.mypy]
python_version = "3.8"
files = ["src/click", "tests/typing"]
files = ["src/asyncclick", "tests/typing"]
show_error_codes = true
pretty = true
strict = true
@@ -73,7 +73,7 @@ ignore_missing_imports = true

[tool.pyright]
pythonVersion = "3.8"
include = ["src/click", "tests/typing"]
include = ["src/asyncclick", "tests/typing"]
typeCheckingMode = "basic"

[tool.ruff]
13 changes: 9 additions & 4 deletions requirements/build.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile build.in
# pip-compile requirements/build.in
#
--trusted-host pypi.python.org
--trusted-host pypi.org
--trusted-host files.pythonhosted.org
--trusted-host pypi01vp.office.noris.de

build==1.2.2.post1
# via -r build.in
packaging==24.1
# via -r requirements/build.in
packaging==24.2
# via build
pyproject-hooks==1.2.0
# via build
2 changes: 2 additions & 0 deletions requirements/dev.in
Original file line number Diff line number Diff line change
@@ -4,3 +4,5 @@
pip-compile-multi
pre-commit
tox
anyio
trio
69 changes: 48 additions & 21 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile dev.in
# pip-compile requirements/dev.in
#
--trusted-host pypi.python.org
--trusted-host pypi.org
--trusted-host files.pythonhosted.org
--trusted-host pypi01vp.office.noris.de

alabaster==1.0.0
# via sphinx
anyio==4.8.0
# via -r requirements/dev.in
attrs==24.3.0
# via
# outcome
# trio
babel==2.16.0
# via sphinx
build==1.2.2.post1
@@ -36,10 +47,13 @@ filelock==3.16.1
# via
# tox
# virtualenv
identify==2.6.1
identify==2.6.3
# via pre-commit
idna==3.10
# via requests
# via
# anyio
# requests
# trio
imagesize==1.4.1
# via sphinx
iniconfig==2.0.0
@@ -49,14 +63,16 @@ jinja2==3.1.4
markupsafe==3.0.2
# via jinja2
mypy==1.13.0
# via -r typing.in
# via -r /src/asyncclick/requirements/typing.in
mypy-extensions==1.0.0
# via mypy
nodeenv==1.9.1
# via
# pre-commit
# pyright
packaging==24.1
outcome==1.3.0.post0
# via trio
packaging==24.2
# via
# build
# pallets-sphinx-themes
@@ -65,9 +81,9 @@ packaging==24.1
# sphinx
# tox
pallets-sphinx-themes==2.3.0
# via -r docs.in
pip-compile-multi==2.6.4
# via -r dev.in
# via -r /src/asyncclick/requirements/docs.in
pip-compile-multi==2.7.1
# via -r requirements/dev.in
pip-tools==7.4.1
# via pip-compile-multi
platformdirs==4.3.6
@@ -79,7 +95,7 @@ pluggy==1.5.0
# pytest
# tox
pre-commit==4.0.1
# via -r dev.in
# via -r requirements/dev.in
pygments==2.18.0
# via
# sphinx
@@ -90,30 +106,36 @@ pyproject-hooks==1.2.0
# via
# build
# pip-tools
pyright==1.1.386
# via -r typing.in
pytest==8.3.3
# via -r tests.in
pyright==1.1.390
# via -r /src/asyncclick/requirements/typing.in
pytest==8.3.4
# via -r /src/asyncclick/requirements/tests.in
pyyaml==6.0.2
# via pre-commit
requests==2.32.3
# via sphinx
sniffio==1.3.1
# via
# anyio
# trio
snowballstemmer==2.2.0
# via sphinx
sortedcontainers==2.4.0
# via trio
sphinx==8.1.3
# via
# -r docs.in
# -r /src/asyncclick/requirements/docs.in
# pallets-sphinx-themes
# sphinx-issues
# sphinx-notfound-page
# sphinx-tabs
# sphinxcontrib-log-cabinet
sphinx-issues==5.0.0
# via -r docs.in
# via -r /src/asyncclick/requirements/docs.in
sphinx-notfound-page==1.0.4
# via pallets-sphinx-themes
sphinx-tabs==3.4.7
# via -r docs.in
# via -r /src/asyncclick/requirements/docs.in
sphinxcontrib-applehelp==2.0.0
# via sphinx
sphinxcontrib-devhelp==2.0.0
@@ -123,26 +145,31 @@ sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-log-cabinet==1.0.1
# via -r docs.in
# via -r /src/asyncclick/requirements/docs.in
sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
# via sphinx
toposort==1.10
# via pip-compile-multi
tox==4.23.2
# via -r dev.in
# via -r requirements/dev.in
trio==0.28.0
# via
# -r /src/asyncclick/requirements/tests.in
# -r requirements/dev.in
typing-extensions==4.12.2
# via
# anyio
# mypy
# pyright
urllib3==2.2.3
# via requests
virtualenv==20.27.0
virtualenv==20.28.0
# via
# pre-commit
# tox
wheel==0.44.0
wheel==0.45.1
# via pip-tools

# The following packages are considered to be unsafe in a requirements file:
21 changes: 13 additions & 8 deletions requirements/docs.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile docs.in
# pip-compile requirements/docs.in
#
--trusted-host pypi.python.org
--trusted-host pypi.org
--trusted-host files.pythonhosted.org
--trusted-host pypi01vp.office.noris.de

alabaster==1.0.0
# via sphinx
babel==2.16.0
@@ -24,12 +29,12 @@ jinja2==3.1.4
# via sphinx
markupsafe==3.0.2
# via jinja2
packaging==24.1
packaging==24.2
# via
# pallets-sphinx-themes
# sphinx
pallets-sphinx-themes==2.3.0
# via -r docs.in
# via -r requirements/docs.in
pygments==2.18.0
# via
# sphinx
@@ -40,18 +45,18 @@ snowballstemmer==2.2.0
# via sphinx
sphinx==8.1.3
# via
# -r docs.in
# -r requirements/docs.in
# pallets-sphinx-themes
# sphinx-issues
# sphinx-notfound-page
# sphinx-tabs
# sphinxcontrib-log-cabinet
sphinx-issues==5.0.0
# via -r docs.in
# via -r requirements/docs.in
sphinx-notfound-page==1.0.4
# via pallets-sphinx-themes
sphinx-tabs==3.4.7
# via -r docs.in
# via -r requirements/docs.in
sphinxcontrib-applehelp==2.0.0
# via sphinx
sphinxcontrib-devhelp==2.0.0
@@ -61,7 +66,7 @@ sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-log-cabinet==1.0.1
# via -r docs.in
# via -r requirements/docs.in
sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
1 change: 1 addition & 0 deletions requirements/tests.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pytest
trio
29 changes: 24 additions & 5 deletions requirements/tests.txt
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile tests.in
# pip-compile requirements/tests.in
#
--trusted-host pypi.python.org
--trusted-host pypi.org
--trusted-host files.pythonhosted.org
--trusted-host pypi01vp.office.noris.de

attrs==24.3.0
# via
# outcome
# trio
idna==3.10
# via trio
iniconfig==2.0.0
# via pytest
packaging==24.1
outcome==1.3.0.post0
# via trio
packaging==24.2
# via pytest
pluggy==1.5.0
# via pytest
pytest==8.3.3
# via -r tests.in
pytest==8.3.4
# via -r requirements/tests.in
sniffio==1.3.1
# via trio
sortedcontainers==2.4.0
# via trio
trio==0.28.0
# via -r requirements/tests.in
15 changes: 10 additions & 5 deletions requirements/typing.txt
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile typing.in
# pip-compile requirements/typing.in
#
--trusted-host pypi.python.org
--trusted-host pypi.org
--trusted-host files.pythonhosted.org
--trusted-host pypi01vp.office.noris.de

mypy==1.13.0
# via -r typing.in
# via -r requirements/typing.in
mypy-extensions==1.0.0
# via mypy
nodeenv==1.9.1
# via pyright
pyright==1.1.386
# via -r typing.in
pyright==1.1.390
# via -r requirements/typing.in
typing-extensions==4.12.2
# via
# mypy
2 changes: 1 addition & 1 deletion src/click/__init__.py → src/asyncclick/__init__.py
Original file line number Diff line number Diff line change
@@ -72,4 +72,4 @@
from .utils import get_text_stream as get_text_stream
from .utils import open_file as open_file

__version__ = "8.1.7"
__version__ = "8.1.8.0"
File renamed without changes.
110 changes: 79 additions & 31 deletions src/click/_termui_impl.py → src/asyncclick/_termui_impl.py
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@
import typing as t
from gettext import gettext as _
from io import StringIO
from shutil import which
from types import TracebackType

from ._compat import _default_text_stdout
@@ -372,31 +373,42 @@ def pager(generator: t.Iterable[str], color: t.Optional[bool] = None) -> None:
pager_cmd = (os.environ.get("PAGER", None) or "").strip()
if pager_cmd:
if WIN:
return _tempfilepager(generator, pager_cmd, color)
return _pipepager(generator, pager_cmd, color)
if _tempfilepager(generator, pager_cmd, color):
return
elif _pipepager(generator, pager_cmd, color):
return
if os.environ.get("TERM") in ("dumb", "emacs"):
return _nullpager(stdout, generator, color)
if WIN or sys.platform.startswith("os2"):
return _tempfilepager(generator, "more <", color)
if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0:
return _pipepager(generator, "less", color)
if (WIN or sys.platform.startswith("os2")) and _tempfilepager(
generator, "more", color
):
return
if _pipepager(generator, "less", color):
return

import tempfile

fd, filename = tempfile.mkstemp()
os.close(fd)
try:
if hasattr(os, "system") and os.system(f'more "{filename}"') == 0:
return _pipepager(generator, "more", color)
if _pipepager(generator, "more", color):
return
return _nullpager(stdout, generator, color)
finally:
os.unlink(filename)


def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> None:
def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> bool:
"""Page through text by feeding it to another program. Invoking a
pager through this might support colors.
Returns True if the command was found, False otherwise and thus another
pager should be attempted.
"""
cmd_absolute = which(cmd)
if cmd_absolute is None:
return False

import subprocess

env = dict(os.environ)
@@ -412,19 +424,25 @@ def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) ->
elif "r" in less_flags or "R" in less_flags:
color = True

c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env)
stdin = t.cast(t.BinaryIO, c.stdin)
encoding = get_best_encoding(stdin)
c = subprocess.Popen(
[cmd_absolute],
shell=True,
stdin=subprocess.PIPE,
env=env,
errors="replace",
text=True,
)
assert c.stdin is not None
try:
for text in generator:
if not color:
text = strip_ansi(text)

stdin.write(text.encode(encoding, "replace"))
c.stdin.write(text)
except (OSError, KeyboardInterrupt):
pass
else:
stdin.close()
c.stdin.close()

# Less doesn't respect ^C, but catches it for its own UI purposes (aborting
# search or other commands inside less).
@@ -442,11 +460,25 @@ def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) ->
else:
break

return True


def _tempfilepager(
generator: t.Iterable[str], cmd: str, color: t.Optional[bool]
) -> None:
"""Page through text by invoking a program on a temporary file."""
generator: t.Iterable[str],
cmd: str,
color: t.Optional[bool],
) -> bool:
"""Page through text by invoking a program on a temporary file.
Returns True if the command was found, False otherwise and thus another
pager should be attempted.
"""
# Which is necessary for Windows, it is also recommended in the Popen docs.
cmd_absolute = which(cmd)
if cmd_absolute is None:
return False

import subprocess
import tempfile

fd, filename = tempfile.mkstemp()
@@ -458,11 +490,16 @@ def _tempfilepager(
with open_stream(filename, "wb")[0] as f:
f.write(text.encode(encoding))
try:
os.system(f'{cmd} "{filename}"')
subprocess.call([cmd_absolute, filename])
except OSError:
# Command not found
pass
finally:
os.close(fd)
os.unlink(filename)

return True


def _nullpager(
stream: t.TextIO, generator: t.Iterable[str], color: t.Optional[bool]
@@ -497,7 +534,7 @@ def get_editor(self) -> str:
if WIN:
return "notepad"
for editor in "sensible-editor", "vim", "nano":
if os.system(f"which {editor} >/dev/null 2>&1") == 0:
if which(editor) is not None:
return editor
return "vi"

@@ -596,22 +633,33 @@ def _unquote_file(url: str) -> str:
null.close()
elif WIN:
if locate:
url = _unquote_file(url.replace('"', ""))
args = f'explorer /select,"{url}"'
url = _unquote_file(url)
args = ["explorer", f"/select,{url}"]
else:
url = url.replace('"', "")
wait_str = "/WAIT" if wait else ""
args = f'start {wait_str} "" "{url}"'
return os.system(args)
args = ["start"]
if wait:
args.append("/WAIT")
args.append("")
args.append(url)
try:
return subprocess.call(args)
except OSError:
# Command not found
return 127
elif CYGWIN:
if locate:
url = os.path.dirname(_unquote_file(url).replace('"', ""))
args = f'cygstart "{url}"'
url = _unquote_file(url)
args = ["cygstart", os.path.dirname(url)]
else:
url = url.replace('"', "")
wait_str = "-w" if wait else ""
args = f'cygstart {wait_str} "{url}"'
return os.system(args)
args = ["cygstart"]
if wait:
args.append("-w")
args.append(url)
try:
return subprocess.call(args)
except OSError:
# Command not found
return 127

try:
if locate:
File renamed without changes.
File renamed without changes.
264 changes: 174 additions & 90 deletions src/click/core.py → src/asyncclick/core.py

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions src/click/decorators.py → src/asyncclick/decorators.py
Original file line number Diff line number Diff line change
@@ -72,8 +72,8 @@ def new_func(ctx, *args, **kwargs):
remembered on the context if it's not there yet.
"""

def decorator(f: "t.Callable[te.Concatenate[T, P], R]") -> "t.Callable[P, R]":
def new_func(*args: "P.args", **kwargs: "P.kwargs") -> "R":
def decorator(f: "t.Callable[te.Concatenate[T, P], R]") -> "t.Callable[P, t.Coroutine[t.Any, t.Any, R]]":
def new_func(*args: "P.args", **kwargs: "P.kwargs") -> "t.Coroutine[t.Any, t.Any, R]":
ctx = get_current_context()

obj: t.Optional[T]
@@ -111,8 +111,8 @@ def pass_meta_key(
.. versionadded:: 8.0
"""

def decorator(f: "t.Callable[te.Concatenate[t.Any, P], R]") -> "t.Callable[P, R]":
def new_func(*args: "P.args", **kwargs: "P.kwargs") -> R:
def decorator(f: "t.Callable[te.Concatenate[t.Any, P], R]") -> "t.Callable[P, t.Coroutine[t.Any, t.Any, R]]":
def new_func(*args: "P.args", **kwargs: "P.kwargs") -> "t.Coroutine[t.Any, t.Any, R]":
ctx = get_current_context()
obj = ctx.meta[key]
return ctx.invoke(f, obj, *args, **kwargs)
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/click/parser.py → src/asyncclick/parser.py
Original file line number Diff line number Diff line change
@@ -325,7 +325,7 @@ def add_argument(
"""
self._args.append(Argument(obj, dest=dest, nargs=nargs))

def parse_args(
async 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)``
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
from .utils import echo


def shell_complete(
async def shell_complete(
cli: BaseCommand,
ctx_args: t.MutableMapping[str, t.Any],
prog_name: str,
@@ -46,7 +46,7 @@ def shell_complete(
return 0

if instruction == "complete":
echo(comp.complete())
echo(await comp.complete())
return 0

return 1
@@ -260,7 +260,7 @@ def get_completion_args(self) -> t.Tuple[t.List[str], str]:
"""
raise NotImplementedError

def get_completions(
async def get_completions(
self, args: t.List[str], incomplete: str
) -> t.List[CompletionItem]:
"""Determine the context and last complete command or parameter
@@ -270,7 +270,7 @@ def get_completions(
: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)
ctx = await _resolve_context(self.cli, self.ctx_args, self.prog_name, args)
obj, incomplete = _resolve_incomplete(ctx, args, incomplete)
return obj.shell_complete(ctx, incomplete)

@@ -282,15 +282,15 @@ def format_completion(self, item: CompletionItem) -> str:
"""
raise NotImplementedError

def complete(self) -> str:
async 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)
completions = await self.get_completions(args, incomplete)
out = [self.format_completion(item) for item in completions]
return "\n".join(out)

@@ -303,12 +303,19 @@ class BashComplete(ShellComplete):

@staticmethod
def _check_version() -> None:
import shutil
import subprocess

output = subprocess.run(
["bash", "-c", 'echo "${BASH_VERSION}"'], stdout=subprocess.PIPE
)
match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode())
bash_exe = shutil.which("bash")

if bash_exe is None:
match = None
else:
output = subprocess.run(
[bash_exe, "--norc", "-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()
@@ -492,7 +499,7 @@ def _is_incomplete_option(ctx: Context, args: t.List[str], param: Parameter) ->
return last_option is not None and last_option in param.opts


def _resolve_context(
async def _resolve_context(
cli: BaseCommand,
ctx_args: t.MutableMapping[str, t.Any],
prog_name: str,
@@ -507,31 +514,31 @@ def _resolve_context(
: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)
ctx = await 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)
name, cmd, args = await command.resolve_command(ctx, args)

if cmd is None:
return ctx

ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True)
ctx = await cmd.make_context(name, args, parent=ctx, resilient_parsing=True)
args = ctx.protected_args + ctx.args
else:
sub_ctx = ctx

while args:
name, cmd, args = command.resolve_command(ctx, args)
name, cmd, args = await command.resolve_command(ctx, args)

if cmd is None:
return ctx

sub_ctx = cmd.make_context(
sub_ctx = await cmd.make_context(
name,
args,
parent=ctx,
File renamed without changes.
11 changes: 7 additions & 4 deletions src/click/testing.py → src/asyncclick/testing.py
Original file line number Diff line number Diff line change
@@ -17,7 +17,6 @@
if t.TYPE_CHECKING:
from .core import BaseCommand


class EchoingStdin:
def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None:
self._input = input
@@ -234,6 +233,7 @@ def isolation(
bytes_input = make_input_stream(input, self.charset)
echo_input = None

global o_stdin, o_stdout, o_stderr
old_stdin = sys.stdin
old_stdout = sys.stdout
old_stderr = sys.stderr
@@ -312,6 +312,7 @@ def should_strip_ansi(
old_hidden_prompt_func = termui.hidden_prompt_func
old__getchar_func = termui._getchar
old_should_strip_ansi = utils.should_strip_ansi # type: ignore
old__compat_should_strip_ansi = _compat.should_strip_ansi
termui.visible_prompt_func = visible_input
termui.hidden_prompt_func = hidden_input
termui._getchar = _getchar
@@ -339,16 +340,18 @@ def should_strip_ansi(
pass
else:
os.environ[key] = value
global o_stdin, o_stdout, o_stderr
sys.stdout = old_stdout
sys.stderr = old_stderr
sys.stdin = old_stdin
termui.visible_prompt_func = old_visible_prompt_func
termui.hidden_prompt_func = old_hidden_prompt_func
termui._getchar = old__getchar_func
utils.should_strip_ansi = old_should_strip_ansi # type: ignore
_compat.should_strip_ansi = old__compat_should_strip_ansi
formatting.FORCED_WIDTH = old_forced_width

def invoke(
async def invoke(
self,
cli: "BaseCommand",
args: t.Optional[t.Union[str, t.Sequence[str]]] = None,
@@ -407,7 +410,7 @@ def invoke(
prog_name = self.get_default_prog_name(cli)

try:
return_value = cli.main(args=args or (), prog_name=prog_name, **extra)
return_value = await cli.main(args=args or (), prog_name=prog_name, **extra)
except SystemExit as e:
exc_info = sys.exc_info()
e_code = t.cast(t.Optional[t.Union[int, t.Any]], e.code)
@@ -426,7 +429,7 @@ def invoke(
exit_code = e_code

except Exception as e:
if not catch_exceptions:
if not catch_exceptions:
raise
exception = e
exit_code = 1
49 changes: 26 additions & 23 deletions src/click/types.py → src/asyncclick/types.py
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@ class ParamType:
#: Windows).
envvar_list_splitter: t.ClassVar[t.Optional[str]] = None

def to_info_dict(self) -> t.Dict[str, t.Any]:
async def to_info_dict(self) -> t.Dict[str, t.Any]:
"""Gather information that could be useful for a tool generating
user-facing documentation.
@@ -167,8 +167,8 @@ def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None:
self.name: str = func.__name__
self.func = func

def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
async def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = await super().to_info_dict()
info_dict["func"] = self.func
return info_dict

@@ -247,8 +247,8 @@ def __init__(self, choices: t.Sequence[str], case_sensitive: bool = True) -> Non
self.choices = choices
self.case_sensitive = case_sensitive

def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
async def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = await super().to_info_dict()
info_dict["choices"] = self.choices
info_dict["case_sensitive"] = self.case_sensitive
return info_dict
@@ -318,7 +318,7 @@ def shell_complete(
.. versionadded:: 8.0
"""
from click.shell_completion import CompletionItem
from .shell_completion import CompletionItem

str_choices = map(str, self.choices)

@@ -361,8 +361,8 @@ def __init__(self, formats: t.Optional[t.Sequence[str]] = None):
"%Y-%m-%d %H:%M:%S",
]

def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
async def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = await super().to_info_dict()
info_dict["formats"] = self.formats
return info_dict

@@ -435,8 +435,8 @@ def __init__(
self.max_open = max_open
self.clamp = clamp

def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
async def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = await super().to_info_dict()
info_dict.update(
min=self.min,
max=self.max,
@@ -659,12 +659,15 @@ class File(ParamType):
will not be held open until first IO. lazy is mainly useful when opening
for writing to avoid creating the file until it is needed.
Starting with Click 2.0, files can also be opened atomically in which
case all writes go into a separate file in the same folder and upon
completion the file will be moved over to the original location. This
is useful if a file regularly read by other users is modified.
Files can also be opened atomically in which case all writes go into a
separate file in the same folder and upon completion the file will
be moved over to the original location. This is useful if a file
regularly read by other users is modified.
See :ref:`file-args` for more information.
.. versionchanged:: 2.0
Added the ``atomic`` parameter.
"""

name = "filename"
@@ -684,8 +687,8 @@ def __init__(
self.lazy = lazy
self.atomic = atomic

def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
async def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = await super().to_info_dict()
info_dict.update(mode=self.mode, encoding=self.encoding)
return info_dict

@@ -753,7 +756,7 @@ def shell_complete(
.. versionadded:: 8.0
"""
from click.shell_completion import CompletionItem
from .shell_completion import CompletionItem

return [CompletionItem(incomplete, type="file")]

@@ -826,8 +829,8 @@ def __init__(
else:
self.name = _("path")

def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
async def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = await super().to_info_dict()
info_dict.update(
exists=self.exists,
file_okay=self.file_okay,
@@ -941,7 +944,7 @@ def shell_complete(
.. versionadded:: 8.0
"""
from click.shell_completion import CompletionItem
from .shell_completion import CompletionItem

type = "dir" if self.dir_okay and not self.file_okay else "file"
return [CompletionItem(incomplete, type=type)]
@@ -964,9 +967,9 @@ class Tuple(CompositeParamType):
def __init__(self, types: t.Sequence[t.Union[t.Type[t.Any], ParamType]]) -> None:
self.types: t.Sequence[ParamType] = [convert_type(ty) for ty in types]

def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict()
info_dict["types"] = [t.to_info_dict() for t in self.types]
async def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = await super().to_info_dict()
info_dict["types"] = [await t.to_info_dict() for t in self.types]
return info_dict

@property
File renamed without changes.
24 changes: 22 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import pytest
import anyio
from functools import partial
from threading import Thread

from click.testing import CliRunner
from asyncclick.testing import CliRunner

class SyncCliRunner(CliRunner):
def invoke(self,*a,_sync=False,**k):
fn = super().invoke
if _sync:
return fn(*a,**k)

# anyio now protects against nested calls, so we use a thread
result = None
def f():
nonlocal result,fn
async def r():
return await fn(*a,**k)
result = anyio.run(r) ## , backend="trio")
t=Thread(target=f, name="TEST")
t.start()
t.join()
return result

@pytest.fixture(scope="function")
def runner(request):
return CliRunner()
return SyncCliRunner()
8 changes: 6 additions & 2 deletions tests/test_arguments.py
Original file line number Diff line number Diff line change
@@ -3,8 +3,9 @@

import pytest

import click
import asyncclick as click

PY2 = False # churn

def test_nargs_star(runner):
@click.command()
@@ -15,6 +16,8 @@ def copy(src, dst):
click.echo(f"dst={dst}")

result = runner.invoke(copy, ["foo.txt", "bar.txt", "dir"])
if result.exception:
raise result.exception
assert not result.exception
assert result.output.splitlines() == ["src=foo.txt|bar.txt", "dst=dir"]

@@ -357,7 +360,8 @@ 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
if result.exception:
raise result.exception
assert "Usage: cli NAME VAL cmd [OPTIONS]" in result.output


14 changes: 8 additions & 6 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@

import pytest

import click
import asyncclick as click


def test_basic_functionality(runner):
@@ -43,13 +43,14 @@ def subcommand():
assert repr(subcommand) == "<Command subcommand>"


def test_return_values():
@pytest.mark.anyio
async def test_return_values():
@click.command()
def cli():
async def cli():
return 42

with cli.make_context("foo", []) as ctx:
rv = cli.invoke(ctx)
async with await cli.make_context("foo", []) as ctx:
rv = await cli.invoke(ctx)
assert rv == 42


@@ -73,7 +74,8 @@ def subcommand():
assert "ROOT EXECUTED" not in result.output

result = runner.invoke(cli, ["subcommand"])
assert not result.exception
if result.exception:
raise result.exception
assert result.exit_code == 0
assert "ROOT EXECUTED" in result.output
assert "SUBCOMMAND EXECUTED" in result.output
2 changes: 1 addition & 1 deletion tests/test_chain.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

import pytest

import click
import asyncclick as click


def debug():
4 changes: 3 additions & 1 deletion tests/test_command_decorators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import click
import pytest

import asyncclick as click


def test_command_no_parens(runner):
209 changes: 179 additions & 30 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import re
from inspect import iscoroutine

import pytest

import click
import asyncclick as click


def test_other_command_invoke(runner):
@@ -17,11 +18,13 @@ def other_cmd(arg):
click.echo(arg)

result = runner.invoke(cli, [])
assert not result.exception
if result.exception:
raise result.exception
assert result.output == "42\n"


def test_other_command_forward(runner):
@pytest.mark.anyio
async def test_other_command_forward(runner):
cli = click.Group()

@cli.command()
@@ -32,16 +35,18 @@ def test(count):
@cli.command()
@click.option("--count", default=1)
@click.pass_context
def dist(ctx, count):
ctx.forward(test)
ctx.invoke(test, count=42)
async def dist(ctx, count):
await ctx.forward(test)
await ctx.invoke(test, count=42)

result = runner.invoke(cli, ["dist"])
assert not result.exception
result = await runner.invoke(cli, ["dist"], _sync=True)
if result.exception:
raise result.exception
assert result.output == "Count: 1\nCount: 42\n"


def test_forwarded_params_consistency(runner):
@pytest.mark.anyio
async def test_forwarded_params_consistency(runner):
cli = click.Group()

@cli.command()
@@ -54,12 +59,13 @@ def first(ctx, **kwargs):
@click.option("-a")
@click.option("-b")
@click.pass_context
def second(ctx, **kwargs):
async def second(ctx, **kwargs):
click.echo(f"{ctx.params}")
ctx.forward(first)
await ctx.forward(first)

result = runner.invoke(cli, ["second", "-a", "foo", "-b", "bar"])
assert not result.exception
if result.exception:
raise result.exception
assert result.output == "{'a': 'foo', 'b': 'bar'}\n{'a': 'foo', 'b': 'bar'}\n"


@@ -117,7 +123,8 @@ def foo(name):

result = runner.invoke(cli, ["foo"], default_map={"foo": {"name": "changed"}})

assert not result.exception
if result.exception:
raise result.exception
assert result.output == "changed\n"


@@ -158,7 +165,7 @@ def __init__(self, name, parser, callback):
self.parser = parser
self.callback = callback

def parse_args(self, ctx, args):
async def parse_args(self, ctx, args):
try:
opts, args = parser.parse_args(args)
except Exception as e:
@@ -172,8 +179,10 @@ def get_usage(self, ctx):
def get_help(self, ctx):
return self.parser.format_help()

def invoke(self, ctx):
ctx.invoke(self.callback, ctx.args, **ctx.params)
async def invoke(self, ctx):
rv = ctx.invoke(self.callback, ctx.args, **ctx.params)
if iscoroutine(rv):
await rv

parser = optparse.OptionParser(usage="Usage: foo test [OPTIONS]")
parser.add_option(
@@ -196,11 +205,13 @@ def test_callback(args, filename, verbose):
cli.add_command(OptParseCommand("test", parser, test_callback))

result = runner.invoke(cli, ["test", "-f", "f.txt", "-q", "q1.txt", "q2.txt"])
assert result.exception is None
if result.exception is not None:
raise result.exception
assert result.output.splitlines() == ["q1.txt q2.txt", "f.txt", "False"]

result = runner.invoke(cli, ["test", "--help"])
assert result.exception is None
if result.exception is not None:
raise result.exception
assert result.output.splitlines() == [
"Usage: foo test [OPTIONS]",
"",
@@ -255,10 +266,10 @@ def other_cmd(ctx, a, b, c):
def test_invoked_subcommand(runner):
@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
async def cli(ctx):
if ctx.invoked_subcommand is None:
click.echo("no subcommand, use default")
ctx.invoke(sync)
await ctx.invoke(sync)
else:
click.echo("invoke subcommand")

@@ -267,11 +278,13 @@ def sync():
click.echo("in subcommand")

result = runner.invoke(cli, ["sync"])
assert not result.exception
if result.exception:
raise result.exception
assert result.output == "invoke subcommand\nin subcommand\n"

result = runner.invoke(cli)
assert not result.exception
if result.exception:
raise result.exception
assert result.output == "no subcommand, use default\nin subcommand\n"


@@ -280,8 +293,8 @@ 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)
async def resolve_command(self, ctx, args):
_, command, args = await super().resolve_command(ctx, args)
return command.name, command, args

cli = AliasedGroup()
@@ -291,7 +304,8 @@ def push():
click.echo("push command")

result = runner.invoke(cli, ["pu", "--help"])
assert not result.exception
if result.exception:
raise result.exception
assert result.output.startswith("Usage: root push [OPTIONS]")


@@ -305,6 +319,138 @@ def test_group_add_command_name(runner):
assert result.exit_code == 0


@pytest.mark.parametrize(
("invocation_order", "declaration_order", "expected_order"),
[
# Non-eager options.
([], ["-a"], ["-a"]),
(["-a"], ["-a"], ["-a"]),
([], ["-a", "-c"], ["-a", "-c"]),
(["-a"], ["-a", "-c"], ["-a", "-c"]),
(["-c"], ["-a", "-c"], ["-c", "-a"]),
([], ["-c", "-a"], ["-c", "-a"]),
(["-a"], ["-c", "-a"], ["-a", "-c"]),
(["-c"], ["-c", "-a"], ["-c", "-a"]),
(["-a", "-c"], ["-a", "-c"], ["-a", "-c"]),
(["-c", "-a"], ["-a", "-c"], ["-c", "-a"]),
# Eager options.
([], ["-b"], ["-b"]),
(["-b"], ["-b"], ["-b"]),
([], ["-b", "-d"], ["-b", "-d"]),
(["-b"], ["-b", "-d"], ["-b", "-d"]),
(["-d"], ["-b", "-d"], ["-d", "-b"]),
([], ["-d", "-b"], ["-d", "-b"]),
(["-b"], ["-d", "-b"], ["-b", "-d"]),
(["-d"], ["-d", "-b"], ["-d", "-b"]),
(["-b", "-d"], ["-b", "-d"], ["-b", "-d"]),
(["-d", "-b"], ["-b", "-d"], ["-d", "-b"]),
# Mixed options.
([], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
(["-a"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
(["-b"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
(["-c"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-c", "-a"]),
(["-d"], ["-a", "-b", "-c", "-d"], ["-d", "-b", "-a", "-c"]),
(["-a", "-b"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
(["-b", "-a"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
(["-d", "-c"], ["-a", "-b", "-c", "-d"], ["-d", "-b", "-c", "-a"]),
(["-c", "-d"], ["-a", "-b", "-c", "-d"], ["-d", "-b", "-c", "-a"]),
(["-a", "-b", "-c", "-d"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
(["-b", "-d", "-a", "-c"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]),
([], ["-b", "-d", "-e", "-a", "-c"], ["-b", "-d", "-e", "-a", "-c"]),
(["-a", "-d"], ["-b", "-d", "-e", "-a", "-c"], ["-d", "-b", "-e", "-a", "-c"]),
(["-c", "-d"], ["-b", "-d", "-e", "-a", "-c"], ["-d", "-b", "-e", "-c", "-a"]),
],
)
def test_iter_params_for_processing(
invocation_order, declaration_order, expected_order
):
parameters = {
"-a": click.Option(["-a"]),
"-b": click.Option(["-b"], is_eager=True),
"-c": click.Option(["-c"]),
"-d": click.Option(["-d"], is_eager=True),
"-e": click.Option(["-e"], is_eager=True),
}

invocation_params = [parameters[opt_id] for opt_id in invocation_order]
declaration_params = [parameters[opt_id] for opt_id in declaration_order]
expected_params = [parameters[opt_id] for opt_id in expected_order]

assert (
click.core.iter_params_for_processing(invocation_params, declaration_params)
== expected_params
)


def test_help_param_priority(runner):
"""Cover the edge-case in which the eagerness of help option was not
respected, because it was internally generated multiple times.
See: https://github.com/pallets/click/pull/2811
"""

def print_and_exit(ctx, param, value):
if value:
click.echo(f"Value of {param.name} is: {value}")
ctx.exit()

@click.command(context_settings={"help_option_names": ("--my-help",)})
@click.option("-a", is_flag=True, expose_value=False, callback=print_and_exit)
@click.option(
"-b", is_flag=True, expose_value=False, callback=print_and_exit, is_eager=True
)
def cli():
pass

# --my-help is properly called and stop execution.
result = runner.invoke(cli, ["--my-help"])
assert "Value of a is: True" not in result.stdout
assert "Value of b is: True" not in result.stdout
assert "--my-help" in result.stdout
assert result.exit_code == 0

# -a is properly called and stop execution.
result = runner.invoke(cli, ["-a"])
assert "Value of a is: True" in result.stdout
assert "Value of b is: True" not in result.stdout
assert "--my-help" not in result.stdout
assert result.exit_code == 0

# -a takes precedence over -b and stop execution.
result = runner.invoke(cli, ["-a", "-b"])
assert "Value of a is: True" not in result.stdout
assert "Value of b is: True" in result.stdout
assert "--my-help" not in result.stdout
assert result.exit_code == 0

# --my-help is eager by default so takes precedence over -a and stop
# execution, whatever the order.
for args in [["-a", "--my-help"], ["--my-help", "-a"]]:
result = runner.invoke(cli, args)
assert "Value of a is: True" not in result.stdout
assert "Value of b is: True" not in result.stdout
assert "--my-help" in result.stdout
assert result.exit_code == 0

# Both -b and --my-help are eager so they're called in the order they're
# invoked by the user.
result = runner.invoke(cli, ["-b", "--my-help"])
assert "Value of a is: True" not in result.stdout
assert "Value of b is: True" in result.stdout
assert "--my-help" not in result.stdout
assert result.exit_code == 0

# But there was a bug when --my-help is called before -b, because the
# --my-help option created by click via help_option_names is internally
# created twice and is not the same object, breaking the priority order
# produced by iter_params_for_processing.
result = runner.invoke(cli, ["--my-help", "-b"])
assert "Value of a is: True" not in result.stdout
assert "Value of b is: True" not in result.stdout
assert "--my-help" in result.stdout
assert result.exit_code == 0


def test_unprocessed_options(runner):
@click.command(context_settings=dict(ignore_unknown_options=True))
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
@@ -314,7 +460,8 @@ def cli(verbose, args):
click.echo(f"Args: {'|'.join(args)}")

result = runner.invoke(cli, ["-foo", "-vvvvx", "--muhaha", "x", "y", "-x"])
assert not result.exception
if result.exception:
raise result.exception
assert result.output.splitlines() == [
"Verbosity: 4",
"Args: -foo|-x|--muhaha|x|y|-x",
@@ -340,20 +487,22 @@ def deprecated_cmd():
assert "DeprecationWarning:" in result.output


def test_command_parse_args_collects_option_prefixes():
@pytest.mark.anyio
async def test_command_parse_args_collects_option_prefixes():
@click.command()
@click.option("+p", is_flag=True)
@click.option("!e", is_flag=True)
def test(p, e):
pass

ctx = click.Context(test)
test.parse_args(ctx, [])
await test.parse_args(ctx, [])

assert ctx._opt_prefixes == {"-", "--", "+", "!"}


def test_group_parse_args_collects_base_option_prefixes():
@pytest.mark.anyio
async def test_group_parse_args_collects_base_option_prefixes():
@click.group()
@click.option("~t", is_flag=True)
def group(t):
@@ -370,7 +519,7 @@ def command2(e):
pass

ctx = click.Context(group)
group.parse_args(ctx, ["command1", "+p"])
await group.parse_args(ctx, ["command1", "+p"])

assert ctx._opt_prefixes == {"-", "--", "~"}

2 changes: 1 addition & 1 deletion tests/test_compat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from click._compat import should_strip_ansi
from asyncclick._compat import should_strip_ansi


def test_is_jupyter_kernel_output():
81 changes: 67 additions & 14 deletions tests/test_context.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from contextlib import asynccontextmanager
from contextlib import contextmanager

import pytest

import click
from click.core import ParameterSource
from click.decorators import pass_meta_key
import asyncclick as click
from asyncclick.core import ParameterSource
from asyncclick.decorators import pass_meta_key


def test_ensure_context_objects(runner):
@@ -105,18 +106,19 @@ def test_multi_enter(runner):

@click.command()
@click.pass_context
def cli(ctx):
async def cli(ctx):
def callback():
called.append(True)

ctx.call_on_close(callback)

with ctx:
async with ctx:
pass
assert not called

result = runner.invoke(cli, [])
assert result.exception is None
if result.exception:
raise result.exception
assert called == [True]


@@ -174,7 +176,8 @@ def test_make_pass_meta_decorator_doc():
assert "passes the test value" in pass_value.__doc__


def test_context_pushing():
@pytest.mark.anyio
async def test_context_pushing():
rv = []

@click.command()
@@ -187,19 +190,67 @@ def cli():
def test_callback():
rv.append(42)

with ctx.scope(cleanup=False):
async with ctx.scope(cleanup=False):
# Internal
assert ctx._depth == 2

assert rv == []

with ctx.scope():
async with ctx.scope():
# Internal
assert ctx._depth == 1

assert rv == [42]


@pytest.mark.anyio
async def test_async_context_mgr():
@asynccontextmanager
async def manager():
val = [1]
yield val
val[0] = 0

@click.command()
def cli():
pass

ctx = click.Context(cli)

async with ctx.scope():
rv = await ctx.with_async_resource(manager())
assert rv[0] == 1, rv

# Internal
assert ctx._depth == 1

assert rv == [0], rv


@pytest.mark.anyio
async def test_context_mgr():
@contextmanager
def manager():
val = [1]
yield val
val[0] = 0

@click.command()
def cli():
pass

ctx = click.Context(cli)

async with ctx.scope():
rv = ctx.with_resource(manager())
assert rv[0] == 1, rv

# Internal
assert ctx._depth == 1

assert rv == [0], rv


def test_pass_obj(runner):
@click.group()
@click.pass_context
@@ -237,7 +288,8 @@ def foo():
assert called == [True]


def test_with_resource():
@pytest.mark.anyio
async def test_with_resource():
@contextmanager
def manager():
val = [1]
@@ -246,7 +298,7 @@ def manager():

ctx = click.Context(click.Command("test"))

with ctx.scope():
async with ctx.scope():
rv = ctx.with_resource(manager())
assert rv[0] == 1

@@ -304,20 +356,21 @@ def test_propagate_show_default_setting(runner):
assert "[default: a]" in result.output


def test_exit_not_standalone():
@pytest.mark.anyio
async def test_exit_not_standalone():
@click.command()
@click.pass_context
def cli(ctx):
ctx.exit(1)

assert cli.main([], "test_exit_not_standalone", standalone_mode=False) == 1
assert await cli.main([], "test_exit_not_standalone", standalone_mode=False) == 1

@click.command()
@click.pass_context
def cli(ctx):
ctx.exit(0)

assert cli.main([], "test_exit_not_standalone", standalone_mode=False) == 0
assert await cli.main([], "test_exit_not_standalone", standalone_mode=False) == 0


@pytest.mark.parametrize(
13 changes: 8 additions & 5 deletions tests/test_custom_classes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import click
import asyncclick as click

import pytest

def test_command_context_class():

@pytest.mark.anyio
async def test_command_context_class():
"""A command with a custom ``context_class`` should produce a
context using that type.
"""
@@ -13,7 +16,7 @@ class CustomCommand(click.Command):
context_class = CustomContext

command = CustomCommand("test")
context = command.make_context("test", [])
context = await command.make_context("test", [])
assert isinstance(context, CustomContext)


@@ -37,9 +40,9 @@ def second(ctx, first_id):

@click.command(cls=CustomCommand)
@click.pass_context
def first(ctx):
async def first(ctx):
assert isinstance(ctx, CustomContext)
ctx.invoke(second, first_id=id(ctx))
await ctx.invoke(second, first_id=id(ctx))

assert not runner.invoke(first).exception

2 changes: 1 addition & 1 deletion tests/test_defaults.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import click
import asyncclick as click


def test_basic_defaults(runner):
2 changes: 1 addition & 1 deletion tests/test_formatting.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import click
import asyncclick as click


def test_basic_functionality(runner):
12 changes: 7 additions & 5 deletions tests/test_imports.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
import subprocess
import sys

from click._compat import WIN
from asyncclick._compat import WIN

IMPORT_TEST = b"""\
import builtins
@@ -14,18 +14,19 @@
def tracking_import(module, locals=None, globals=None, fromlist=None,
level=0):
rv = real_import(module, locals, globals, fromlist, level)
if globals and globals['__name__'].startswith('click') and level == 0:
if globals and '__name__' in globals and globals['__name__'].startswith('asyncclick') and level == 0:
found_imports.add(module)
return rv
builtins.__import__ = tracking_import
import click
import asyncclick
rv = list(found_imports)
import json
click.echo(json.dumps(rv))
asyncclick.echo(json.dumps(rv))
"""

ALLOWED_IMPORTS = {
"anyio",
"weakref",
"os",
"struct",
@@ -47,6 +48,7 @@ def tracking_import(module, locals=None, globals=None, fromlist=None,
"typing",
"types",
"gettext",
"shutil",
}

if WIN:
@@ -62,6 +64,6 @@ def test_light_imports():
imported = json.loads(rv)

for module in imported:
if module == "click" or module.startswith("click."):
if module == "asyncclick" or module.startswith("asyncclick."):
continue
assert module in ALLOWED_IMPORTS
23 changes: 14 additions & 9 deletions tests/test_info_dict.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

import click.types
import asyncclick as click
import asyncclick.types

# Common (obj, expect) pairs used to construct multiple tests.
STRING_PARAM_TYPE = (click.STRING, {"param_type": "String", "name": "text"})
@@ -208,8 +209,9 @@
pytest.param(*NAME_ARGUMENT, id="Argument"),
],
)
def test_parameter(obj, expect):
out = obj.to_info_dict()
@pytest.mark.anyio
async def test_parameter(obj, expect):
out = await obj.to_info_dict()
assert out == expect


@@ -249,15 +251,17 @@ def test_parameter(obj, expect):
),
],
)
def test_command(obj, expect):
@pytest.mark.anyio
async def test_command(obj, expect):
ctx = click.Context(obj)
out = obj.to_info_dict(ctx)
out = await obj.to_info_dict(ctx)
assert out == expect


def test_context():
@pytest.mark.anyio
async def test_context():
ctx = click.Context(HELLO_COMMAND[0])
out = ctx.to_info_dict()
out = await ctx.to_info_dict()
assert out == {
"command": HELLO_COMMAND[1],
"info_name": None,
@@ -268,8 +272,9 @@ def test_context():
}


def test_paramtype_no_name():
@pytest.mark.anyio
async def test_paramtype_no_name():
class TestType(click.ParamType):
pass

assert TestType().to_info_dict()["name"] == "TestType"
assert (await TestType().to_info_dict())["name"] == "TestType"
2 changes: 1 addition & 1 deletion tests/test_normalization.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import click
import asyncclick as click

CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.lower())

8 changes: 4 additions & 4 deletions tests/test_options.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import os
import re
import os

import pytest
import asyncclick as click

import click
from click import Option
import pytest

from asyncclick import Option

def test_prefixes(runner):
@click.command()
6 changes: 3 additions & 3 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import pytest

import click
from click.parser import OptionParser
from click.parser import split_arg_string
import asyncclick as click
from asyncclick.parser import OptionParser
from asyncclick.parser import split_arg_string


@pytest.mark.parametrize(
244 changes: 134 additions & 110 deletions tests/test_shell_completion.py

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions tests/test_termui.py
Original file line number Diff line number Diff line change
@@ -3,8 +3,9 @@

import pytest

import click._termui_impl
from click._compat import WIN
import asyncclick as click
import asyncclick._termui_impl
from asyncclick._compat import WIN


class FakeClock:
@@ -268,7 +269,7 @@ def cli():
print("")

monkeypatch.setattr(time, "time", fake_clock.time)
monkeypatch.setattr(click._termui_impl, "isatty", lambda _: True)
monkeypatch.setattr(asyncclick._termui_impl, "isatty", lambda _: True)
output = runner.invoke(cli, []).output

lines = [line for line in output.split("\n") if "[" in line]
@@ -330,8 +331,8 @@ def test_progress_bar_update_min_steps(runner):
@pytest.mark.parametrize("echo", [True, False])
@pytest.mark.skipif(not WIN, reason="Tests user-input using the msvcrt module.")
def test_getchar_windows(runner, monkeypatch, key_char, echo):
monkeypatch.setattr(click._termui_impl.msvcrt, "getwche", lambda: key_char)
monkeypatch.setattr(click._termui_impl.msvcrt, "getwch", lambda: key_char)
monkeypatch.setattr(asyncclick._termui_impl.msvcrt, "getwche", lambda: key_char)
monkeypatch.setattr(asyncclick._termui_impl.msvcrt, "getwch", lambda: key_char)
monkeypatch.setattr(click.termui, "_getchar", None)
assert click.getchar(echo) == key_char

@@ -345,7 +346,7 @@ def test_getchar_windows(runner, monkeypatch, key_char, echo):
def test_getchar_special_key_windows(runner, monkeypatch, special_key_char, key_char):
ordered_inputs = [key_char, special_key_char]
monkeypatch.setattr(
click._termui_impl.msvcrt, "getwch", lambda: ordered_inputs.pop()
asyncclick._termui_impl.msvcrt, "getwch", lambda: ordered_inputs.pop()
)
monkeypatch.setattr(click.termui, "_getchar", None)
assert click.getchar() == f"{special_key_char}{key_char}"
@@ -356,7 +357,7 @@ def test_getchar_special_key_windows(runner, monkeypatch, special_key_char, key_
)
@pytest.mark.skipif(not WIN, reason="Tests user-input using the msvcrt module.")
def test_getchar_windows_exceptions(runner, monkeypatch, key_char, exc):
monkeypatch.setattr(click._termui_impl.msvcrt, "getwch", lambda: key_char)
monkeypatch.setattr(asyncclick._termui_impl.msvcrt, "getwch", lambda: key_char)
monkeypatch.setattr(click.termui, "_getchar", None)

with pytest.raises(exc):
183 changes: 109 additions & 74 deletions tests/test_testing.py

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions tests/test_types.py
Original file line number Diff line number Diff line change
@@ -5,8 +5,8 @@

import pytest

import click
from click import FileError
import asyncclick as click
from asyncclick import FileError


@pytest.mark.parametrize(
16 changes: 8 additions & 8 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -6,9 +6,10 @@

import pytest

import click._termui_impl
import click.utils
from click._compat import WIN
import asyncclick as click
import asyncclick._termui_impl
import asyncclick.utils
from asyncclick._compat import WIN


def test_echo(runner):
@@ -161,7 +162,7 @@ def test_prompts_abort(monkeypatch, capsys):
def f(_):
raise KeyboardInterrupt()

monkeypatch.setattr("click.termui.hidden_prompt_func", f)
monkeypatch.setattr("asyncclick.termui.hidden_prompt_func", f)

try:
click.prompt("Password", hide_input=True)
@@ -179,7 +180,6 @@ def _test_gen_func():
yield "abc"


@pytest.mark.skipif(WIN, reason="Different behavior on windows.")
@pytest.mark.parametrize("cat", ["cat", "cat ", "cat "])
@pytest.mark.parametrize(
"test",
@@ -196,7 +196,7 @@ def _test_gen_func():
)
def test_echo_via_pager(monkeypatch, capfd, cat, test):
monkeypatch.setitem(os.environ, "PAGER", cat)
monkeypatch.setattr(click._termui_impl, "isatty", lambda x: True)
monkeypatch.setattr(asyncclick._termui_impl, "isatty", lambda x: True)

expected_output = test[0]
test_input = test[1]()
@@ -427,7 +427,7 @@ def test_iter_keepopenfile(tmpdir):
p = tmpdir.mkdir("testdir").join("testfile")
p.write("\n".join(expected))
with p.open() as f:
for e_line, a_line in zip(expected, click.utils.KeepOpenFile(f)):
for e_line, a_line in zip(expected, asyncclick.utils.KeepOpenFile(f)):
assert e_line == a_line.strip()


@@ -436,7 +436,7 @@ def test_iter_lazyfile(tmpdir):
p = tmpdir.mkdir("testdir").join("testfile")
p.write("\n".join(expected))
with p.open() as f:
with click.utils.LazyFile(f.name) as lf:
with asyncclick.utils.LazyFile(f.name) as lf:
for e_line, a_line in zip(expected, lf):
assert e_line == a_line.strip()

2 changes: 1 addition & 1 deletion tests/typing/typing_aliased_group.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@

from typing_extensions import assert_type

import click
import asyncclick as click


class AliasedGroup(click.Group):
2 changes: 1 addition & 1 deletion tests/typing/typing_confirmation_option.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

from typing_extensions import assert_type

import click
import asyncclick as click


@click.command()
2 changes: 1 addition & 1 deletion tests/typing/typing_group_kw_options.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing_extensions import assert_type

import click
import asyncclick as click


@click.group(context_settings={})
2 changes: 1 addition & 1 deletion tests/typing/typing_help_option.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing_extensions import assert_type

import click
import asyncclick as click


@click.command()
2 changes: 1 addition & 1 deletion tests/typing/typing_options.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

from typing_extensions import assert_type

import click
import asyncclick as click


@click.command()
2 changes: 1 addition & 1 deletion tests/typing/typing_password_option.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

from typing_extensions import assert_type

import click
import asyncclick as click


@click.command()
2 changes: 1 addition & 1 deletion tests/typing/typing_simple_example.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

from typing_extensions import assert_type

import click
import asyncclick as click


@click.command()
2 changes: 1 addition & 1 deletion tests/typing/typing_version_option.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@

from typing_extensions import assert_type

import click
import asyncclick as click


@click.command()
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
envlist =
py3{13,12,11,10,9,8,7}
py3{13,12,11,10,9}
pypy310
style
typing
@@ -28,7 +28,7 @@ deps = -r requirements/typing.txt
commands =
mypy
pyright tests/typing
pyright --verifytypes click --ignoreexternal
pyright --verifytypes asyncclick --ignoreexternal

[testenv:docs]
deps = -r requirements/docs.txt